railiance-infra/docs/verification.md
codex c84fe7a3de
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Make the k3s API firewall allowlist declarative
The live host restricted 6443/tcp to specific operator addresses, added by hand,
while this role still declared the port open to Anywhere with no source
restriction. The declared config was weaker than reality: running the base role
would have REMOVED the restriction and exposed the Kubernetes API to the
internet. Security was tightened on the host and never fed back into the source
of truth.

Found 2026-08-11 while diagnosing lost cluster access, which turned out to be an
ISP lease rotation (89.244.90.246 -> .236) against a hand-maintained allowlist.

Changes:
- defaults: k3s_api_allowed_sources (empty = 6443 closed to all external
  sources, the safe failure; SSH unaffected so the host stays recoverable) and
  k3s_api_revoked_sources, so rotated addresses are pruned rather than left as
  standing grants to whoever the ISP reassigns them to
- tasks: grant approved sources, then remove any blanket rule, then revoke
  retired ones. Order matters - grants are added before the blanket rule is
  deleted so convergence never opens a window with no API access
- group_vars/all.yaml: the current operator address, plus the two stale grants
  (.246 rotated, 85.132.220.102 historic) marked for revocation
- docs/verification.md: state that 6443 is source-restricted rather than
  listing it as a plainly allowed port

Not yet converged against the live host - the role change is committed but
running it is a production action needing operator approval.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:56:28 +02:00

83 lines
3.2 KiB
Markdown

# Server Verification
`railiance-infra` ships a declarative baseline spec and a Goss test suite that
asserts every managed node matches it. This replaces manual spot-checks with
a reproducible, CI-friendly pass/fail verdict.
## The spec
`spec/server-baseline.yaml` is the single source of truth for the target state
of every managed node. It covers:
- **Firewall** — UFW active, default deny inbound, required ports allowed
(SSH 22/tcp, Flannel VXLAN 8472/udp). The k3s API (6443/tcp) is
**source-restricted**, not world-open: only addresses in
`k3s_api_allowed_sources` may reach it, and `k3s_api_revoked_sources` is
pruned on convergence. A host with an empty allowlist has 6443 closed to all
external sources — the safe failure, recoverable over SSH.
- **SSH daemon** — root login disabled, password auth disabled, pubkey auth enabled
- **Services** — ufw, fail2ban, ssh.socket enabled and running
- **Packages** — ufw, fail2ban, git, curl, vim, htop (age and sops installed as binaries)
- **Users** — admin user with bash shell and passwordless sudo
- **Security** — fail2ban sshd jail active, HISTCONTROL=ignorespace in /etc/profile.d/
When you change the desired state of a node, update this file first. Then
update the Ansible role **and** the Goss tests to match.
## Running verification
```bash
make verify
```
This runs `ansible/playbooks/verify.yaml` against all hosts. The playbook:
1. Downloads the Goss binary (pinned version) to `/usr/local/bin/goss`
2. Copies `goss/baseline.yaml` to `/etc/goss/baseline.yaml` on each host
3. Runs `goss validate --format tap`
4. Fails the play (non-zero exit) if any assertion fails
5. Fetches the TAP report to `reports/goss-<host>-<timestamp>.tap`
6. Auto-commits the report to git
**All assertions passed** → exit 0
**One or more assertions FAILED** → exit non-zero, TAP report in `reports/`
## After convergence
The standard workflow after converging a new or updated node:
```bash
make converge # bring the node to the desired state
make verify # assert it got there
```
Run `make status` for a quick human-readable summary; run `make verify` when
you need a structured, automatable check.
## Goss test file
`goss/baseline.yaml` contains one Goss assertion per spec item. The mapping is:
| spec section | Goss resource |
|---|---|
| `firewall` | `command: ufw status` stdout patterns |
| `ssh` | `file: /etc/ssh/sshd_config.d/10-hardening.conf` contains |
| `services` | `service:` blocks |
| `packages` | `package:` blocks |
| `users` | `user:` block + `command: grep NOPASSWD` |
| `security.histcontrol` | `command: grep -r HISTCONTROL /etc/profile.d/` |
| `security.fail2ban_jails` | `command: fail2ban-client status sshd` |
| `age`, `sops` (binary installs) | `command: test -x /usr/local/bin/{age,sops}` |
## Adding new assertions
1. Add the desired state to `spec/server-baseline.yaml`
2. Add the Ansible task to `ansible/roles/base/tasks/main.yml`
3. Add the Goss assertion to `goss/baseline.yaml`
4. Run `make converge && make verify` to confirm
## Reports
TAP reports are committed to `reports/` after each `make verify` run.
They are machine-readable and suitable for CI pipelines. A cleanup policy
for old reports is tracked as extension point EP `78ef4879`.