docs/TODO: add #15 — scope root overlay to per-subtree tmpfs (un-shadowable /usr + STIG mount options)
Captures the design + quantified STIG payoff (~21 mount/partition rules notapplicable/waived -> pass, per reports/stig-arf.xml), the writable-set + noexec/nosuid/nodev flag matrix, the kiosk-/tmp-noexec prerequisite, the fstab-vs-runtime OVAL note, and that it's bench-gated after TPM enrolment (#11).
This commit is contained in:
@@ -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-
|
manifest + SPDK flow. Build-time decisions (telemetry encoding, metrics/desired-
|
||||||
state store, per-host vs cohort, osquery/FleetDM eval) listed in the design note.
|
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.
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user