Open RAIL-HO-WP-0009: firewall declared-state integrity and k3s API exposure
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Records the security defect found 2026-08-11: this repo declared 6443/tcp open
to Anywhere while the live host was source-restricted by hand, so converging the
base role would have exposed the Kubernetes API to the internet. A hardening run
that de-hardens, undetected because nothing compares declared UFW state to live
UFW state.

T01 (declarative allowlist) is done. T02 converges it - production action,
approval required, and until it runs the host still carries two stale grants to
addresses the ISP may have reassigned. T03 audits the role for the same class of
defect and asks about the undeclared 2224/tcp open to Anywhere. T04 proposes
removing the API from the public internet via the ops-bridge tunnel pattern
already documented for CoulombCore. T05 routes a declared-vs-live conformance
check to the unowned Q7 loop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
codex 2026-08-12 00:01:39 +02:00
parent c84fe7a3de
commit f4a7805cca

View file

@ -0,0 +1,160 @@
---
id: RAIL-HO-WP-0009
type: workplan
title: "Firewall declared-state integrity and k3s API exposure"
domain: financials
repo: railiance-infra
status: active
owner: codex
topic_slug: railiance
created: "2026-08-12"
updated: "2026-08-12"
related_repos:
- railiance-cluster
- railiance-platform
- railiance-master
---
# RAIL-HO-WP-0009 - Firewall declared-state integrity and k3s API exposure
## Goal
Close a security defect in which this repo's declared firewall configuration was
**weaker** than the live host, and remove the recurring exposure that produced
it.
## What was found
Discovered 2026-08-11 while diagnosing lost cluster access. The lost access was
incidental; the defect underneath was not.
**The defect.** `ansible/roles/base/tasks/main.yml` declared `6443/tcp` allowed
with no source restriction:
```yaml
- name: Allow k3s API in UFW
ansible.builtin.ufw:
rule: allow
port: '6443'
proto: tcp
```
The live host had **source-restricted** rules, added by hand with operator
comments (`k3s-api-operator-current`, `k3s-api-operator-hist`). Security was
tightened on the host and never fed back into the source of truth.
Consequence: **running this role would have removed the restriction and exposed
the Kubernetes API to the internet.** A convergence run intended to harden the
host would have de-hardened it, silently, with no failure to notice.
**Why it went undetected.** Nothing compares declared UFW state to live UFW
state. The divergence was security-relevant, had existed for some time, and
surfaced only because an ISP lease rotation (`89.244.90.246``.236`) happened
to break operator access mid-session.
**Contradiction with our own INTENT.** This repo declares "Declarative and
Reproducible — no irreproducible, hand-tuned hosts" and "Hardened by Default".
The hardening that actually protected the API was precisely the part that was
not declarative.
**Stale grants.** Two standing allowlist entries pointed at addresses no longer
ours: `89.244.90.246` (rotated) and `85.132.220.102` (historic). Dynamic ISP
addresses get reassigned, so an abandoned grant becomes a grant to a stranger.
## Boundaries
This workplan may:
- change firewall declarations and their inventory variables in this repo
- converge the base role against managed hosts, with operator approval
- propose the tunnel-based API access pattern and implement it here
It must not:
- change k3s or cluster runtime configuration (that is `railiance-cluster`, S2)
- silently widen any listening surface
- leave a host unreachable over SSH
## Tasks
```task
id: RAIL-HO-WP-0009-T01
status: done
priority: high
```
Make the allowlist declarative. Add `k3s_api_allowed_sources` (empty default —
6443 closed to all external sources, the safe failure, SSH unaffected so hosts
stay recoverable) and `k3s_api_revoked_sources` so rotated addresses are pruned
rather than left standing. Grant approved sources **before** deleting the
blanket rule so convergence never opens a window without API access. Declare the
current operator address and the two stale grants in `group_vars/all.yaml`;
correct `docs/verification.md` to state that 6443 is source-restricted.
Committed 2026-08-11. Access restored on the live host by hand in the same
session (`89.244.90.236` granted; `kubectl` verified, node Ready v1.35.1+k3s1).
```task
id: RAIL-HO-WP-0009-T02
status: todo
priority: high
```
Converge the base role against `railiance01` and confirm the resulting UFW state
matches the declaration exactly: the current operator source allowed, no blanket
`Anywhere` rule on 6443, and both revoked addresses gone. **Production action —
operator approval required before running.** Until this runs, the live host
still carries the two stale grants.
Verify SSH remains available throughout, and re-check `kubectl get nodes` after.
```task
id: RAIL-HO-WP-0009-T03
status: todo
priority: high
```
Audit the rest of the base role for the same class of defect: any place where
the declared state is weaker than, or simply divergent from, what was applied by
hand. UFW was found by accident; sshd config, fail2ban jails, sudoers, and
listening services deserve a deliberate pass. Record what is found even where no
change is needed — the absence of drift is itself evidence worth having.
Note during the same session: port `2224/tcp` is open to Anywhere on
`railiance01` (comment `nydus-ex-api dashboard agent`) and does not appear in
this role at all. Establish whether it is intended, and either declare it or
remove it.
```task
id: RAIL-HO-WP-0009-T04
status: todo
priority: medium
```
Remove the k3s API from the public internet. Operator addresses rotate, so an
allowlist is a treadmill: it will drift again, and each drift is either an
outage or a stale grant. `docs/deploy-stack.md` already documents API access
over the ops-bridge SSH tunnel (`k3s-api-coulombcore`, local port 16443) — apply
the same pattern to `railiance01` and reduce the public allowlist to nothing.
Decide explicitly rather than by default: this trades convenience for exposure,
and the tunnel becomes a dependency of every operator action.
```task
id: RAIL-HO-WP-0009-T05
status: todo
priority: medium
```
Propose a declared-vs-live conformance check for firewall state, and route it to
whoever owns the conformance loop. This defect is a concrete instance of the
unowned **Q7 Governance and Change Management** gap recorded in
`railiance-platform/ArchitectureBlueprint.md` §5.3 — a UFW declared-vs-live
comparison is a small, sharp first thing for that loop to do.
The existing Goss verification suite is the natural home for the check itself;
what is missing is the loop that runs it and reacts.
## Outcome
Pending. T01 done; the live host is reachable but not yet converged.