railiance-infra/docs/evidence/RAIL-HO-WP-0009-T03-declared-vs-live-audit.md
codex 4d9e77c968
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Close RAIL-HO-WP-0009 declared-state gaps; leave live 6443 prune gated
Make the k3s API tunnel-only (ADR-005), stop declaring Flannel VXLAN
open to Anywhere, tag the base role so firewall can be scoped, and
schedule the Goss declared-vs-live check. CoulombCore sets ufw_manage
false so a converge cannot enable UFW there. T02 still needs operator
approval for make converge-firewall HOST=Railiance01.
2026-08-15 15:41:59 +02:00

5.9 KiB

RAIL-HO-WP-0009-T03 — declared-vs-live audit

Audited 2026-08-15 from the workstation against Railiance01 (92.205.62.239) and CoulombCore (92.205.130.254). SSH as tegwick. This is a record of what was found; absence of drift is recorded too.

Method

Compared ansible/roles/base/tasks/main.yml, sibling bootstrap roles, and spec/server-baseline.yaml to live ufw status, sshd -T, fail2ban, sudoers, listening sockets, systemd user-slice, nproc, swappiness, and timezone.

Matches declaration

Surface Railiance01 CoulombCore
sshd drop-in 10-hardening.conf present; root/password off, pubkey on same
sshd -T effective matches drop-in matches drop-in
fail2ban sshd jail active active
/etc/profile.d/histcontrol.sh ignorespace ignorespace
sudoers tegwick NOPASSWD present present
OpenSSH UFW ALLOW Anywhere UFW inactive (see below)

Divergences

1. k3s API allowlist (Railiance01) — same defect class as T01

Live 6443 grants on 2026-08-15:

From Declared?
89.244.90.236 was declared; now revoked by ADR-005
89.244.90.255 was declared; now revoked by ADR-005
89.244.90.248 never declared — third hand grant

No blanket Anywhere rule. Stale grants from T02 (.246, .102) are gone.

.248 appeared after the 2026-08-12 session that was already fixing allowlist drift. Goss as of T01 would not have failed: it asserted declared sources present, not that the live set equalled the declared set.

Workstation egress on this audit was 85.132.220.102 — already on the revoked list. That is the allowlist treadmill in one line.

2. Flannel VXLAN declaration weaker than needed (T06)

8472/udp listens on 0.0.0.0 on both hosts. Railiance01 has no UFW rule for it (default deny incoming covers it). The role used to declare 8472/udp ALLOW with no source restriction, so a converge would have opened VXLAN to the internet. Fixed: empty flannel_vxlan_allowed_sources, blanket rule deleted if present.

3. Port 2224/tcp Anywhere — intended, now declared

Both hosts run nydus-ex-api on 0.0.0.0:2224. Railiance01 UFW allows it Anywhere with comment nydus-ex-api dashboard agent. This is the HostEurope Nydus agent (dashboard, root-password reset, backups); see hosteurope/260308-dependency-nydus.md. Removing it would break the provider. Declared as ufw_extra_allowed. Source-restricting it is not possible without HostEurope's egress list.

4. CoulombCore: UFW inactive, but not "no firewall"

ufw statusStatus: inactive. Live filter is iptables INPUT DROP with a Plesk-era accept list: 22, 80, 443, 2224, mail, 3306, 5432, 53, 445/139, and tcp 49152-65535. 6443 and 8472 are not in that accept list, so they are dropped even with UFW off.

Converging the base role would enable UFW, allow only SSH + (now empty) k3s

  • nydus, and take down 80/443 and the rest of the accepted surface. ufw_manage: false on CoulombCore prevents that. Enabling UFW there is its own decision and needs a declared accept list first.

5. Resource limits and swap — applied on CoulombCore, never on Railiance01

Railiance01 CoulombCore
user-1000.slice MemoryMax absent 1500M / swap 512M (Ansible)
PAM nproc no limits.d files 60-nproc-tegwick.conf
swappiness 60 (kernel default) 10
timezone Etc/UTC Etc/UTC

The user-slice cap can OOM agent workloads (T02). Do not apply it to Railiance01 without a separate decision. Role tags now isolate it (resource_limits, swap, timezone).

inventory/group_vars/all.yaml sets timezone: Europe/Berlin but that file is not loaded by the Ansible inventory script. The role default is UTC, which matches live. Leave it; do not start loading that file as a side effect.

6. Inventory host_vars path bug

ansible/inventory_from_yaml.py loaded inventory/host_vars/<name>.yml (does not exist). The real file is ansible/inventory/host_vars/CoulombCore.yml. Ansible does not auto-load host_vars next to a script inventory, so ufw_manage, swap, and resource-limit overrides would not reach the play. Fixed: the script now reads ansible/inventory/host_vars/ first.

7. sshd restart was an unconditioned task

Restart sshd ran on every converge, which is why --check reported an sshd restart among the 11 changes. It is now a handler notified only when the hardening drop-in changes.

8. Undeclared listening sockets (not opened in UFW)

On Railiance01, not in this role, UFW default-deny so not currently public:

  • *:6443 k3s-server (the API this workplan closes)
  • 92.205.62.239:2379 / :2380 etcd on the public address — S2 concern
  • *:10250 kubelet — S2 concern
  • 0.0.0.0:18081 Python reverse proxy to 127.0.0.1:18765 (issue-core), started 2026-07-26. No UFW rule. Out of scope here; record only.

On CoulombCore, *:6443 and 0.0.0.0:8472 listen; iptables INPUT DROP without an accept for those ports is what currently protects them, not UFW.

9. Routed policy

Railiance01 UFW default routed is deny. The role used to set routed allow for Flannel. Routed allow is now applied only when flannel_vxlan_allowed_sources is non-empty (multi-node). Single-node converge will not flip it.

Decisions taken in this task

  • Declare Nydus 2224; do not remove it.
  • Do not enable UFW on CoulombCore (ufw_manage: false).
  • Do not apply Railiance01 resource limits / swappiness / timezone as part of this workplan.
  • Tag the base role and sibling bootstrap roles so firewall can be scoped.
  • Fix the host_vars loader.

Still operator decisions (not this workplan)

  • Enable UFW on CoulombCore, with a declared accept list that includes 80/443.
  • Apply or drop the declared user-slice memory cap on Railiance01.
  • Whether etcd/kubelet listening on the public address should be rebound (railiance-cluster).
  • What the 0.0.0.0:18081 issue-core proxy is for, and whether it stays.