Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02994-7685-7940-bf34-3555b8256018
115 lines
4.8 KiB
Markdown
115 lines
4.8 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 executable through
|
|
`scripts/baseline_contract.py`. The dynamic inventory resolves its selected
|
|
profile into the variables consumed by both Ansible and Goss. It covers:
|
|
|
|
- **Firewall** — UFW active, default deny inbound, required ports allowed
|
|
(SSH 22/tcp; HostEurope Nydus 2224/tcp). The k3s API (6443/tcp) is
|
|
**tunnel-only** (ADR-005): `k3s_api_allowed_sources` is empty, and
|
|
`k3s_api_revoked_sources` is pruned on a firewall-tagged converge. Flannel
|
|
VXLAN (8472/udp) is omitted while the cluster is single-node; peer
|
|
addresses go in `flannel_vxlan_allowed_sources` when a second node appears.
|
|
`CoulombCore` selects an explicit external-firewall profile and verifies its
|
|
replacement INPUT default-drop control without rewriting it. A host with an
|
|
empty 6443 allowlist is 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 governed desired state, update this file. The consumers use the
|
|
same resolved variables; `make validate-baseline` fails if either consumer is
|
|
disconnected from the contract.
|
|
|
|
## Running verification
|
|
|
|
```bash
|
|
make verify
|
|
```
|
|
|
|
This runs `ansible/playbooks/verify.yaml` against all hosts. The playbook:
|
|
|
|
1. Reads the installed Goss binary and `/etc/goss/baseline.yaml` metadata.
|
|
2. Renders the selected profile on the controller and requires its SHA-256
|
|
digest to match the installed baseline exactly.
|
|
3. Runs the fixed `goss validate --format tap` argument vector.
|
|
4. Fails the play if the surface is missing/stale or any assertion fails.
|
|
5. Writes TAP evidence only on the controller under `reports/`.
|
|
|
|
`scripts/handoff_contract.py` statically rejects remote modules other than
|
|
`stat`, `assert`, and the exact Goss `command`. Controller writes must be
|
|
delegated to `localhost` and remain under `reports/`. It also pins every Goss
|
|
command assertion and both profile firewall probes to their reviewed read-only
|
|
set. `make verify` therefore does not install packages, rewrite files, reload
|
|
systemd, or change services on a managed host.
|
|
|
|
**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.
|
|
|
|
If verification fails because the installed surface is stale, review the
|
|
rendered changes and use the explicit mutating interface for only the intended
|
|
host:
|
|
|
|
```bash
|
|
make verify-refresh-host \
|
|
HOST=Railiance01 \
|
|
APPROVE_VERIFY_REFRESH=REFRESH-GOSS-Railiance01
|
|
```
|
|
|
|
Refreshing every host requires the distinct approval value
|
|
`APPROVE_VERIFY_REFRESH=REFRESH-GOSS-ALL`. Both refresh targets install or
|
|
update Goss, its baseline, wrapper, service, and timer before running checks.
|
|
|
|
## Goss test file
|
|
|
|
`goss/baseline.yaml.j2` is rendered per host from the same inventory lists
|
|
that converge UFW. 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. If it introduces a new control kind, teach both consumers that kind.
|
|
3. Run `make validate-baseline` and the unit tests.
|
|
4. Run the applicable reviewed convergence/refresh command, then use
|
|
`make verify-host HOST=…` as the read-only acceptance check.
|
|
|
|
An hourly on-host timer (`railiance-goss-baseline.timer`) reruns the last
|
|
rendered baseline. See `docs/conformance-loop.md`.
|
|
|
|
## Reports
|
|
|
|
TAP reports are transient under ignored `reports/`. They are machine-readable
|
|
and suitable for CI pipelines. Promote deliberately retained evidence under
|
|
`docs/evidence/` after reviewing it for safe metadata. A cleanup policy for old
|
|
reports is tracked as extension point EP `78ef4879`.
|