diff --git a/docs/TODO.md b/docs/TODO.md index eef1d93..2d302e8 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1247,3 +1247,81 @@ desired-state pull (version pin, reboot/drain, kiosk URL) wired to the deploymen manifest + SPDK flow. Build-time decisions (telemetry encoding, metrics/desired- state store, per-host vs cohort, osquery/FleetDM eval) listed in the design note. Depends on the step-ca fleet CA (#8) for the mTLS identity. + +## [ ] 15. Scope the root overlay → per-subtree tmpfs/overlay (un-shadowable `/usr` + STIG mount options) + +**DESIGN CAPTURED 2026-07-02.** Today the whole root is one `systemd.volatile=overlay` +tmpfs over `/` (see #5, invariant on the volatile overlay). Replace it with a +**read-only verity root + a handful of scoped writable mounts**, for two payoffs +that land from the same change: + +1. **Security — un-shadowable `/usr`.** dm-verity seals the backing store, but the + whole-root overlay lets any root process write a tmpfs *copy* over a system + binary (e.g. `/usr/sbin/fleet-enrol`) that then executes in preference to the + sealed one until reboot. fapolicyd is the current compensating control (CLAUDE.md: + "the writable-overlay exec guard dm-verity can't provide"). Keeping `/usr` (and + `/`, `/opt`, `/boot`) strictly **read-only** makes the executable set both + verity-sealed *and* un-shadowable — defence in depth, shrinks fapolicyd reliance. + Also: writes outside the sanctioned set fail loudly (`EROFS`) instead of silently + landing in RAM and vanishing at reboot — enforces invariant #1 (no per-host state) + architecturally. + +2. **STIG — converts the partition/mount family from `notapplicable`/waived to real + `pass`.** Quantified against the scan in `reports/stig-arf.xml` (NOT estimates): + the `mount_option_*` rules currently score **`notapplicable`** (no separate mount + to check — the "false-clean via notapplicable" pattern #2 warns about), and + `partition_for_*` are **`notselected`** (waived in `diskless-tailoring.xml` + lines 61-66). Scoped tmpfs mounts with `noexec,nosuid,nodev` + re-selecting the + partition rules convert **~21 rules to genuine `pass`**: + - 15 `mount_option_*` N/A→pass: `/tmp` (nodev,nosuid[,noexec*]), `/var/tmp` + (all 3), `/var/log` (all 3), `/var/log/audit` (all 3), `/var` (nodev), `/home` + (nodev,nosuid). + - 6 `partition_for_*` re-selects→pass: tmp, var, var_log, var_log_audit, var_tmp, + home (+1 easy: `partition_for_dev_shm` — already a separate tmpfs). + - Retires **deviation-register entry #1** (the narrative hand-wave over the whole + partition/mount family) — one fewer waiver to defend at ATO. + - `/dev/shm`'s three option rules **already pass** — no work there. + +**Writable-set + flag matrix (the design):** +- `/` `/usr` `/opt` `/boot` → **read-only** (verity as-is, no overlay). `/usr` keeps + suid for legit setuid bins (sudo/su/passwd). +- `/etc` → overlay (lower=verity `/etc`, upper=tmpfs) — needs defaults + writability + (machine-id, regenerated ssh host keys, NM connections, resolv.conf, chrony). +- `/var` → overlay (NOT bare tmpfs — bare tmpfs hides `/var/lib/rpm`, selinux store); + `nodev`. Nest the noexec leaf mounts on top (order: `/var` first, then children). +- `/var/tmp` `/var/log` `/var/log/audit` → tmpfs `noexec,nosuid,nodev`. +- `/tmp` → tmpfs `nosuid,nodev` (+`noexec` only after the kiosk repoint, below). +- `/home` `/root` `/srv` → tmpfs `nodev,nosuid`. +- Each tmpfs needs the **correct SELinux context** (`tmp_t`, `var_log_t`, + `auditd_log_t`, …) via `context=` or restorecon — this is the login-deny hazard + class (#5); a mislabeled tree denies logins / stops auditd. + +**Prerequisites / caveats that gate the count:** +- `mount_option_tmp_noexec` (1 of the 21) is **conditional**: register entry #1 notes + the Chromium kiosk uses `/tmp` for profile/cache and breaks under `noexec`. Must + **repoint the kiosk scratch** (Chromium `--user-data-dir`/`--disk-cache-dir`) to a + dedicated exec-capable tmpfs (e.g. `/run/kiosk`) first → ties to #13/#4. So ~20 + land immediately, +1 after the repoint. +- **fstab-vs-runtime OVAL — mostly de-risked:** the rules return `notapplicable` + (evaluating the live mountpoint) not `fail` (missing fstab line), evidence they read + the runtime mount table — so systemd mount units / initramfs mounts should score + without fstab entries. Confirm per-rule on the bench; add fstab lines if any rule is + fstab-based (compatible with the empty-fstab invariant — root still comes from + netroot+verity). +- **Does NOT fix red findings** (this family is ~0 fail today) — the value is + evidenced compliance + one fewer deviation + kernel-enforced noexec, not clearing + fails. Framed honestly so it isn't oversold. +- **`partition_for_usr` stays waived** — `/usr` should be verity-sealed, not a + separate writable partition (a *stronger* control than the rule asks). Same for + `encrypt_partitions`, `auditd_..._large_partition`, boot/boot_efi (genuinely N/A). + +**Mechanism:** `systemd.volatile=` has no per-dir knob (it's all-or-var-only), so this +means dropping the global volatile overlay for explicit early mount units (ordered +before `local-fs.target`) or initramfs mounts. Reproducibility unaffected (runtime +mounts, not image content → no verity-hash drift). + +**Load-bearing / bench-gated:** touches the boot path (#5 territory) — full boot-chain +bench (`serving/dot1x-bench/`) + careful SELinux-label verification + a reconciliation +note in `design-v2-errata.md`. **Do AFTER TPM enrolment (#11) is bench-verified.** +Tailoring edits: re-select the 6 `partition_for_*` (+dev_shm), delete/gut register +entry #1, re-run `scripts/stig-deviations.sh` to confirm the new passes.