diff --git a/workplans/RAIL-HO-WP-0009-firewall-declared-state-and-api-exposure.md b/workplans/RAIL-HO-WP-0009-firewall-declared-state-and-api-exposure.md new file mode 100644 index 0000000..984ab11 --- /dev/null +++ b/workplans/RAIL-HO-WP-0009-firewall-declared-state-and-api-exposure.md @@ -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.