# 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 status` → `Status: 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/.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.