From fbb8100ecd2b98c73c58e9763dd456e0d35dc6a4 Mon Sep 17 00:00:00 2001 From: Audiobuzz Date: Sat, 11 Jul 2026 17:32:05 +0930 Subject: [PATCH] Adding secure NTP --- CLAUDE.md | 53 +++- diskless.pkr.hcl | 39 ++- docs/TODO.md | 48 ++- docs/irap-poam.md | 98 ++++++ docs/irap-sar.md | 290 ++++++++++++++++++ docs/secure-ntp-design.md | 127 ++++++++ docs/ssh-host-certificate-design.md | 140 +++++++++ docs/ssp-annex-a-ism-residual-risk.md | 15 +- docs/ssp.md | 4 +- docs/stig-deviations.md | 12 +- .../dispatcher.d/10-chrony-dhcp-ntp | 58 +++- serving/chrony/chrony.conf | 21 +- serving/chrony/nts-cert.sh | 52 ++++ serving/chrony/nts/.gitignore | 4 + serving/defaults.env | 5 + serving/docker-compose.yml | 34 +- 16 files changed, 929 insertions(+), 71 deletions(-) create mode 100644 docs/irap-poam.md create mode 100644 docs/irap-sar.md create mode 100644 docs/secure-ntp-design.md create mode 100644 docs/ssh-host-certificate-design.md create mode 100755 serving/chrony/nts-cert.sh create mode 100644 serving/chrony/nts/.gitignore diff --git a/CLAUDE.md b/CLAUDE.md index 44d6144..17398d2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,6 +36,14 @@ files/hostapi/ Go host-info agent (fleet-hostapi): re loopback :9099 /v1/host for the kiosk SPA (HOST-API.md contract). Built in 2b-hostapi, fapolicyd-trusted, golang removed in 2d. +files/ssh-hostcert/ Go SSH host-cert client (fleet-ssh-hostcert): + at boot signs this host's per-boot SSH host + key with the fleet SSH CA via the enrol-broker + host plane (TPM-identity mTLS), so admins trust + via one @cert-authority line instead of per-boot + TOFU (F-02/P-17). Built in 2b-ssh-hostcert, + fapolicyd-trusted; enable_ssh_hostcert flag. + docs/ssh-host-certificate-design.md. scripts/extract-artifacts.sh qcow2 -> kernel/initramfs/erofs+verity/cmdline scripts/build-oci.sh ukify + sbsign + roothash gate + OCI image scripts/build-ipxe-oci.sh sbsign snponly.efi -> FROM-scratch fleet-ipxe image @@ -156,7 +164,11 @@ instead of working around it. 1. **Signing keys never enter the build VM or this repo.** `sb_key` / `sb_cert` are CI/HSM-provided paths on the build host. Never commit key material, never copy keys into provisioner steps, never echo them in - logs. Unsigned builds are lab-only and must keep their loud warning. + logs. Unsigned builds are **local-lab-only** (a bare `packer build` without + keys) and must keep their loud warning; **CI fails closed** — the + `gitea-main-ci.yaml`/`ipxe-image.yaml` "Production build gate" aborts unless + `SB_KEY`/`SB_CERT` (and `FLEET_CA` for the UKI build) are set, so no unsigned + or placeholder-CA artifact is producible via CI (POA&M P-03/P-05). 1. **Reproducibility of the root image is a security property.** `mkfs.erofs` runs with fixed UUID and `-T 0`; the verity root hash must be a pure function of the input tree. Don’t introduce timestamps, @@ -214,8 +226,10 @@ instead of working around it. are evaluated, and `stig-deviations.sh` flags an INVALID SCAN + exits 2. Also confirm the `...content_profile_stig` profile id exists in the chosen datastream; (b) remediation must not break - boot/login (NM/NVMe, key-only sshd, kiosk, SELinux enforcing) — `enable_stig=false` - is the escape hatch; (c) **reproducibility**: build twice and confirm identical + boot/login (NM/NVMe, key-only sshd, kiosk, SELinux enforcing) — waive the + offending rule in the tailoring as a justified deviation (STIG is now + **enforced-true** by Packer variable validation — `enable_stig=false` is no + longer a build path; POA&M P-03); (c) **reproducibility**: build twice and confirm identical `root_hash` in verity.json — if it drifts, oscap wrote something nondeterministic (timestamp/uuid/key); find + normalize it in 2d hygiene; (d) read the generated `stig-deviations.md` — every REVIEW row is a real finding @@ -247,13 +261,23 @@ instead of working around it. **re-check reproducibility** (semanage rebuilds the policy store → possible verity-hash drift). The triage waivers/decisions live in the tailoring + docs/stig-deviations.md #12; the generated report should show 0–1 REVIEW. -- NTP via DHCP option 42 → chrony (no public-pool egress): confirm on a booted - host `chronyc sources` lists the DHCP-handed lab server (not `*.pool.ntp.org`). - Assumes (a) NM exports the lease's NTP servers to the dispatcher as - `$DHCP4_NTP_SERVERS`, and (b) RHEL 9 `/etc/chrony.conf` honours - `sourcedir /run/chrony-dhcp` (the provisioner appends it if absent). If sources - are empty, the dispatcher didn't fire or the env var name differs — check - `journalctl -u NetworkManager` and `ls /run/chrony-dhcp/`. +- **NTP is `secure_ntp=true` by default (NTS, RFC 8915) — CA-authenticated time** + (closes F-03/P-18; docs/secure-ntp-design.md). **No FQDN is baked:** the NM + dispatcher (`10-chrony-dhcp-ntp`, mode-aware) derives the server name at runtime + as `.` (default `ntp`.`$DHCP4_DOMAIN_NAME`, + e.g. `ntp.fleet.lab`), writes `server … iburst nts` into `sourcedir + /run/chrony-dhcp`, and chrony verifies the NTS-KE TLS cert against the **fleet + CA** (`/etc/dot1x/radius-ca.pem` — reused; baked as `ntstrustedcerts`). Only the + `ntp` label + the CA are baked, so the **site domain can change without a + rebuild** (new DHCP domain + reissued NTS cert). A DHCP/NTP/DNS spoofer can only + DoS (no fleet-CA-signed cert for the name it redirects to ⇒ NTS fails ⇒ no sync), + never shift the clock. Serving needs: DHCP **option 15** (domain-name) handed to + hosts, a **fleet-signed** NTS cert with DNS SAN `ntp.` + (`serving/chrony/nts-cert.sh`), and **tcp/4460** (NTS-KE) open + udp/123. Confirm + on a booted host: `chronyc -N authdata` shows the source in `NTS` mode, cookies + > 0. `nocerttimecheck 1` lets a cold boot clock (no RTC) bootstrap the first sync. + **Toggle:** `secure_ntp=false` reverts the dispatcher to the legacy + unauthenticated **DHCP option 42** path (plain `server iburst`). - `fleet` Plymouth theme: confirm (a) the script plugin package actually installed (`/usr/lib64/plymouth/script.so` — else the build fell back to `spinner`, logged as a WARNING), (b) the scripted theme + a font got bundled @@ -310,7 +334,7 @@ These bit us bringing the build up on Rocky 9.8; they are easy to regress. stack stays in the image, dormant. The matching lab DHCP/HTTP/NVMe-oF serving stack lives in `serving/` (docker compose). Re-enable security by dropping the var / setting `DISABLE_DOT1X: "false"` in the CI workflow. -- **STIG toggle**: `enable_stig=true` (default) applies DISA STIG via +- **STIG toggle** (now **enforced-true**, not just default): `enable_stig=true` applies DISA STIG via OpenSCAP/SSG in a provisioner, tailored for the diskless/UKI/kiosk architecture (`files/stig/diskless-tailoring.xml` unselects the N/A or dangerous-to-remediate rules; every waiver is justified in @@ -320,8 +344,11 @@ These bit us bringing the build up on Rocky 9.8; they are easy to regress. **report-only**: it publishes `stig-report.html` + `stig-arf.xml` + `stig-deviations.md` (generated by `scripts/stig-deviations.sh`) as artifacts and never fails on score. The scanner tooling is removed from the shipped image - (2d hygiene). `enable_stig=false` is the escape hatch if hardening regresses - the boot/secure-path bench. NEVER let a waiver hide a real finding — anything + (2d hygiene). **`enable_stig` is enforced `true` by Packer variable validation + (POA&M P-03) — a build with it false fails `packer validate`/`build`; there is + no unhardened build path.** If hardening regresses the boot/secure-path bench, + waive the offending rule in the tailoring as a justified deviation (never by + disabling STIG). NEVER let a waiver hide a real finding — anything non-pass that isn't in the register shows as **REVIEW** in the report. - **Boot splash toggle**: `boot_splash=true` (default) bakes `rhgb quiet` for the Plymouth splash; `false` bakes `plymouth.enable=0` for a verbose console boot — diff --git a/diskless.pkr.hcl b/diskless.pkr.hcl index 9dca771..4b6cea7 100644 --- a/diskless.pkr.hcl +++ b/diskless.pkr.hcl @@ -164,6 +164,18 @@ variable "audit_host" { default = "audit.fleet.lab" } +variable "secure_ntp" { + type = bool + description = "true (default): SECURE, CA-AUTHENTICATED time via NTS (RFC 8915). The NM dispatcher derives the NTP server name at runtime as . (NO FQDN baked — the domain can change without a rebuild) and chrony verifies the server's NTS-KE TLS cert against the fleet CA (/etc/dot1x/radius-ca.pem — same anchor as RADIUS/agent). A DHCP/NTP/DNS spoofer can then only DoS (cause no-sync), never shift the clock to resurrect expired/revoked certs (closes F-03/P-18; docs/secure-ntp-design.md). The serving NTP server needs a fleet-signed NTS cert with SAN . (serving/chrony/nts-cert.sh). false: legacy UNAUTHENTICATED DHCP option-42 NTP (the dispatcher writes the handed IPs) — lab/interop only." + default = true +} + +variable "ntp_prefix" { + type = string + description = "secure_ntp=true only: the label prepended to the DHCP-provided domain to form the NTS NTP server name (., e.g. ntp.fleet.lab). Deliberately NOT a full FQDN — only this label + the fleet CA are baked, so the environment's domain can change (new DHCP domain + reissued NTS cert) without rebuilding the image. Must match the serving NTS cert SAN and resolve via dnsmasq." + default = "ntp" +} + variable "sb_key" { type = string description = "Secure Boot signing key (PEM) for the UKI. Empty = unsigned lab build. Production: CI/HSM-held; never enters the build VM." @@ -413,15 +425,32 @@ build { # secure mode, reserving the physical NIC for wpa_supplicant. "chmod 0755 /usr/lib/dracut/modules.d/95dot1x-macsec/*.sh", - # ---- NTP from DHCP (option 42) -> chrony, no public-pool egress -------- - # NM dispatcher writes DHCP-provided NTP servers into chrony's sourcedir - # (replaces the removed dhclient.d/chrony.sh bridge). Strip the baked public - # pool so the host syncs ONLY from the lab NTP server handed via DHCP — the - # workstation never reaches *.pool.ntp.org. + # ---- NTP: CA-authenticated NTS (default) or legacy DHCP option 42 ------ + # BOTH modes run through the SAME machinery: the NM dispatcher writes a + # source file into chrony's `sourcedir /run/chrony-dhcp`; the baked public + # pool is stripped so the host never egresses to *.pool.ntp.org. + # secure_ntp=true (NTS): the dispatcher derives the server NAME at runtime + # as . and writes `server NAME iburst + # nts`. chrony verifies the NTS-KE TLS cert against the FLEET CA + # (ntstrustedcerts below). NOTHING host- or domain-specific is baked + # (only the `ntp` label + the CA) — the domain can change without a + # rebuild. A DHCP/NTP/DNS spoofer can only DoS; it can't present a + # fleet-CA-signed cert for the name it redirects to, so it can't shift + # the clock (F-03/P-18). No fallback to unauthenticated time. + # nocerttimecheck 1: a diskless host may boot with a bad/epoch clock (no + # reliable RTC), failing the cert validity window (chicken-and-egg); + # allow the FIRST measurement without the cert-time check to bootstrap, + # then enforce. Isolated LAN + one-measurement window bound the trade-off. + # secure_ntp=false (legacy): the dispatcher writes the DHCP option-42 IPs + # as plain `server iburst` — UNAUTHENTICATED, lab/interop only. + # The dispatcher is mode-aware via baked @NTP_MODE@/@NTP_PREFIX@ tokens. "install -D -m0755 /tmp/etc-stage/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp /etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp", + "if [ \"${var.secure_ntp}\" = \"true\" ]; then NTP_MODE=nts; else NTP_MODE=dhcp; fi", + "sed -i \"s/@NTP_MODE@/$NTP_MODE/; s/@NTP_PREFIX@/${var.ntp_prefix}/\" /etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp", "restorecon /etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp", "sed -i -E 's/^[[:space:]]*(pool|server)[[:space:]]/#&/' /etc/chrony.conf", "grep -q '^sourcedir /run/chrony-dhcp' /etc/chrony.conf || echo 'sourcedir /run/chrony-dhcp' >> /etc/chrony.conf", + "if [ \"${var.secure_ntp}\" = \"true\" ]; then echo '==> secure NTP: NTS via <${var.ntp_prefix}>. (fleet-CA-authenticated)'; { printf '\\n# Secure NTP (NTS, RFC 8915) — verify the server against the fleet CA. docs/secure-ntp-design.md\\n'; printf 'ntstrustedcerts /etc/dot1x/radius-ca.pem\\n'; printf 'nocerttimecheck 1\\n'; } >> /etc/chrony.conf; else echo '==> legacy NTP: unauthenticated DHCP option-42'; fi", # Enable chrony's tracking log so the fleet-agent can report clock health # (source/stratum/offset) WITHOUT touching chronyd's command interface. That # interface is unusable by the hardened agent: the unix socket is diff --git a/docs/TODO.md b/docs/TODO.md index aab7d0e..bec6175 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -14,16 +14,32 @@ flipped back for a secure/production build (consolidated as item #8): - `nvmet_traddr` / CI `NVMET_TRADDR="192.168.50.10"` → hardcoded IP (should be a per-env DNS name like `nvme.fleet.lab` once DNS-at-boot is trusted). - Unsigned UKI (`sb_key`/`sb_cert` not passed) → Secure Boot must be OFF on the - test target. Production needs CI/HSM signing. -- `enable_kiosk=true`; `boot_splash=true`; NTP via DHCP→serving-host chrony. -- **`enable_stig` / CI `ENABLE_STIG="false"` RIGHT NOW** — temporarily disabled - to get a working box while the STIG regressions were diagnosed (now fixed, - below). **Flip back to `true`** and bench-verify the hardened build. -- **`debug_shell=true` (DEBUG HOLE — passwordless root on tty2). REMOVE before - prod** (`-var debug_shell=false`). Note: it didn't actually come up on tty2 in - testing; diagnosis ended up done via offline `rootfs.erofs` mount + SMBIOS - `enforcing=0`. SMBIOS cmdline-extra works on Proxmox only after a full VM - **stop/start** (not `reset`). + test target. Production needs CI/HSM signing. **NOTE (2026-07-11, `poam-hardening`): + the main CI now fails closed** — a "Production build gate" aborts unless + `SB_KEY`/`SB_CERT`/`FLEET_CA` are set, so an unsigned/placeholder-CA build is no + longer producible via CI (only a local `packer build` without keys still yields + the lab warning). The `fleet-ipxe` build is gated the same way. +- `enable_kiosk=true`; `boot_splash=true`. +- **`secure_ntp` — DONE (2026-07-11, `secure-ntp`).** NTP is now **NTS + (CA-authenticated)** by default: the host verifies the serving NTP server's + fleet-signed NTS-KE cert against `radius-ca.pem` (closes F-03/P-18; + docs/secure-ntp-design.md). **No baked FQDN** — the NM dispatcher derives the + server name from the DHCP domain at runtime (`ntp.`), so a site-domain + change needs no rebuild. `secure_ntp=false` reverts to the legacy unauthenticated + DHCP option-42 path. Serving needs `serving/chrony/nts-cert.sh` (DNS SAN + `ntp.`), DHCP option-15 domain handout, + tcp/4460 open. **BENCH-VERIFY** + `chronyc -N authdata` (NTS cookies) + `$DHCP4_DOMAIN_NAME` populated. +- **`enable_stig` — DONE (2026-07-11, `poam-hardening`).** CI `ENABLE_STIG="true"` + and, more importantly, the Packer template now **enforces** `enable_stig=true` + via variable validation — a build with it false fails `packer validate`/`build`. + There is no unhardened build path anymore. (Was temporarily off during the STIG + regression diagnosis; that escape hatch is gone.) +- **`debug_shell` — REMOVED (2026-07-11, `poam-hardening`).** The DEBUG HOLE + (passwordless root shell) and its `debug_shell` variable are deleted from the + template entirely; no build can bake it (closes POA&M P-02 / R-02). Offline + diagnosis via `rootfs.erofs` mount + SMBIOS `enforcing=0` remains the supported + path (SMBIOS cmdline-extra works on Proxmox only after a full VM **stop/start**, + not `reset`). - **Admin SSH key MUST be ECDSA or RSA, NOT ed25519**, when STIG is on — the STIG crypto policy drops ed25519 from sshd's accepted pubkey algorithms. Set the `SSH_AUTHORIZED_KEY` CI secret to an ECDSA/RSA key (see #2). @@ -154,7 +170,8 @@ was incomplete:** gtls+x509+CA and broke the plaintext `2f` forward → unselected in the tailoring; production TLS offload tracked under #8. - Diagnosis tooling notes: SMBIOS cmdline-extra works only after a Proxmox VM - **stop/start** (not `reset`); the debug-shell-on-tty2 never came up — the + **stop/start** (not `reset`); the debug-shell-on-tty2 never came up (and is now + removed from the template — see the build-posture section) — the decisive diagnosis was **offline `rootfs.erofs` mount** + `ssh -vvv` + `enforcing=0`. @@ -190,8 +207,9 @@ pass / N/A. `TYPE=sysadm_t ROLE=sysadm_r NOPASSWD:ALL` and maps the admin to **`staff_u`** (`semanage login`) so the sudo role transition is authorized. ⚠ **TWO BENCH GATES:** (a) admin SSH login + sudo + privileged ops must work under `staff_u` - confinement (escape hatch: `enable_stig=false` → reverts to a justified - deviation); (b) **reproducibility** — `semanage` rebuilds the policy store; + confinement (if it locks out, waive the offending rule in the tailoring as a + justified deviation — `enable_stig=false` is no longer available, STIG is + enforced-true); (b) **reproducibility** — `semanage` rebuilds the policy store; confirm the verity hash is stable across two builds (normalize in 2d if it drifts). Needs `policycoreutils-python-utils` (installed in 2c-STIG if absent). @@ -408,6 +426,10 @@ Path to getting here (don't re-discover): stripped from `/etc/chrony.conf`. Result: the workstation syncs time only from the lab server — no external NTP egress. BENCH-VERIFY: on a booted host `chronyc sources` shows the lab server (the DHCP one), not a public pool. + **SUPERSEDED (2026-07-11, `secure-ntp`) by `secure_ntp=true`:** the DHCP + option-42 path is now the *unauthenticated fallback*; the default is + NTS/CA-authenticated time (the DHCP source is no longer mixed in). See the + build-posture note above + docs/secure-ntp-design.md. - **Image size:** Chromium+Mesa add hundreds of MB to the *uncompressed* erofs — feeds the "shrink the rootfs" concern (and #7); reconsider whether a lighter browser/render path is viable. diff --git a/docs/irap-poam.md b/docs/irap-poam.md new file mode 100644 index 0000000..fa537f4 --- /dev/null +++ b/docs/irap-poam.md @@ -0,0 +1,98 @@ +# Plan of Action & Milestones (POA&M) — Diskless Fleet Boot System + +> **Protective marking:** `STARFLEET RESTRICTED — CLEARANCE LEVEL 2 — BRIDGE CREW / SECURITY DIVISION` +> *(marking scheme: `STARFLEET ROUTINE` ≈ OFFICIAL · `STARFLEET RESTRICTED` ≈ +> OFFICIAL: Sensitive · `STARFLEET SECRET` ≈ PROTECTED · `OMEGA DIRECTIVE` ≈ +> SECRET and above — see the [SAR](irap-sar.md) banner.)* +> +> **Source:** IRAP security assessment 2026-07-10 — see the +> [Security Assessment Report](irap-sar.md). Weaknesses derive from the SSP +> [Annex A residual-risk register](ssp-annex-a-ism-residual-risk.md) (R-NN) and +> the assessor's additional findings (F-NN, SAR §5.2). The repo's +> [TODO.md](TODO.md) remains the engineering-level working backlog; this POA&M +> is the authorisation-facing view and the two must be kept in sync. + +| Field | Value | +|---|---| +| POA&M version | 1.2 (adds P-24 / SAR F-08 — DNS/DHCP residual flows: kiosk HTTP, UKI rollback) | +| Date raised | 2026-07-10 | +| Last updated | 2026-07-11 | +| Review cadence | Monthly by System Manager; quarterly to the AO | +| Overall status | 0 of 24 items closed; P-02 engineering-complete (awaiting assessor M2) | + +> **2026-07-11 remediation (`poam-hardening` branch).** The build pipeline is now +> a **fail-closed production profile** (POA&M P-03). Changes: (i) the passwordless +> debug shell was **deleted entirely** from the Packer template — no `debug_shell` +> variable, no `debug-shell.service` provisioner (P-02, R-02); (ii) `enable_stig` +> is enforced `true` by Packer **variable validation** — a build with it false +> fails `packer validate`/`build` (P-04); (iii) the CI "Production build gate" +> aborts the run unless `SB_KEY`, `SB_CERT` and `FLEET_CA` are set — no unsigned +> fallback, no placeholder RADIUS CA (P-03, P-05, P-06 CA wiring); the signed +> `fleet-ipxe` bootloader build is gated the same way. Not yet done in P-03 M1: +> `disable_dot1x=false` (blocked on P-01 secure-path bench) and distinct +> production-artifact tagging (M2). See the row-level status below. + +**Severity** = assessor's pre-treatment rating. **Milestone dates** are the +assessor's recommended targets, to be confirmed by the System Owner on +appointment. Items marked **[GATE]** are conditions precedent for a production +ATO (SAR §7.2) — they cannot be risk-accepted. + +--- + +## 1. Production-ATO gating items + +| # | Weakness | Source | Severity | Remediation actions & milestones | Owner | Target | Status | +|---|---|---|---|---|---|---|---| +| P-01 **[GATE]** | Secure path (802.1X EAP-TLS + MACsec) implemented but never verified end-to-end; disabled in current builds (`disable_dot1x=true`). | R-01 | **High** | M1: complete `serving/dot1x-bench/` harness config (swtpm + hostapd + FreeRADIUS) — 2026-08-07. M2: bench pass: port `Authorized`, macsec0 addressed + DHCP, MKA survives switch-root, NVMe root over macsec0, lease renews — 2026-08-21. M3: re-enable in CI (`DISABLE_DOT1X=false`) — 2026-08-28. M4: **assessor witness** at delta assessment. | Platform team | 2026-08-28 | Open | +| P-02 **[GATE]** | Passwordless root debug shell (`debug_shell=true`) present in lab builds — undocumented privileged access path. | R-02 | **High** | M1: ~~`debug_shell=false` default + CI assertion~~ **DONE (exceeded) 2026-07-11** — the `debug_shell` variable and the `debug-shell.service` provisioner were **removed entirely** from the template; no build can bake it. M2: assessor verifies absence on the assessed digest. | Platform team | 2026-07-31 | **M1 done — awaiting assessor (M2)** | +| P-03 **[GATE]** | No enforced production build profile — security toggles (dot1x, STIG, signing, debug shell, RADIUS CA) individually settable; drift detectable only by human review. | F-01 | **High** | M1: define a CI "production" profile hard-asserting `disable_dot1x=false`, `enable_stig=true`, `debug_shell=false`, `SB_KEY`/`SB_CERT` set, non-placeholder RADIUS CA — 2026-08-14. **Partly DONE 2026-07-11:** CI "Production build gate" hard-asserts `SB_KEY`/`SB_CERT`/`FLEET_CA` (fail-closed); `enable_stig=true` enforced by template validation; `debug_shell` removed. **Still open:** `disable_dot1x=false` (blocked on P-01) + M2 distinct production-artifact tagging. | Platform team + pipeline owner | 2026-08-21 | **M1 partial** | +| P-04 **[GATE]** | STIG hardening verified but `enable_stig` was toggled off during bring-up; production CI posture unconfirmed. | R-03 | Medium | M1: ~~`ENABLE_STIG=true` in CI~~ **DONE 2026-07-11** — `enable_stig` is enforced `true` by Packer variable validation (folds into P-03); `enable_stig=false` fails the build. M2: regenerate deviations report on the assessed build; confirm 0 unjustified REVIEW rows — 2026-08-21. | Platform team | 2026-08-21 | **M1 done** | +| P-05 **[GATE]** | Boot-chain signing + Secure Boot not the standing default (unsigned/SB-off builds possible; `db` not enrolled fleet-wide). | R-13 | Low (High impact if regressed) | M1: ~~CI signing secrets permanently set~~ **DONE 2026-07-11** — the CI Production build gate fails closed without `SB_KEY`/`SB_CERT`; no unsigned-UKI fallback remains (the signed `fleet-ipxe` build is gated too). M2: `db` enrolled on all production hosts via `fleet-sb-enroll`; enrolment recorded per host — 2026-09-11. M3: assessor spot-checks `mokutil --sb-state` + `sbverify` — delta assessment. | Platform team | 2026-09-11 | **M1 done** | +| P-06 **[GATE]** | Placeholder RADIUS CA — real EAP-TLS trust anchor not provisioned. | R-08 | Medium | M1: provision environment RADIUS CA out-of-band (classification permitting, not committed) — 2026-08-07 (pre-req for P-01 M2). | Environment owner | 2026-08-07 | Open | +| P-07 **[GATE]** | Governance layer absent: AO/System Owner/CISO unappointed; data classification unconfirmed; documentation unapproved. | SAR §1.4, §2.4.2 | **High** (blocks ATO structurally) | M1: appoint AO, System Owner, CISO — 2026-08-14. M2: System Owner confirms data classification; re-baseline SSP if above STARFLEET RESTRICTED — 2026-08-28. M3: AO approves documentation set — with ATO decision. | Organisation / System Owner | 2026-08-28 | Open | +| P-08 **[GATE]** | No Incident Response Plan, continuous-monitoring plan, or log-retention policy; volatile RAM overlay complicates forensics (no break-glass procedure). | R-12 | Medium | M1: author IRP incl. stateless-host forensics (capture-before-reboot guidance, collector/SIEM as evidence store) — 2026-09-04. M2: continuous-monitoring plan + log-retention policy — 2026-09-04. M3: tabletop exercise of the IRP — 2026-09-25. | SecOps | 2026-09-25 | Open | +| P-09 **[GATE]** | Audit-log offload plaintext on both hops (host→collector, collector→Wazuh); retention/archival unconfigured; Wazuh default credentials; dashboard/API unrestricted. | R-07 | Low–Medium | M1: rsyslog TLS (gtls/6514, fleet CA) host→collector; re-select the 3 waived `rsyslog_encrypt_offload_*` STIG rules — 2026-09-04 (also closes the DNS-spoofable audit-stream flow — F-08/P-24). M2: TLS collector→Wazuh — 2026-09-04. M3: rotate Wazuh credentials, restrict dashboard/API, set allowed-ips — 2026-08-21. M4: configure retention/archival per policy from P-08 — 2026-09-18. | Platform / SecOps | 2026-09-18 | Open | +| P-10 **[GATE]** | ISM control identifiers unreconciled against the May 2026 ISM release (representative numbering throughout Annex A). | SAR §2.4.1 (P-17 in SAR text) | Medium (assurance) | M1: reconcile all `ISM-NNNN*`; map any new May-2026 controls; disposition retired ones — 2026-08-21 (assessor task, System Owner to facilitate). | IRAP assessor + System Owner | 2026-08-21 | Open | +| P-11 **[GATE]** | Management/enrolment plane (fleet-manager, broker, step-ca fleet CA, InfluxDB) is an unauthorised connected system the host now trusts; fleet CA key protection unassessed. | R-14 | Medium | M1: author management-plane SSP (CA key protection, broker hardening incl. F-04 abuse controls, InfluxDB token/data, admin-plane exposure) — 2026-09-11. M2: separate assessment/authorisation or AO interim risk position covering the fleet CA — 2026-10-02. | Platform / SecOps | 2026-10-02 | Open | + +## 2. Risk-acceptance candidates (production at STARFLEET ROUTINE, isolated LAN) — treat on roadmap + +| # | Weakness | Source | Severity | Remediation actions & milestones | Owner | Target | Status | +|---|---|---|---|---|---|---|---| +| P-12 | NVMe/TCP root traffic plaintext on the LAN. | R-04 | Medium | M1: document transport decision (TLS/IPsec vs MACsec end-to-end across fabric) — 2026-09-25. M2: implement chosen transport protection — 2026-11-27. Interim: isolated LAN + dm-verity integrity accepted. | Platform / network | 2026-11-27 | Open | +| P-13 | Host admin access: NOPASSWD, fleet-generic, non-rotatable key; no human MFA on host SSH. | R-05 | Medium | M1: design per-host/rotatable access (SSH CA `TrustedUserCAKeys` or config agent) — 2026-10-16. M2: implement + bench under `staff_u` confinement — 2026-11-27. | Platform team | 2026-11-27 | Open | +| P-14 | AACA/AACP conformance not asserted; FIPS off; crypto-module evaluation unconfirmed. **Escalates to blocking if classification > STARFLEET RESTRICTED (P-07 M2).** | R-06 | Medium | M1: AACA/AACP conformance statement for EAP-TLS, MACsec-GCM, mTLS, SSH suites — 2026-09-25. M2: FIPS/evaluated-module decision alongside signing posture — 2026-10-30. | Security + platform | 2026-10-30 | Open | +| P-15 | Host firewall (firewalld) disabled on the workstation. | R-09 | Low–Medium | M1 (after P-01): re-enable with explicit allows (NVMe/TCP 4420, DHCP, log offload 6514, SSH; reciprocal egress rules) + bench NVMe/NM stability — 2026-09-25. | Platform / network | 2026-09-25 | Open | +| P-16 | Patch cadence / critical-CVE SLA undefined for the rebuild-redeploy model. | R-10 | Medium | M1: define cadence + CVE SLA — 2026-08-28. M2: automate rebuild triggers on upstream advisories — 2026-10-30. | Platform team | 2026-10-30 | Open | +| P-17 | SSH host keys regenerate every boot — admin connections are perpetual TOFU; LAN MITM undetectable client-side. | **F-02 (new — add to Annex A register)** | Medium | M1: add to risk register — 2026-07-24. M2: ~~stable TPM-derived host keys or SSH host CA~~ **IMPLEMENTED 2026-07-11 (`poam-hardening`)** — the **SSH host CA** path (Design A, broker-mediated): `fleet-ssh-hostcert` (files/ssh-hostcert) gets each host's per-boot key signed by the fleet SSH CA **via the enrol-broker host plane** (TPM-identity mTLS) at boot; admins trust via one `@cert-authority` line. Keeps step-ca behind the single front door. **BENCH-VERIFY** the broker↔step-ca SSH-CA contract + principal policy + SELinux (docs/ssh-host-certificate-design.md). M3: bench-verify end-to-end on a booted+enrolled host; roll the `@cert-authority` anchor to admins — 2026-10-16. | Platform team | 2026-10-16 | **M2 implemented — bench-verify (M3)** | +| P-18 | Time source unauthenticated (DHCP-handed NTP, no NTS) — host clock is an input to all certificate validation. | **F-03 (new — add to register)** | Low–Medium | M1: add to register — 2026-07-24. M2: ~~NTS or keyed NTP~~ **IMPLEMENTED 2026-07-11 (`secure-ntp`)** — **NTS (RFC 8915)**: the serving NTP server's NTS-KE TLS cert is fleet-CA-signed; the host (`secure_ntp=true`) verifies it against `radius-ca.pem` and syncs authenticated time. The server name is **derived at runtime from the DHCP domain** (`ntp.`) — no baked FQDN, so a site-domain change needs no rebuild. A DHCP/DNS spoofer can only DoS, not shift the clock. **BENCH-VERIFY** `chronyc authdata` NTS cookies + DHCP option-15 domain handout + tcp/4460 + STIG chrony (docs/secure-ntp-design.md). M3: bench-verify end-to-end; issue the NTS server cert per environment — 2026-10-30. | Platform team | 2026-10-30 | **M2 implemented — bench-verify (M3)** | +| P-19 | Fleet-wide availability SPOF: single unreplicated NVMe/TCP root target. | **F-05 (new — add to register)** | Medium (availability) | M1: interim `ctrl_loss_tmo`/`reconnect_delay` tuning at connect time — 2026-08-28. M2: multipath/ANA replica design (byte-identical same-UUID namespaces, multi-path discovery) — 2026-11-27. M3: document fleet RTO in IRP (P-08). | Platform team | 2026-11-27 | Open | +| P-20 | No remote attestation of TPM boot measurements (compromised boot not centrally detectable). | R-11 | Low–Medium | M1: design TPM quote → verifier flow (candidate: extend fleet-agent/manager, gated on P-11 authorisation) — 2027 roadmap. Interim compensation: SB + signed UKI + dm-verity + F-07 posture telemetry. | Platform team | 2027-Q1 | Open | +| P-21 | Runtime posture drift not fully monitored (SB state, fapolicyd enforcement, cmdline hash not in telemetry). | **F-07 (new)** | Low | M1: extend fleet-agent facts + manager drift alerts — 2026-09-25 (pairs with P-05 M2). | Platform team | 2026-09-25 | Open | +| P-22 | Cross-protocol TPM key reuse ("one identity, three stages") — domain-separation analysis and revocation blast-radius plan undocumented. | **F-06 (observation)** | Low | M1: add a short analysis + revocation procedure to the SSP — 2026-08-28. | Platform + security | 2026-08-28 | Open | +| P-23 | Agent reconcile actuators deferred; enabling them creates a remote host-mutation channel outside this assessment's scope. | R-15 | Low (v1) | Standing condition, not a dated milestone: **delta assessment of the command/authZ path required before any actuator is enabled** (SAR §7.3 revocation trigger). Keep behind an explicit build flag. | Platform team | At actuator build | Open (condition) | +| P-24 | DNS/DHCP untrusted-input pattern undocumented, and two flows escape it: **kiosk content over plain HTTP** (DNS/DHCP spoof → attacker page fullscreen on every workstation; Chromium PNA allowance to the loopback host-info agent reachable from it) and **UKI rollback** (a spoofer can serve an older, still-validly-signed UKI — Secure Boot does not prevent this). The plaintext audit offload was the third such flow; it is closed by P-09 M1 (cross-ref). DNSSEC/DoT is explicitly **not** the treatment — authenticate above resolution, per the system's own pattern. | **F-08 (new — add to register)** | Medium | M1: add F-08 to the Annex A register + state the "DNS/DHCP are untrusted; every security-relevant connection authenticates its peer above name resolution" principle in the SSP — 2026-08-14. M2: serve the kiosk over **TLS with a fleet-CA-signed cert** (serving nginx/kiosk proxy; pin the fleet CA via Chromium enterprise policy; kiosk URL → `https://`) — 2026-09-25. M3: record the UKI-rollback residual against R-13 and document the anti-rollback design decision (dbx revocation / TPM monotonic counter, design v2 §6) — 2026-11-27. | Platform team | 2026-11-27 | Open | + +## 3. Milestone timeline (summary) + +``` +2026-07 P-02 debug shell off ── P-17/P-18 register updates +2026-08 P-06 RADIUS CA ── P-01 secure-path bench ── P-03/P-04/P-05 prod profile+signing + P-07 roles+classification ── P-10 ISM reconciliation ── P-16 patch SLA ── P-22 + P-24 register+SSP DNS principle +2026-09 P-08 IRP/monitoring ── P-09 log TLS+retention ── P-11 mgmt-plane SSP + P-15 firewall ── P-21 posture telemetry ── P-12/P-14 decisions ── P-24 kiosk TLS +2026-10 P-11 mgmt-plane authorisation ── P-13/P-17 access model ── P-14/P-18 crypto/NTS + ── DELTA ASSESSMENT + production ATO decision window +2026-11 P-12 storage transport ── P-13 rotatable access ── P-19 multipath + P-24 anti-rollback decision +2027-Q1 P-20 remote attestation +``` + +**Earliest realistic production-ATO decision: mid-October 2026**, contingent on +the delta assessment witnessing P-01 through P-11 closed. + +--- + +*End of POA&M. Maintain alongside [TODO.md](TODO.md); update item status at +each monthly review and re-issue to the AO quarterly or on material change.* diff --git a/docs/irap-sar.md b/docs/irap-sar.md new file mode 100644 index 0000000..b828db7 --- /dev/null +++ b/docs/irap-sar.md @@ -0,0 +1,290 @@ +# Security Assessment Report (SAR) — Diskless Fleet Boot System + +> **Protective marking:** `STARFLEET RESTRICTED — CLEARANCE LEVEL 2 — BRIDGE CREW / SECURITY DIVISION` +> *(Starfleet marking scheme used throughout this document set: +> `STARFLEET ROUTINE` ≈ OFFICIAL · `STARFLEET RESTRICTED` ≈ OFFICIAL: Sensitive · +> `STARFLEET SECRET` ≈ PROTECTED · `OMEGA DIRECTIVE` ≈ SECRET and above. +> Confirm against the system's final data classification.)* +> +> **Assessment framework:** Australian Government PSPF + ASD *Information +> Security Manual* (ISM), May 2026 release (engagement target). This report is +> the IRAP assessor's Security Assessment Report produced under stage 4 +> (**Assess**) of the ISM assessment and authorisation lifecycle, for use by the +> Authorising Officer at stage 5 (**Authorise**). + +| Field | Value | +|---|---| +| System assessed | Diskless Fleet Boot System ("the fleet") | +| SSP version assessed | v0.3 (2026-07-10) + Annex A v0.3 | +| Assessment type | Initial IRAP security assessment (design + implementation review; limited effectiveness witnessing) | +| Assessment period | 2026-07-10 | +| Assessor | IRAP Assessor (engaged) | +| Report version | 1.0 | +| Companion outputs | [Plan of Action & Milestones](irap-poam.md) (this assessment); SSP [Annex A](ssp-annex-a-ism-residual-risk.md) risk register | + +--- + +## 1. Executive summary and determination + +The Diskless Fleet Boot System is an unusually well-architected endpoint +platform whose central security proposition — **stateless, diskless hosts that +return to a cryptographically verified known-good state on every boot, with all +host identity anchored in hardware (TPM) and no per-host secrets in any +artifact** — is sound, clearly documented, and in several areas demonstrably +implemented and bench-verified. The engineering culture evidenced by the +documentation set is a material strength: findings are recorded honestly +(including a false-clean compliance scan and an ineffective application-control +configuration, both self-identified and fixed), deviations are registered and +justified, and verification claims are dated and traceable. + +**Determination: the system is NOT yet recommended for a production Authority +to Operate.** The assessor recommends the AO consider a **time-limited interim +authorisation for continued lab/pre-production operation only**, with a +production ATO contingent on closure of the conditions in §7. + +The determination rests on four grounds: + +1. **The assessed configuration is not the target configuration.** The system's + headline preventive controls — 802.1X EAP-TLS network authentication and + MACsec link protection — are *disabled* in the current build + (`disable_dot1x=true`), have never been bench-verified end-to-end (R-01), and + the EAP-TLS trust anchor is a placeholder (R-08). The security architecture's + value is contingent on these controls; until they are demonstrated + operating, the assessment of them is a design review only. +2. **A known privileged back-door existed in lab builds — since removed.** + `debug_shell=true` baked a passwordless root shell (R-02). *Remediated + 2026-07-11 (`poam-hardening`, POA&M P-02): the `debug_shell` variable and its + `debug-shell.service` provisioner were deleted from the template — no build can + bake it.* Assessor to verify absence on the assessed digest (P-02 M2). +3. **Boot-chain signing is proven but not the standing posture.** Secure Boot + enforcement of the signed UKI (and signed iPXE chain) has been verified on + real hardware — a significant achievement — but unsigned/SB-off builds remain + possible and `db` enrolment is not fleet-wide (R-13). +4. **The governance layer required for authorisation does not yet exist.** No + Authorising Officer, System Owner, or CISO is appointed; the data + classification is unconfirmed; there is no Incident Response Plan, + continuous-monitoring plan, or approved security documentation set. An ATO + cannot be granted without an AO, and the applicable ISM control baseline + cannot be finalised without a confirmed classification. + +None of these grounds indicates architectural weakness; all are completion and +assurance gaps, most already self-identified in the SSP's own risk register. +The assessor **broadly concurs with the SSP Annex A risk register** (ratings and +treatments), with the adjustments and additional findings at §5. + +## 2. Scope and methodology + +### 2.1 What was assessed + +- The **authorisation boundary** as defined in SSP §2.3: the running + workstation host — signed UKI, dracut initramfs (802.1X/MACsec module), + dm-verity/erofs root, tmpfs overlay, TPM-backed identity, STIG-hardened OS + configuration, kiosk, and the in-image `fleet-agent` telemetry client. +- Boundary crossings (a)–(g) per SSP §2.3, including the new v0.3 + management-plane interface (outbound-only mTLS to the fleet-manager). + +### 2.2 What was explicitly out of scope (connected systems) + +The lab serving stack (DHCP/HTTP/NVMe-oF target, audit collector), the +server-side fleet-management plane (fleet-manager, admin UI, InfluxDB, +Traefik), the TPM enrolment broker and step-ca fleet CA, the RADIUS/802.1X +authenticator infrastructure, the Wazuh SIEM node, the network fabric, and +facility physical security. **These each require their own authorisation**; the +host's dependence on them is assessed here only at the interface. See finding +F-01 and risk R-14 — the AO should note that the host system's authorisation +is materially dependent on connected systems that are currently unauthorised. + +### 2.3 Method and evidence base + +- **Document review:** SSP v0.3, Annex A v0.3, architecture design v2 + errata, + fleet-management design, TPM enrolment design, STIG deviation register, + secure-boot key procedure, backlog/POA&M source (TODO.md). +- **Implementation-of-record review:** the repository is the implementation + (Packer template, dracut module, provisioners, CI workflows). Control + implementation statements were checked against the code and build gates + (e.g. the roothash coupling gate, the 0-rules-evaluated STIG scan guard, the + lsinitrd content assertions). +- **Verification evidence review:** dated bench-verification records for the + test boot path (2026-06-15/17), hardened build boot/login (2026-06-19/20), + audit offload end-to-end (2026-06-20), Wazuh decode+alert (2026-06-27), + Secure Boot enforcement on hardware (2026-06-20/22), fapolicyd overlay + effectiveness (2026-07-04), and fleet-agent TPM-mTLS telemetry (2026-07-10). +- **Not witnessed by the assessor:** the bench verifications above were + reviewed as records, not independently witnessed; the secure path (802.1X/ + MACsec) has no verification record to review. Witness points for the + follow-up assessment are listed in §7.2. + +### 2.4 Limitations and caveats + +1. **ISM control-identifier currency.** The SSP self-declares its `ISM-NNNN` + identifiers as representative of a January 2026 baseline. The reconciliation + of every identifier against the May 2026 ISM release **remains open** and is + carried as POA&M item P-17. Control *intent* mappings were assessed and are + sound; individual numbers in Annex A must not be quoted to the AO as final. +2. **Classification not confirmed.** The assessment assumes the SSP's working + classification of STARFLEET ROUTINE / STARFLEET RESTRICTED. If the kiosk + renders or the workstation can reach STARFLEET SECRET data, the control + baseline changes materially (approved cryptography becomes mandatory — R-06 + escalates from Medium to a blocking finding; gateway and media controls + tighten). +3. **Point-in-time.** This SAR reflects the system at 2026-07-10, lab + configuration, pre-production. + +## 3. Assessment results by ISM guideline area + +Effectiveness scale: **Effective** (implemented and operating as intended, with +evidence) · **Partially effective** (implemented with material gaps or lab-only +scope) · **Ineffective** (present but not achieving the control objective) · +**Not implemented** · **Alternative control** (objective met by a justified +compensating mechanism) · **Not assessed** (no visibility / out of scope). + +| ISM guideline area | Assessed effectiveness | Basis | +|---|---|---| +| Security documentation | **Partially effective** | High-quality SSP/annex/registers exist and are maintained; but no IRP, no continuous-monitoring plan, no AO approval, roles unappointed. | +| Cyber security roles | **Not implemented** | AO / System Owner / CISO all TBA. Blocking for authorisation. | +| OS & application hardening | **Effective** (current build) | STIG applied + bench-verified with a credible tailoring and deviation register; SELinux enforcing with three minimal documented local modules; fapolicyd allow-listing verified effective on the overlay root (post-2026-07-04 fix); USBGuard enforcing + verified; minimal install; kiosk lockdown. Residual: `enable_stig` must be confirmed on in production CI (R-03). | +| File integrity / anti-malware | **Alternative control — accepted** | dm-verity continuous block-level verification of a signed, reproducible root supersedes AIDE; immutable root + allow-listing + RAM-only overlay is a justified compensating posture for endpoint AV. The assessor accepts both N/A justifications. | +| Authentication (machine) | **Partially effective** | Design (EAP-TLS, non-exportable TPM key, cert in NV, no baked credentials) is strong and the same identity is demonstrated working for agent mTLS on hardware. But the 802.1X path itself is unverified and disabled in builds (R-01) and the RADIUS trust anchor is a placeholder (R-08) — the control is not currently operating. | +| Authentication (privileged users) | **Partially effective** | Key-only single admin, root locked, `staff_u` confinement with sudo role transition — good. No human MFA on host SSH; NOPASSWD sudo; fleet-generic non-rotatable key (R-05). Management-plane admin MFA (WebAuthn passkeys) is effective but sits in a connected system. | +| System management / patching | **Partially effective** | Reproducible, digest-pinned, atomic rebuild/redeploy model is excellent change control; signing keys correctly held outside repo/build. Gaps: patch cadence/SLA undefined (R-10); debug shell (R-02); actuator path deferred (R-15). | +| Event logging & monitoring | **Partially effective** | Centralised auditd offload verified end-to-end; Wazuh SIEM decode+alert verified; health telemetry adds operational visibility. Gaps: both log-offload hops plaintext, retention/archival unconfigured, Wazuh default credentials (R-07). | +| Networking | **Partially effective** | Port-based 802.1X design + segmentation + outbound-only agent are sound but the enforcement path is unverified (R-01); host firewall disabled (R-09). | +| Communications / link protection | **Not assessed → design sound** | MACsec implementation exists but has never been observed operating (MKA survival across switch-root specifically flagged by the system's own documentation as unverified). | +| Cryptography | **Partially effective** | Modern STIG crypto policy, TPM-held keys, HSM/CI signing keys. AACA/AACP conformance not asserted, FIPS off, NVMe/TCP root and log transport plaintext (R-04, R-06, R-07). Acceptable at STARFLEET ROUTINE on an isolated LAN; not above. | +| ICT equipment & media | **Effective** | Diskless/no-remanence posture is inherent and genuine; Secure Boot + signed UKI + dm-verity verified enforcing on hardware. Residual is default posture (R-13) and no remote attestation (R-11). | +| Supply chain / artifact integrity | **Effective** | FROM-scratch artifact, SHA256SUMS, digest pinning, reproducible builds, roothash hard gate in CI. | + +## 4. Strengths worth the AO's attention + +1. **Statelessness as a control.** No persistent local state, RAM-only overlay, + per-boot return to a verified root — this structurally eliminates whole ISM + control families (media sanitisation, data remanence, endpoint backup) and + sharply limits persistence for an attacker. +2. **Hardware-anchored identity, one key three uses.** Non-exportable TPM key + backing 802.1X, the NVMe hostnqn, and management-plane mTLS, with + human-anchored enrolment (OOB fingerprint + admin mTLS approval, no + auto-issue). See also observation F-06 on cross-protocol key reuse. +3. **Verification culture.** The false-clean STIG scan (datastream/CPE + mismatch evaluating 0 rules) and the inert fapolicyd overlay guard were both + found by the team's own effectiveness testing and fixed with build-time + guards. This is exactly the behaviour an assessor wants to see, and it + raises confidence in the other dated verification claims. +4. **Honest deviation management.** The STIG tailoring + per-build deviations + report with a triaged register (REVIEW rows driven to 0–1) is a model + pattern; waivers carry justifications rather than hiding findings. +5. **Recovery posture.** Rollback = revert one digest-pinned manifest commit; + compromised host recovery = reboot. Respond is structurally strong even + before an IRP exists. + +## 5. Assessor findings + +### 5.1 Concurrence with the SSP risk register + +The assessor **concurs** with the ratings, compensating controls, and +treatments for R-01 through R-15 in Annex A §3, with these adjustments: + +- **R-01 (secure path unverified) — concur High, emphasise centrality.** This + is the pivotal finding of the assessment. Until the swtpm + hostapd + + FreeRADIUS bench demonstrates port authorisation, MACsec establishment, MKA + survival across switch-root, and NVMe root mount over macsec0, the system's + primary network-preventive control set is unassessed. It must be *witnessed*, + not just recorded, at the follow-up assessment. +- **R-08 (placeholder RADIUS CA) — concur Medium, but couple to R-01.** These + close together or not at all: a verified secure path against a placeholder + trust anchor is not a verified control. +- **R-06 (AACA/AACP not asserted) — concur Medium at STARFLEET ROUTINE**, with + the explicit caveat that this becomes a **blocking finding if the + classification is confirmed STARFLEET SECRET or above** (see §2.4.2). +- **R-14 (management plane unauthorised connected system) — concur Medium.** + The host-side exposure limitation (outbound-only, telemetry-only, commands + inert) is genuine and was verified on hardware; however the host now + *trusts a new CA* (step-ca) whose key protection is unassessed. The AO + should treat the fleet CA as part of the system's effective trust boundary + even though it is administratively a connected system. +- **R-15 (deferred actuators) — concur Low, with a hard condition:** enabling + any actuator (reboot/drain/kiosk_url) without a delta assessment of the + command authorisation path would invalidate this SAR's characterisation of + the agent as read-only. + +### 5.2 Additional assessor findings (not in the SSP register) + +| ID | Finding | Severity | Detail / recommendation | +|---|---|---|---| +| **F-01** | **Assessed build ≠ production configuration.** *Partly remediated 2026-07-11 (`poam-hardening`, POA&M P-03).* A fail-closed production profile now exists: the CI "Production build gate" aborts unless `SB_KEY`/`SB_CERT`/`FLEET_CA` are set (no unsigned fallback, no placeholder CA), `enable_stig=true` is enforced by Packer variable validation, and `debug_shell` is deleted from the template. **Two of the five toggles remain unpinned in the standing CI:** `disable_dot1x` is still `true` (blocked on the P-01 secure-path bench) and production artifacts are not yet distinctly tagged (P-03 M2). | **High** → **Medium** (residual) | Complete P-03: flip `disable_dot1x=false` after the P-01 bench and add distinct production-artifact tagging; make the follow-up assessment target *that* artifact digest. Individual toggles are tracked (R-01/02/03/08/13). | +| **F-02** | **SSH host keys regenerate every boot.** The ephemeral overlay means sshd host keys are new each boot, so administrators can never pin a host key — every connection is effectively trust-on-first-use, and a MITM of admin SSH on the LAN is undetectable by the client. *Remediation IMPLEMENTED 2026-07-11 (`poam-hardening`, POA&M P-17):* the **SSH host CA** treatment (Design A, broker-mediated) — `fleet-ssh-hostcert` gets each host's per-boot key signed by the fleet SSH CA **via the enrol-broker host plane** (TPM-identity mTLS) at boot; admins trust the fleet via one `@cert-authority` line. step-ca stays behind the single front door; fail-open (bare key if the broker/CA is unreachable). | **Medium** → **Low** (once bench-verified) | Bench-verify end-to-end (broker↔step-ca SSH-CA contract, host principal policy, SELinux for the root cert-installer) per docs/ssh-host-certificate-design.md; roll the `@cert-authority` anchor to admin workstations. A stable TPM-resident host key (Design B) is a deferred option, not required. | +| **F-03** | **Time source is unauthenticated.** chrony syncs solely from a DHCP-handed lab NTP server with no NTS or symmetric-key authentication. Certificate validity decisions (EAP-TLS, agent mTLS, future log TLS) depend on host time; an attacker on the pre-auth segment who can spoof DHCP/NTP could shift time to resurrect expired/revoked certificates. *Remediation IMPLEMENTED 2026-07-11 (`secure-ntp`, POA&M P-18):* **NTS (RFC 8915)** — the serving NTP server presents a **fleet-CA-signed** NTS-KE cert; the host (`secure_ntp=true`) verifies it against `radius-ca.pem` and syncs authenticated time (server name derived at runtime from the DHCP domain — no baked FQDN), and no longer mixes in the unauthenticated DHCP source. A DHCP/DNS spoofer can now only DoS, not shift the clock. | **Low–Medium** → **Low** (once bench-verified) | Bench-verify (`chronyc authdata` NTS cookies, tcp/4460, cold-clock `nocerttimecheck`, STIG chrony) per docs/secure-ntp-design.md; issue the per-environment NTS server cert. Compensating meanwhile: isolated LAN + cert-serial inventory gate. | +| **F-04** | **Enrolment broker pre-auth endpoint lacks abuse controls.** The one unauthenticated CSR endpoint is human-gated for *issuance* (good) but the SSP records no rate-limiting/queue-flooding controls; a flooded pending-approval queue is a plausible denial/enrolment-confusion vector. Noted inside R-14's treatment text; called out here so it is not lost in the connected-system SSP. | **Low (host) / Medium (plane)** | Carry explicitly into the management-plane SSP: rate limiting, pending-CSR TTL and queue caps, and alerting on anomalous CSR volume. | +| **F-05** | **Fleet-wide availability depends on a single NVMe/TCP target.** Every running host's root filesystem is served by one unreplicated SPDK process; its failure stalls or kills the entire fleet (the system's own analysis, TODO #6). For a fleet whose stated recovery model is "reboot", loss of the root target defeats that model. Availability of the root service is a security property of this system. | **Medium** | Add to the register (availability class). The drain-gated long-lived target design mitigates *operational* restarts; structural treatment is the already-identified multipath/ANA replica design plus documented RTO. Note `ctrl_loss_tmo` tuning as the cheap interim mitigation. | +| **F-06** | **Cross-protocol reuse of the TPM identity key.** The same key authenticates EAP-TLS, derives the NVMe hostnqn, and authenticates agent mTLS ("one identity, three stages"). This is an explicit, documented design choice with real benefits (one enrolment, one revocation point); the residual concern is cross-protocol signing-oracle exposure (a signature obtained in one protocol context being replayable in another) and a single revocation event disabling all three functions at once. | **Low (observation)** | No change required at STARFLEET ROUTINE. Document a short analysis in the SSP: confirm the protocols' signed structures are domain-separated (TLS 1.2 vs 1.3 CertificateVerify context strings largely provide this), and document the operational plan for the revocation blast radius. | +| **F-07** | **No continuous assurance that runtime posture matches assessed posture.** SELinux-permissive and version-drift are surfaced via fleet-agent telemetry (good), but nothing yet alerts on: Secure Boot state, debug-shell presence, or fapolicyd non-enforcing. The system has the telemetry channel to make assessed-posture drift visible fleet-wide. | **Low** | Extend agent facts to include SB state (`SecureBoot` EFI var), fapolicyd enforcement, and cmdline hash; alert on drift in the manager. Complements R-13. | +| **F-08** | **DNS/DHCP are untrusted inputs by design — but the pattern is undocumented and two flows escape it.** The architecture consistently authenticates endpoints *above* name resolution (manager → mTLS; NTP → NTS, post-P-18; UKI → Secure Boot signature; NVMe root → dm-verity), so DNSSEC/DoT would add little and is *not* recommended. However: (a) the **kiosk URL is plain HTTP** — a DNS or DHCP (option-15 domain) spoofer serves an arbitrary page fullscreen on every workstation's only user interface, with the Chromium policy's PNA allowance to the loopback host-info agent as reachable attack surface; (b) a spoofer can serve an **older, still-validly-signed UKI** (rollback) — Secure Boot does not prevent this; the design's anti-rollback mechanisms (dbx revocation / TPM monotonic counter, design v2 §6) are unimplemented and unmentioned in the register; (c) the plaintext **audit offload** is DNS-spoofable today — closed as a side effect of P-09's TLS, but that coverage was untraceable until now. | **Medium** (driver: kiosk content flow) | POA&M P-24. State the "DNS/DHCP untrusted; authenticate above resolution" principle explicitly in the SSP; serve the kiosk over TLS with a fleet-CA-signed cert (CA pinned via Chromium policy) rather than deploying DNSSEC; record the UKI-rollback residual against R-13 with the design-§6 anti-rollback treatment; cross-reference P-09 for the audit-stream closure. | + +## 6. Essential Eight maturity (assessor view) + +The SSP's self-assessment (§6) is broadly fair. Assessor adjustments: +application control is genuinely **strong post-2026-07-04** (and, notably, was +*over-claimed before* that fix — the honest correction is credited); +"restrict admin privileges" and "MFA" should both be read as **partial** until +the host SSH access model gains rotation and the management plane is +authorised; "patching" remains **process-only** until R-10 defines an SLA. +Against Essential Eight maturity levels, the system sits at approximately +**ML1–ML2 overall** with individual mitigations (application control, user +application hardening) performing at ML3-like strength — but E8 maturity +formally requires the *whole* mitigation strategy set at level, so no formal +level claim above ML1 should be made yet. + +## 7. Recommendation to the Authorising Officer + +### 7.1 Determination + +- **Production ATO: not recommended at this time.** +- **Interim authorisation (lab/pre-production, isolated LAN, no production + data): recommended**, valid for a defined period (suggest ≤ 6 months), + conditional on the POA&M being maintained and on no production or + classified data being processed. + +### 7.2 Conditions precedent for a production ATO + +The following must be closed, then verified at a **delta assessment** at which +the assessor witnesses (not merely reviews) the evidence: + +1. **Witness the secure-path bench test** (R-01 + R-08): real RADIUS CA; port + `Authorized`; macsec0 addressed; MKA survives switch-root; NVMe root + mounted over macsec0; lease renewal on macsec0 past expiry. +2. **Production build profile** (F-01): a signed, SB-enforcing, STIG-on, + debug-shell-off, dot1x-on build produced by CI with hard assertions; + assessor verifies `sbverify` on UKI + iPXE, `mokutil --sb-state` on sample + hosts, absence of the debug shell, and a valid STIG deviations report with + 0 unjustified REVIEW rows on the assessed digest (R-02, R-03, R-13). +3. **Governance**: AO/System Owner/CISO appointed; data classification + confirmed by the System Owner; IRP, continuous-monitoring plan and + log-retention policy authored and approved (R-12, §2.1 gaps). +4. **Log transport and retention** (R-07): TLS on both offload hops; retention + configured; Wazuh credentials rotated and dashboard/API restricted. +5. **ISM reconciliation** (P-17): Annex A identifiers reconciled to the May + 2026 ISM, unmapped new controls dispositioned. +6. **Management plane**: either a granted authorisation for the connected + system (R-14) or an AO-accepted interim risk position covering the fleet + CA's key protection specifically. +7. **Witness re-verifications**: reproducible build (two builds, identical + verity root hash — including post-`semanage` policy-store stability); + fapolicyd denial of a novel binary on the overlay root; USBGuard + mass-storage block with console HID intact; agent actuators inert (R-15). + +Items R-04, R-05, R-09, R-10, R-11, F-02, F-03, F-05, F-08 may, at the AO's +discretion, be **accepted as residual risk with POA&M target dates** for a +production ATO at STARFLEET ROUTINE on an isolated/managed network, given the +compensating controls recorded in Annex A. They are not recommended for +indefinite acceptance. + +### 7.3 Revocation triggers the AO should set + +- Enabling fleet-agent actuators without a delta assessment (R-15 / §5.1). +- Processing data above the confirmed classification. +- Any unsigned or SB-off build deployed outside the lab (R-13 / F-01). +- Lapse of the POA&M review cadence (suggest quarterly). + +--- + +*End of SAR. The consolidated remediation plan with owners, milestones and +target dates is at [irap-poam.md](irap-poam.md).* diff --git a/docs/secure-ntp-design.md b/docs/secure-ntp-design.md new file mode 100644 index 0000000..78e5393 --- /dev/null +++ b/docs/secure-ntp-design.md @@ -0,0 +1,127 @@ +# Secure NTP design — CA-authenticated time via NTS (F-03 / P-18) + +> Status: **implemented (host + serving scaffold), BENCH-VERIFY.** Branch +> `secure-ntp`. Closes SAR finding **F-03** / POA&M **P-18**. + +## Problem + +The host clock is an input to **every** certificate-validity decision (EAP-TLS to +RADIUS, agent mTLS, the SSH host-cert flow, future log TLS). Today chrony syncs +from a **DHCP-handed** NTP server (option 42) with **no authentication**. An +attacker on the pre-auth segment who can spoof DHCP or NTP can shift the clock — +e.g. forward it to make an **expired/revoked** certificate look valid. The +isolated LAN and the cert-**serial** inventory gate are only partial compensations. + +## Approach — NTS (RFC 8915), CA-signed server authentication + +**NTS** is the modern, CA-based way to authenticate an NTP server. The server runs +an **NTS-KE** (key-establishment) endpoint over **TLS 1.3** on tcp/4460; the client +verifies the server's TLS certificate against a trust anchor, then uses the +negotiated keys to authenticate the NTP (udp/123) packets that carry the time. + +This maps onto the fleet's existing PKI: the NTS-KE cert is issued by the **fleet +step-ca**, so it chains to the **same `radius-ca.pem`** the hosts already trust. +No new trust anchor. + +Symmetric-key NTP and Autokey were rejected: neither is CA-based (symmetric needs +per-server shared secrets; Autokey is deprecated and broken). + +## No baked FQDN — the server name is derived from the DHCP domain + +NTS validates the server's TLS cert against the *name/address the client used*. We +deliberately **do not bake an NTP FQDN** into the image (a site's domain may change +and we don't want to rebuild images for that). Instead, at runtime the NM +dispatcher takes the **DHCP-provided domain** (`$DHCP4_DOMAIN_NAME`, option 15; +falls back to the first `$DHCP4_DOMAIN_SEARCH`, option 119) and forms: + +``` + server . iburst nts # e.g. ntp.fleet.lab +``` + +Only the **label** `ntp_prefix` (default `ntp`) and the **fleet CA** are baked — +both stable. Change the site domain? Hand a new DHCP domain and reissue the NTS +cert with the new SAN; **no image rebuild**. The cert uses a clean **DNS SAN** +(not an IP SAN, which deriving from DHCP option-42 IPs would have forced). + +## Architecture + +``` + serving 'ntp' (chrony): + pool pool.ntp.org iburst # upstream (serving host only) + allow all # serve the boot LAN + ntsservercert /etc/chrony/nts/nts.crt # FLEET-signed, SAN = ntp. + ntsserverkey /etc/chrony/nts/nts.key + ▲ tcp/4460 NTS-KE (TLS 1.3) + udp/123 authenticated NTP + │ + dnsmasq: option 15 domain-name = ; A ntp. -> serving host + │ + host (secure_ntp=true): + NM dispatcher (10-chrony-dhcp-ntp, mode=nts): + domain=$DHCP4_DOMAIN_NAME -> /run/chrony-dhcp/.sources: + server ntp. iburst nts + chrony.conf: + sourcedir /run/chrony-dhcp + ntstrustedcerts /etc/dot1x/radius-ca.pem # the fleet CA verifies the server + nocerttimecheck 1 # bootstrap a cold boot clock once +``` + +**Components** + +| Piece | Where | Notes | +|---|---|---| +| mode + prefix, chrony NTS directives | `diskless.pkr.hcl` (chrony provisioner) | `secure_ntp` (default true), `ntp_prefix` (default `ntp`); bakes `ntstrustedcerts`/`nocerttimecheck` + `sourcedir` | +| runtime name derivation | `files/etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp` | mode-aware: `nts` derives `ntp.`; `dhcp` = legacy option-42 IPs | +| NTS server directives | `serving/chrony/chrony.conf` | `ntsservercert`/`ntsserverkey`/`ntsdumpdir` | +| NTS server cert issuance | `serving/chrony/nts-cert.sh` | `step ca certificate` (DNS SAN = `NTP_HOST` = `ntp.`) | +| compose mount + firewall + domain handout | `serving/docker-compose.yml` | mounts `./chrony/nts`; udp/123 **and tcp/4460**; DHCP option 15 | +| DNS resolution | dnsmasq `--address=/${NTP_HOST}/` + `NTP_HOST` in `defaults.env` | `ntp.fleet.lab` → serving host | + +## Why `nocerttimecheck` — the chicken-and-egg + +NTS-KE is TLS, so the client checks the server cert's **validity window** — which +needs a roughly-correct clock. A diskless host has no reliable RTC, so its boot +clock can be far enough off (or epoch) to make a *valid* cert look +not-yet-valid/expired, and NTS would never bootstrap. `nocerttimecheck 1` lets the +**first** measurement set the clock without the cert-time check; **every +subsequent** update enforces it. The trade-off (one measurement could accept an +expired cert) is bounded by the isolated LAN and the single-measurement window, +and is the standard chrony guidance for RTC-less devices. + +## Security properties + +- **Authenticated time:** a DHCP/NTP/**DNS** spoofer can only cause **no-sync + (DoS)** — it cannot present a cert that chains to the fleet CA for the name it + redirects you to, and NTS never falls back to unauthenticated time. So the clock + cannot be shifted (the whole point of F-03). This holds *because of NTS*, even + though DHCP/DNS themselves are unauthenticated. +- **No baked FQDN, no per-host state (invariant #1):** only the `ntp` label + the + fleet CA (already baked) are fixed; the domain is learned from DHCP at runtime. +- **No unauthenticated source mixed in:** in `nts` mode the dispatcher writes only + the NTS source; the option-42 IP handout is ignored. +- **Availability:** `local stratum 10` on the server keeps time serving if upstream + is down; a host that can't reach NTS-KE simply holds its clock (no bad time). + +## Bench checklist (BENCH-VERIFY) + +1. Serving: run `serving/chrony/nts-cert.sh` (after the CA is up); confirm + `serving/chrony/nts/{nts.crt,nts.key}` exist with **DNS SAN = `ntp.`**; + restart the `ntp` service; open **tcp/4460** + udp/123 on the boot NIC. +2. Domain handout: on a booted host confirm the DHCP domain arrived — + `nmcli -g DHCP4.OPTION.* dev show | grep domain` or the dispatcher wrote + `/run/chrony-dhcp/.sources` with `server ntp. iburst nts`. +3. Host: `chronyc -N authdata` shows the source in `NTS` mode with cookies > 0; + `chronyc sources` shows `ntp.` reachable and selected. +4. Spoof test: a rogue NTP/DHCP/DNS source is **not** trusted (NTS handshake fails + ⇒ not selected) — DoS at worst, never a clock shift. +5. Cold-clock test: boot with the RTC set far off; confirm `nocerttimecheck 1` + lets the first sync land, then the clock tracks. +6. Domain-change test: change the DHCP domain + reissue the NTS cert with the new + SAN; confirm the SAME image picks up `ntp.` with no rebuild. +7. STIG: confirm remediation doesn't strip `ntstrustedcerts`/`nocerttimecheck` or + the `nts` option in the sourcedir (host is a client; STIG-compatible). + +## Toggle + +`secure_ntp=false` reverts the dispatcher to the legacy unauthenticated DHCP +option-42 path (plain `server iburst`) for lab/interop. Keep it **true** for +production. diff --git a/docs/ssh-host-certificate-design.md b/docs/ssh-host-certificate-design.md new file mode 100644 index 0000000..015b97e --- /dev/null +++ b/docs/ssh-host-certificate-design.md @@ -0,0 +1,140 @@ +# SSH host-certificate design — killing per-boot TOFU (F-02 / P-17) + +> Status: **implemented (host + broker + serving scaffold), BENCH-VERIFY.** Branch +> `poam-hardening`. Closes SAR finding **F-02** / POA&M **P-17**. Signing is +> **broker-mediated** (host → enrol-broker host plane → step-ca), not host-direct. + +## Problem + +The diskless root is an immutable erofs + a **RAM overlay**, so +`sshd-keygen@.service` regenerates the SSH **host keys on every boot** (see +`scripts/extract-artifacts.sh`, which strips baked host keys — a baked key would +be shared fleet-wide, invariant #1). Consequences: + +- Administrators can never pin a host key — every `ssh` to a fleet host is + **trust-on-first-use**, and the key *changes each reboot* so even a cached entry + triggers the "REMOTE HOST IDENTIFICATION HAS CHANGED" warning. +- A **LAN man-in-the-middle** of admin SSH is undetectable client-side. + +## Approaches considered + +- **A — per-boot key + boot-time CA-signed host cert (CHOSEN).** Keep the + ephemeral per-boot host key; at boot, get it **signed by the fleet SSH CA** over + a TPM-authenticated request. Clients trust via one `@cert-authority` line. The + TPM only *authenticates the signing request*; it is not the host key. +- **B — dedicated stable TPM host key + `HostKeyAgent` + cert at enrolment.** + Hardware-anchored, CA-independent, stable host identity — but needs a new + persistent TPM handle, a host-key agent daemon (SELinux/fapolicyd/ordering), and + more provisioning. Deferred; a follow-on if a CA-independent hardware host key is + specifically wanted. + +**The certificate — not the key's origin — is what removes TOFU.** A per-boot key +*with a cert* is fine (short-lived keys are good hygiene), so A delivers the +security outcome with the least new attack surface and no added cross-protocol key +reuse. See the chat rationale; A was chosen 2026-07-11. + +## Architecture (Design A, broker-mediated) + +The signing request goes **through the enrol-broker**, not directly to step-ca — +so step-ca stays behind the single front door (consistent with enrolment), and the +proven `step` CLI does the SSH signing server-side. The host authenticates to the +broker's new **host plane** with its TPM identity over mTLS. + +``` + boot ─▶ sshd-keygen@ecdsa ─▶ /etc/ssh/ssh_host_ecdsa_key(.pub) (ephemeral) + │ + ssh-hostcert.timer (OnBootSec + hourly) + └─▶ fleet-ssh-hostcert: + 1. TPM: LoadSigner(0x81000100) + ReadNVCert(0x01800100) → mTLS client cert + 2. read ssh_host_ecdsa_key.pub + 3. POST {publicKey, principals, keyID} ─(mTLS)─▶ enrol-broker host plane + 4. write ssh_host_ecdsa_key-cert.pub + 60-hostcert.conf; reload sshd + + enrol-broker host plane (POST /ssh/hostcert): + verify client cert to the fleet CA (⇒ enrolled host) + └─▶ step ssh certificate --host --sign (broker holds the provisioner) ─▶ step-ca SSH CA + └─▶ returns {cert} + + client: @cert-authority *.fleet.lab (one line, once) +``` + +**Components** + +| Piece | Where | Notes | +|---|---|---| +| `fleet-ssh-hostcert` | `files/ssh-hostcert/` (Go, static, go-tpm) | TPM mTLS client; out-of-VM CI build like fleet-enrol/agent; deps pinned by go.sum | +| `ssh-hostcert.service` + `.timer` | `files/ssh-hostcert/` | **root** oneshot (writes `/etc/ssh`, reloads sshd), sandboxed; timer = boot + hourly renew | +| `ssh-hostcert.env` | `files/ssh-hostcert/` → `/etc/ssh-hostcert/` | fleet-generic broker URL / CA / paths | +| Packer `2b-ssh-hostcert` | `diskless.pkr.hcl` | gated on `enable_ssh_hostcert`; fapolicyd-trusts the binary in 2c-STIG-postfix | +| broker **host plane** (`/ssh/hostcert`) | `serving/enrol-broker/host.go` + `sshsigner.go` | mTLS (fleet-CA host certs); `step ssh certificate` signer; enabled by `BROKER_HOST_ADDR` | +| step-ca SSH CA enablement | `serving/dot1x-bench/step-ca/ssh-ca-bootstrap.sh` | SSH CA keys + provisioner `--ssh` + broker host-plane cert; prints the admin `@cert-authority` line | + +## Wire contract (the BENCH-VERIFY seam) + +- **Host → broker** (mTLS): `POST /ssh/hostcert` `{publicKey, principals, keyID}` + → `{cert}`. The host's mTLS client cert is its TPM identity; the broker verifies + it to the fleet CA (`RequireAndVerifyClientCert`) — *that* is the authorisation + (an enrolled host). No token, no CA secret on the host. +- **Broker → step-ca**: `step ssh certificate --host --sign …` with the broker's + provisioner credential. The exact flags are the tunable part + (`serving/enrol-broker/sshsigner.go`); the CA must have its **SSH host CA + enabled** and the provisioner allowed to issue SSH host certs + (`ssh-ca-bootstrap.sh`). + +**Principal authorisation** is the remaining sharp edge: step-ca / the provisioner +must *authorise* the requested principals (the host's hostname). Tighten this in +the broker (validate `principals` against the peer cert) and/or via an SSH host +template on step-ca. Today the broker forwards the requested principals and logs +the peer CN. + +## Security properties + +- **Invariant #1 (no per-host state):** the image carries only a fleet-generic + broker DNS name + CA path. The host identity, and therefore the cert, derive from + the TPM at runtime. Principals default to the runtime hostname (not baked). +- **step-ca stays behind the front door:** hosts reach the broker, not step-ca. The + broker holds the provisioner credential; a host can never self-sign. +- **Fail-open:** never installs a missing/broken `HostCertificate` — a broker/CA + outage degrades to today's bare-key behaviour, not a boot/sshd failure. +- **Key reuse (cf. F-06):** the TPM identity authenticates the mTLS request; the + SSH host key is a separate ephemeral key. +- **Short-lived certs** + hourly renewal bound the blast radius of a leaked cert + (the key is ephemeral anyway). + +## Client rollout + +One line per admin (or a system `known_hosts`), from `ssh-ca-bootstrap.sh` output: + +``` +@cert-authority *.fleet.lab ecdsa-sha2-nistp256 AAAA... # fleet SSH host CA +``` + +## Bench checklist + +1. Serving: run `ssh-ca-bootstrap.sh`; confirm `ca.json` has the `ssh` block, the + `enrol` provisioner is SSH-enabled, and `pki/broker/{host-server.crt, + fleet-client-ca.crt}` + `pki/ssh_host_ca.pub` exist. Bring up the broker with + `BROKER_HOST_ADDR` set; `journalctl` shows "host plane up". +2. Broker: from an enrolled host's identity, `curl --cert/--key … https:// + boot.fleet.lab:9444/ssh/hostcert` returns a `{cert}`; an un-enrolled client is + rejected at the TLS handshake (`RequireAndVerifyClientCert`). +3. Host: `systemctl start ssh-hostcert.service`; `journalctl -u ssh-hostcert` + shows "installed SSH host certificate"; `/etc/ssh/ssh_host_ecdsa_key-cert.pub` + present; `sshd -T | grep hostcertificate`. +4. SELinux: watch for AVCs on the root service writing `/etc/ssh/*` and running + `systemctl reload sshd` under confinement — add a local TE module if needed + (like `fleet_agent_chronylog`). Reproducibility: a new `semodule` would rebuild + the policy store → re-check verity-hash stability across two builds. +5. Client: with the `@cert-authority` line set, `ssh ` shows **no** host-key + prompt/warning across reboots. +6. Confirm STIG crypto policy accepts the `ecdsa-sha2-nistp256-cert-v01` host cert + (same family already accepted for admin keys). +7. Principal policy: confirm the broker/step-ca only issue the principals a host is + entitled to (tighten `handleSSHHostCert` to validate against the peer cert). + +## Deferred (Design B follow-on) + +Stable, CA-independent hardware host key: a dedicated TPM key at a new persistent +handle (e.g. `0x81000101`, avoiding OEM/reserved NV like the identity key) served +to sshd via a small TPM `HostKeyAgent`, cert issued at enrolment. Only pursue if a +host identity that survives a CA outage is required. diff --git a/docs/ssp-annex-a-ism-residual-risk.md b/docs/ssp-annex-a-ism-residual-risk.md index 635f1af..37f2187 100644 --- a/docs/ssp-annex-a-ism-residual-risk.md +++ b/docs/ssp-annex-a-ism-residual-risk.md @@ -122,7 +122,7 @@ ISM/PSPF style). Ratings are **pre-treatment** unless stated; the AO accepts the | Secure build & change control | ISM-1483* | Controlled, repeatable builds. | Reproducible Packer build in CI; digest-pinned deployment; one commit/release; rollback = revert commit. | Implemented | — | | Protect build/signing key material | ISM-0501*, ISM-1233* | Protect keys. | Signing keys CI/HSM-held; **never** in repo or build VM; never logged. | Implemented | — | | Privileged actions logged | ISM-1509* | Admin actions auditable. | auditd rules capture privileged/admin events. | Implemented | R-07 (offload protection) | -| Disable debug/back-door access | ISM-1491* | No undocumented access paths. | `debug_shell=true` currently bakes a **passwordless root tty** (lab debugging). | Partial | **R-02 (must remove before prod)** | +| Disable debug/back-door access | ISM-1491* | No undocumented access paths. | The `debug_shell` passwordless-root-tty option was **removed from the template 2026-07-11** (`poam-hardening`); no build can bake it. | Implemented | R-02 (remediated); F-07 (runtime drift telemetry) | | Device enrolment & identity lifecycle | ISM-1195*, ISM-0475* | Controlled device onboarding. | **TPM enrolment broker** (v0.3): the host generates a key in-TPM, submits a CSR to the one *unauthenticated pre-auth* endpoint; an admin verifies an **OOB fingerprint** and **approves over mTLS**, then **step-ca** signs the leaf into TPM NV and the manager births the inventory record (cert-serial = key). No auto-approval; identity is human-anchored. Server-side = connected system. | Partial (connected system) | R-14 | | Server-authoritative management plane | ISM-1483*, ISM-1509* | Controlled remote management. | `fleet-agent` streams telemetry + receives desired-state over mTLS; **v1 logs commands but does not act** (no host mutation). Inventory/desired-state is server-authoritative. Reconcile actuators deferred. | Partial | R-14, **R-15 (actuators = future remote-mutation channel)** | @@ -187,8 +187,8 @@ asked to accept. Treatments map to [docs/TODO.md](TODO.md) (working POA&M). | ID | Residual risk | ISM linkage | Likelihood × Consequence → **Rating** | Existing compensating controls | Treatment (POA&M) | Post-treat. residual | Owner / target | |---|---|---|---|---|---|---|---| | **R-01** | Secure path (802.1X EAP-TLS + MACsec) **implemented but not bench-verified end-to-end**; disabled in test builds (`disable_dot1x=true`). Until verified, devices may attach to an unauthenticated/unencrypted link. | 2.4, 2.7, 2.8 (ISM-1546*, 0520*, 1187*) | Likely × Major → **High** | Test path on an isolated lab LAN; full auth stack present (dormant); design follows ASD intent. | Full boot-chain bench test (swtpm + hostapd + FreeRADIUS): port `Authorized`, macsec0 addressed, MKA survives switch-root, NVMe root mounts. Re-enable in CI. | **Low** (once verified) | Platform team — *before production ATO* | -| **R-02** | `debug_shell=true` bakes a **passwordless root shell** on a tty — an undocumented privileged access path. | 2.5 (ISM-1491*) | Possible × Severe → **High** | Lab-only; physical access required; isolated environment. | Set `debug_shell=false` for all non-lab builds; assert absent in CI. | **Low** | Platform team — *before production* | -| **R-03** | STIG hardening **applied + bench-verified** (2026-06-20, all REVIEW rows triaged); residual is ensuring **CI ships `enable_stig=true`** for production builds (it was toggled off during bring-up). | 2.3 (ISM-1407*/1408*) | Possible × Moderate → **Medium** | Tailoring + register + per-build report; SELinux/fapolicyd/USBGuard on; verified on a real hardened build. | Confirm `ENABLE_STIG="true"` in CI; keep the 0-eval guard; re-run the deviations report on the assessed build. | **Low** | Platform team — *before assessment* | +| **R-02** | ~~`debug_shell=true` bakes a **passwordless root shell** on a tty~~ — **REMOVED 2026-07-11** (`poam-hardening`): the `debug_shell` variable and its `debug-shell.service` provisioner were deleted from the template; no build path can bake a debug shell. | 2.5 (ISM-1491*) | ~~Possible × Severe → **High**~~ → **eliminated at source** | Lab-only; physical access required; isolated environment (historical). | **Done** — removed from the template. Assessor verifies absence on the assessed digest (POA&M P-02 M2). | **Low** (residual = drift only, F-07) | Platform team — **remediated** | +| **R-03** | STIG hardening **applied + bench-verified** (2026-06-20, all REVIEW rows triaged); `enable_stig=true` is now **enforced by Packer variable validation** (a false build fails `packer validate`) and CI fails closed on unsigned/placeholder-CA builds (2026-07-11). Residual: confirm on the *assessed* digest. | 2.3 (ISM-1407*/1408*) | Possible × Moderate → **Medium** | Tailoring + register + per-build report; SELinux/fapolicyd/USBGuard on; verified on a real hardened build; enforced-true toggle + 0-eval guard. | Re-run the deviations report on the assessed (signed) build; keep the 0-eval guard. | **Low** | Platform team — *before assessment* | | **R-04** | NVMe/TCP **root traffic plaintext** on the LAN — exposure of OS content / injection if the segment is compromised. | 2.9 (ISM-1139*/1453*) | Unlikely × Major → **Medium** | Isolated boot LAN; MACsec on the access link (once R-01 done) protects the first hop; dm-verity makes injected root blocks fail verification (integrity preserved even if confidentiality isn't). | Production: TLS/IPsec for the storage transport, or rely on MACsec end-to-end across the fabric; document the decision. | **Low–Medium** | Platform/network — *production* | | **R-05** | **NOPASSWD key-only host admin**; no per-host or rotatable access model. (Management-plane admin MFA is now **enforced** via WebAuthn passkeys — v0.3; residual is the *host SSH* access model.) | 2.4 (ISM-1507*/1173*) | Possible × Moderate → **Medium** | Single fleet-generic admin, key-only, root disabled, ECDSA/RSA key, **SELinux-confined to `staff_u`** with sudo role/type elevation, fully audited; no interactive password to attack. Mgmt-plane admin actions now behind a phishing-resistant passkey. | Per-host/rotatable **host** access (SSH CA / config-agent); the mgmt-plane MFA clause is now met. | **Low–Medium** | Platform team — *roadmap* | | **R-06** | **FIPS off**; ISM **AACA/AACP conformance not formally asserted**; crypto-module evaluation status unconfirmed. | 2.9 (ISM-0471*/1139*/1277*) | Possible × Major → **Medium** | Modern STIG crypto policy; non-exportable TPM key; no deliberately weak algorithms. Acceptable at OFFICIAL; **mandatory to resolve for PROTECTED+.** | Assert AACA/AACP conformance; decide on FIPS module / evaluated crypto alongside Secure Boot signing. | **Low** (at appropriate classification) | Security + Platform — *before PROTECTED+* | @@ -247,8 +247,13 @@ Secure Boot signing verified enforcing on real hardware (R-13 Medium→Low).* 5. **Review the STIG generated report** for the build under assessment — every non-pass rule should map to the [deviation register](stig-deviations.md); anything flagged **REVIEW** is an unjustified finding. -6. **Confirm lab-only flags are off** in the assessed build: `debug_shell=false`, - `enable_stig=true`, `disable_dot1x=false`, real RADIUS CA in place. +6. **Confirm lab-only flags are off** in the assessed build. As of 2026-07-11 + (`poam-hardening`) most are now enforced fail-closed rather than convention: + `debug_shell` no longer exists (removed from the template); `enable_stig=true` + is enforced by Packer variable validation; the CI Production build gate fails + closed unless `SB_KEY`/`SB_CERT`/`FLEET_CA` are set (signed UKI + real RADIUS + CA, no placeholder). **Still verify by hand:** `disable_dot1x=false` (not yet + enforced — blocked on the P-01 secure-path bench). 7. **Confirm boot-chain signing is the default** (R-13): signing + Secure Boot enforcement are **verified on hardware** (VM HTTP + 820 PXE, 2026-06-22) — confirm the *assessed* build is signed and the *assessed* hosts have `db` diff --git a/docs/ssp.md b/docs/ssp.md index d8814ef..569d1f4 100644 --- a/docs/ssp.md +++ b/docs/ssp.md @@ -367,8 +367,8 @@ production ATO: | ID | Residual risk | Rating (pre-treatment) | Disposition | |---|---|---|---| | R-01 | Secure path (802.1X/MACsec) not yet bench-verified; currently disabled in test builds. | **High** | Treat — verify before production authorisation. | -| R-02 | `debug_shell=true` bakes a **passwordless root shell** on a tty. | **High** | Treat — must be removed before production (lab-only). | -| R-03 | STIG hardening **applied + bench-verified** (2026-06-20); residual is that CI ships **unsigned, Secure Boot OFF** and `enable_stig` must be confirmed on for production builds. | **Medium** | Treat — confirm `enable_stig=true` in CI + enforce signing/SB before assessment. | +| R-02 | ~~`debug_shell=true` bakes a **passwordless root shell** on a tty.~~ **Removed 2026-07-11** (`poam-hardening`): the `debug_shell` variable and its provisioner were deleted from the template — no build can bake a debug shell. | **High → residual Low** | **Remediated** — assessor to verify absence on the assessed digest (POA&M P-02 M2). | +| R-03 | STIG hardening **applied + bench-verified** (2026-06-20); `enable_stig=true` is now **enforced by Packer variable validation** and CI fails closed on unsigned/placeholder-CA builds (2026-07-11). Residual: signing/SB must be confirmed on the *assessed* digest. | **Medium** | Treat — confirm signed + `enable_stig=true` on the assessed build (P-03/P-04/P-05). | | R-04 | NVMe/TCP root traffic **plaintext** on the lab LAN. | **Medium** | Accept (isolated LAN) / treat for production. | | R-05 | NOPASSWD key-only **host** admin (now `staff_u`-confined); no per-host/rotatable access model. (Management-plane admin MFA now enforced via WebAuthn passkeys — v0.3.) | **Medium** | Accept with compensating controls / roadmap. | | R-06 | **FIPS off**; ASD-approved-cryptography conformance not yet asserted. | **Medium** | Treat — required for PROTECTED+; revisit with signing. | diff --git a/docs/stig-deviations.md b/docs/stig-deviations.md index f3f7891..a4e5b27 100644 --- a/docs/stig-deviations.md +++ b/docs/stig-deviations.md @@ -109,8 +109,9 @@ SELinux user (`semanage login`) so the `staff_r`→`sysadm_r` sudo transition is authorized at runtime. `staff_u` (not `sysadm_u`) is used so SSH login isn't gated by the `ssh_sysadm_login` boolean. **⚠ Two bench gates before trusting:** (a) admin SSH login + `sudo` + every privileged op must work under `staff_u` -confinement — if it locks out, `enable_stig=false` is the escape hatch and this -reverts to a justified deviation; (b) reproducibility — `semanage` rebuilds the +confinement — if it locks out, waive the offending rule in the tailoring as a +justified deviation (STIG is now enforced-true — there is no `enable_stig=false` +escape hatch, POA&M P-03); (b) reproducibility — `semanage` rebuilds the SELinux policy store, so confirm the verity hash is stable across two builds (and normalize in 2d hygiene if it drifts). @@ -135,7 +136,8 @@ reboot** — it cannot exfiltrate data, alter the read-only root, or bypass auth a shell. Authenticated reboot paths (`systemctl reboot` over the key-only admin SSH session) are unaffected either way. The console itself is otherwise locked down (kiosk autologin to a single locked browser; no getty shell on tty1; -`debug_shell` is lab-only and never shipped). If a future deployment wants the +the `debug_shell` back-door was removed from the template entirely, 2026-07-11). +If a future deployment wants the hotkey disabled (e.g. a physically exposed terminal), re-select both rule IDs. ## 14. Local SELinux policy module `chrony_dhcp_dispatcher` (added 2026-07-03) @@ -214,7 +216,9 @@ turns app-allowlisting ON for the whole root at runtime for the first time**, so **BENCH-VERIFY HARD:** boot + kiosk render + admin SSH + sudo + TPM enrolment all still work (nothing legit is untrusted — the 4 custom binaries + rpmdb are trusted), a novel `/tmp` binary is now **blocked** (EPERM), and no new AVCs (extend the `.te` -if any). `enable_stig=false` is the escape hatch if it regresses the boot/secure path. +if any). If it regresses the boot/secure path, waive the offending rule in the +tailoring as a justified deviation — STIG is enforced-true (no `enable_stig=false` +escape hatch, POA&M P-03). **VERIFIED 2026-07-04 (build `fapolicyd-tmpfs-12043d3`, host `comet-beacon-54`) — enforcing, no false positives.** `fapolicyd active (running)`, `permissive = 0`, diff --git a/files/etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp b/files/etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp index 5941eb6..a78f206 100644 --- a/files/etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp +++ b/files/etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp @@ -1,16 +1,30 @@ #!/bin/bash -# NetworkManager dispatcher: feed DHCP option 42 (NTP servers) into chrony. +# NetworkManager dispatcher: point chrony at the fleet NTP server, learned from +# DHCP at runtime (no FQDN baked into the image). Two modes, baked at build time: # -# Replaces the old dhclient.d/chrony.sh bridge — we use NetworkManager's internal -# DHCP now (the network-manager dracut port), and NM exports the lease's NTP -# servers to dispatcher scripts as $DHCP4_NTP_SERVERS. chrony.conf carries -# `sourcedir /run/chrony-dhcp`, so writing .sources there and reloading -# makes chronyd use the DHCP-provided server. The image's baked public pool is -# stripped (see the provisioner), so the host syncs ONLY from the lab NTP server -# — no time egress from the workstation. +# nts (secure_ntp=true, default): AUTHENTICATED time. Derive the server NAME +# from the DHCP-provided domain as . and write +# `server NAME iburst nts`. chrony verifies the NTS-KE TLS cert against the +# fleet CA (chrony.conf: ntstrustedcerts). Because the domain comes from +# DHCP, a domain change needs only a new DHCP domain + reissued NTS cert — +# no image rebuild. A DHCP/NTP/DNS spoofer can only DoS (no fleet-CA-signed +# cert for the name it redirects to => NTS fails => no sync); it cannot +# shift the clock. See docs/secure-ntp-design.md (F-03/P-18). # -# Installed 0755 root:root at /etc/NetworkManager/dispatcher.d/. Lenient on -# purpose: a hiccup here must never break networking. +# dhcp (secure_ntp=false): LEGACY, UNAUTHENTICATED. Write the DHCP option-42 +# server IPs as plain `server iburst`. Lab/interop only. +# +# We use NetworkManager's internal DHCP (the network-manager dracut port); NM +# exports the lease's domain as $DHCP4_DOMAIN_NAME (option 15) / $DHCP4_DOMAIN_SEARCH +# (option 119) and NTP servers as $DHCP4_NTP_SERVERS (option 42). chrony.conf +# carries `sourcedir /run/chrony-dhcp`, so writing .sources there and +# reloading makes chronyd use them. The baked public pool is stripped (provisioner). +# +# Installed 0755 root:root. Lenient on purpose: a hiccup here must never break +# networking. @NTP_MODE@/@NTP_PREFIX@ are substituted at build time. + +MODE="@NTP_MODE@" # nts | dhcp +PREFIX="@NTP_PREFIX@" # nts: server name = . interface="$1" action="$2" @@ -24,13 +38,23 @@ dir=/run/chrony-dhcp file="$dir/${interface}.sources" mkdir -p "$dir" -if [ -n "${DHCP4_NTP_SERVERS:-}" ]; then - : > "$file" - for s in $DHCP4_NTP_SERVERS; do - printf 'server %s iburst\n' "$s" >> "$file" - done - # Pick up the new sources without restarting chronyd. - chronyc reload sources >/dev/null 2>&1 || true +if [ "$MODE" = nts ]; then + # Prefer option 15 (domain-name); fall back to the first option-119 search domain. + domain="${DHCP4_DOMAIN_NAME:-}" + [ -z "$domain" ] && domain="${DHCP4_DOMAIN_SEARCH%% *}" + if [ -n "$domain" ]; then + printf 'server %s.%s iburst nts\n' "$PREFIX" "$domain" > "$file" + # Pick up the new source without restarting chronyd. + chronyc reload sources >/dev/null 2>&1 || true + fi +else + if [ -n "${DHCP4_NTP_SERVERS:-}" ]; then + : > "$file" + for s in $DHCP4_NTP_SERVERS; do + printf 'server %s iburst\n' "$s" >> "$file" + done + chronyc reload sources >/dev/null 2>&1 || true + fi fi exit 0 diff --git a/serving/chrony/chrony.conf b/serving/chrony/chrony.conf index 33f0d7a..ee11c4e 100644 --- a/serving/chrony/chrony.conf +++ b/serving/chrony/chrony.conf @@ -1,7 +1,8 @@ # Lab NTP server (serving stack). Syncs from upstream and serves the boot LAN so -# diskless workstations get time INTERNALLY (handed out as DHCP option 42 by -# dnsmasq) instead of egressing to the public pool. Only this host reaches the -# internet for time; the workstations never do. +# diskless workstations get time INTERNALLY instead of egressing to the public +# pool. Only this host reaches the internet for time; the workstations never do. +# With secure_ntp=true the workstations use NTS (below) and authenticate this +# server's identity before trusting its time. # Upstream time source (this serving host has internet; the workstations don't). pool pool.ntp.org iburst @@ -10,6 +11,20 @@ pool pool.ntp.org iburst # to your subnet (e.g. `allow 192.168.50.0/24`) for anything less trusted. allow all +# ---- NTS (RFC 8915): CA-AUTHENTICATED time for the fleet ------------------- +# The workstations verify THIS server's identity before trusting its time +# (secure_ntp=true in the image; closes F-03/P-18). The NTS-KE TLS cert is signed +# by the FLEET CA (step-ca), so it chains to the same radius-ca.pem the hosts +# already trust; its SAN must be the name the hosts use (ntp_server / NTP_HOST, +# default ntp.fleet.lab). Issue it with ./nts-cert.sh (mounted at /etc/chrony/nts). +# Without these files the server still answers plain NTP, but NTS clients get an +# "NTS-KE handshake" failure. NTS-KE listens on tcp/4460 — open it on the boot-LAN +# NIC (see docker-compose.yml), same trap as 123/udp. docs/secure-ntp-design.md. +ntsservercert /etc/chrony/nts/nts.crt +ntsserverkey /etc/chrony/nts/nts.key +# Persist NTS server keys/cookies across restarts (best-effort on the container fs). +ntsdumpdir /var/lib/chrony + # Serve our own clock at a high stratum if upstream is unreachable, so a diskless # boot never blocks waiting for time. local stratum 10 diff --git a/serving/chrony/nts-cert.sh b/serving/chrony/nts-cert.sh new file mode 100755 index 0000000..9469a3b --- /dev/null +++ b/serving/chrony/nts-cert.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# nts-cert.sh — issue the NTS-KE server certificate for the fleet NTP server, +# signed by the fleet step-ca so booted hosts (secure_ntp=true) authenticate it +# against the SAME radius-ca.pem they already trust (closes F-03/P-18). Run once +# after the CA is up (serving/dot1x-bench bootstrap.sh), from serving/chrony/. +# Writes ./nts/{nts.crt,nts.key}, which docker-compose mounts at /etc/chrony/nts. +# Pairs with docs/secure-ntp-design.md. BENCH-VERIFY the `step` flags vs your version. +set -euo pipefail + +cd "$(dirname "$0")" +OUT="nts" +# step-ca lives in the dot1x-bench compose project; reach it there (same pattern +# as dot1x-bench/step-ca/bootstrap.sh issuing the RADIUS cert). +BENCH="../dot1x-bench" +[ -d "$BENCH" ] || { echo "ERROR: $BENCH not found (need the step-ca compose project)." >&2; exit 1; } +[ -f "$BENCH/.env" ] && . "$BENCH/.env" +CA_PORT=${CA_PORT:-8443} +CA_CONNECT=${CA_CONNECT:-localhost} +CA_URL="https://${CA_CONNECT}:${CA_PORT}" +PROVISIONER=${PROVISIONER:-enrol} +# The DNS SAN the booted host uses. The image derives it at runtime as +# . (default ntp.), so this MUST equal +# ntp.${BOOT_DOMAIN} and resolve via the fleet dnsmasq (NTP_HOST -> SERVER_IP). +NTP_SAN=${NTP_HOST:-ntp.fleet.lab} + +PKI="$BENCH/pki" +PW="$PKI/stepca/password" +[ -e "$PW" ] || { [ -e "$PKI/stepca/secrets/password" ] && PW="$PKI/stepca/secrets/password"; } +[ -r "$PW" ] || { echo "ERROR: CA password $PW missing — run dot1x-bench bootstrap.sh first." >&2; exit 1; } +CONTAINER_PW="/home/step/${PW#"$PKI/stepca/"}" + +incca() { ( cd "$BENCH" && docker compose exec -T step-ca "$@" ); } + +echo "==> issuing NTS-KE server cert (CN/SAN ${NTP_SAN}) from the fleet CA" +incca mkdir -p /export/nts +# Leaf with serverAuth (default) + the NTS server name as SAN. --not-after within +# the CA's max claim (bootstrap.sh raised it to 2160h). +incca step ca certificate "${NTP_SAN}" \ + /export/nts/nts.crt /export/nts/nts.key \ + --ca-url "$CA_URL" --root /export/root_ca.crt \ + --provisioner "${PROVISIONER}" --provisioner-password-file "$CONTAINER_PW" \ + --not-after 2160h --force + +# The CA wrote into $BENCH/pki/nts via the /export mount; copy to ./nts for the +# ntp container's bind mount. +mkdir -p "$OUT" +cp "$PKI/nts/nts.crt" "$OUT/nts.crt" +cp "$PKI/nts/nts.key" "$OUT/nts.key" +chmod 0644 "$OUT/nts.crt"; chmod 0640 "$OUT/nts.key" +echo "==> wrote $OUT/nts.crt + $OUT/nts.key" +echo " Ensure the image's ntp_server = ${NTP_SAN}, dnsmasq resolves ${NTP_SAN}," +echo " open tcp/4460 (NTS-KE) on the boot NIC, and restart the ntp service." diff --git a/serving/chrony/nts/.gitignore b/serving/chrony/nts/.gitignore new file mode 100644 index 0000000..61c36a5 --- /dev/null +++ b/serving/chrony/nts/.gitignore @@ -0,0 +1,4 @@ +# NTS-KE server cert/key (fleet-signed, per-environment) — issued by ../nts-cert.sh. +# Never commit key material or environment CA-signed certs (see CLAUDE.md invariant). +nts.crt +nts.key diff --git a/serving/defaults.env b/serving/defaults.env index 6fdcb6d..169d8d3 100644 --- a/serving/defaults.env +++ b/serving/defaults.env @@ -91,6 +91,11 @@ AUDIT_HOST=audit.fleet.lab # *.fleet.lab like the boot/nvme/audit names. Must match the agent server cert SAN # (server-pki.sh AGENT_SANS). MGMT_HOST=manager.fleet.lab +# NTS-authenticated NTP server name (secure_ntp). Baked into the image's +# chrony.conf (Packer var ntp_server) so every host syncs authenticated time here; +# resolved per-environment to this serving host. MUST match the NTS server cert SAN +# issued by serving/chrony/nts-cert.sh. +NTP_HOST=ntp.fleet.lab # --- NVMe/TCP target ------------------------------------------------------- # Per-version subsystem NQN base. The long-lived target serves each rootfs diff --git a/serving/docker-compose.yml b/serving/docker-compose.yml index 9e052ac..fc92182 100644 --- a/serving/docker-compose.yml +++ b/serving/docker-compose.yml @@ -108,6 +108,9 @@ services: # Fleet-manager AGENT plane (:9600) — baked into the image so enrolled hosts # dial the control plane by name; resolved here like the other fleet names. - --address=/${MGMT_HOST}/${SERVER_IP} + # NTS-authenticated NTP server name (secure_ntp): baked into the image's + # chrony.conf; must match the NTS server cert SAN. Resolved to this host. + - --address=/${NTP_HOST}/${SERVER_IP} # Kiosk is LOCAL-ONLY (loads boot.fleet.lab), so sinkhole Chromium's # call-home to 0.0.0.0 — telemetry can't leave the lab even if the browser # tries. Defense-in-depth behind the in-image Chromium policy/flags; in @@ -122,9 +125,14 @@ services: - --dhcp-range=${DHCP_RANGE_START},${DHCP_RANGE_END},${DHCP_LEASE} - --dhcp-option=option:router,${GATEWAY_IP} - --dhcp-option=option:dns-server,${SERVER_IP} - # NTP server (option 42): point diskless hosts at the lab 'ntp' service - # below so they sync time internally and never reach the public NTP pool. - # The image's NM dispatcher (10-chrony-dhcp-ntp) feeds this into chrony. + # Domain name (option 15): the image derives its NTS NTP server name at + # runtime as ntp. (secure_ntp — no baked FQDN), so hosts MUST + # receive the domain. NM exports it to the dispatcher as $DHCP4_DOMAIN_NAME. + # Explicit (not just --domain) so the option is reliably sent + matches the + # NTS cert SAN (ntp.${BOOT_DOMAIN}) and the dnsmasq --address for NTP_HOST. + - --dhcp-option=option:domain-name,${BOOT_DOMAIN} + # NTP server (option 42): used only in the LEGACY unauthenticated path + # (secure_ntp=false). With NTS the image ignores this and dials ntp.. - --dhcp-option=option:ntp-server,${SERVER_IP} # ---- Per-host names (creative starship hostnames, server-assigned) ---- # The workstation image is hostname-neutral; naming is done HERE. fleet-namer @@ -255,15 +263,20 @@ services: environment: HASS_URL: ${HASS_URL} HASS_TOKEN: ${HASS_TOKEN} - # --- NTP server (serves time to the boot LAN; handed out via DHCP opt 42) --- + # --- NTP server (serves time to the boot LAN; NTS-authenticated for secure_ntp) --- ntp: build: ./chrony - network_mode: host # serve NTP on udp/123 across the boot LAN + network_mode: host # serve NTP on udp/123 (+ NTS-KE tcp/4460) across the boot LAN cap_add: - SYS_TIME # discipline the (host-shared) clock it serves restart: unless-stopped volumes: - ./chrony/chrony.conf:/etc/chrony/chrony.conf:ro + # NTS-KE server cert/key (fleet-signed) so booted hosts CA-AUTHENTICATE the + # time source (secure_ntp; F-03/P-18). Issue with ./chrony/nts-cert.sh. + # If the dir is empty the server still answers plain NTP; NTS clients then + # fail the KE handshake — so run nts-cert.sh before shipping secure_ntp images. + - ./chrony/nts:/etc/chrony/nts:ro # NOTE: with host networking + SYS_TIME this chronyd disciplines the Docker # HOST clock (the clock isn't namespaced) — intended: the serving host is the # lab time source. Drop SYS_TIME (and add `chronyd -x`) if the host already @@ -271,11 +284,14 @@ services: # # FIREWALL (easy to miss — same trap as the audit collector's 514/tcp): host # networking means Docker does NOT open the port; the serving host's own - # firewall governs 123/udp. Without a rule, workstations get 192.168.50.10 as - # their NTP server (DHCP opt 42) but their packets are dropped — chrony logs - # "Added source … / unreachable" and never syncs. Open it on the boot-LAN NIC: + # firewall governs the NTP ports. Without a rule, workstations get 192.168.50.10 + # as their NTP server but their packets are dropped — chrony logs + # "Added source … / unreachable" and never syncs. Open BOTH on the boot-LAN NIC: + # udp/123 = NTP; tcp/4460 = NTS-KE (secure_ntp key establishment) # ufw: sudo ufw allow in on to any port 123 proto udp comment 'fleet NTP' - # firewalld: sudo firewall-cmd --permanent --add-service=ntp && sudo firewall-cmd --reload + # sudo ufw allow in on to any port 4460 proto tcp comment 'fleet NTS-KE' + # firewalld: sudo firewall-cmd --permanent --add-service=ntp \ + # && sudo firewall-cmd --permanent --add-port=4460/tcp && sudo firewall-cmd --reload # --- Remote audit / log collector (RECEIVE side) --------------------------- # rsyslog receiver: booted diskless hosts (whose RAM-only overlay root loses