Files
diskless-workstation/docs/stig-deviations.md
T
2026-07-11 17:32:05 +09:30

326 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# STIG deviation register — diskless fleet image
DISA STIG (SSG `xccdf_org.ssgproject.content_profile_stig` for RHEL 9) is applied
to the image via `oscap --remediate` with a tailoring file
([../files/stig/diskless-tailoring.xml](../files/stig/diskless-tailoring.xml)).
This register documents every **intentional** deviation — the rules unselected in
that tailoring, plus posture decisions — with the rationale and compensating
control. The per-build **generated** report
(`stig-deviations.md` / `stig-report.html`, produced by
[../scripts/stig-deviations.sh](../scripts/stig-deviations.sh) from the
post-remediation scan) cross-references this file: any non-`pass` rule **not**
listed here is flagged **REVIEW**.
Posture decisions (2026-06-18, lab phase):
- **FIPS: OFF** — revisit alongside UKI signing / Secure Boot.
- **Audit offload: LOCAL-only** — auditd runs and logs to the tmpfs overlay; no
remote collector yet.
- **CI: report-only** — `oscap eval` publishes ARF + HTML; never fails the build.
## Intentional deviations
| # | STIG/SSG area (rules) | Why it doesn't apply / can't be met | Compensating control |
|---|---|---|---|
| 1 | **Separate partitions + their mount options** for `/var`, `/var/log`, `/var/log/audit`, `/tmp`, `/home`, `/var/tmp`, `/dev/shm` (`partition_for_*`, `mount_option_*`) | Root is a single immutable **erofs** image on NVMe/TCP with a **tmpfs volatile overlay** — there is no local disk to partition, and these dirs are not separate mounts. The related `mount_option_*` rules will therefore show as findings; they are covered by this entry. | The entire root is **read-only** (erofs+dm-verity) and the writable layer is **tmpfs (RAM)**, which structurally bounds the tampering / space-exhaustion risks the partition+option rules target. `noexec` on `/tmp` is intentionally NOT forced — the browser kiosk uses `/tmp` for its profile/cache and can break under `noexec`. |
| 2 | **Bootloader / GRUB** (`grub2_*`: password, admin user, and the `*_argument` kernel-param rules) | Boot is a **UKI** (systemd-stub) over UEFI HTTP boot — there is **no GRUB**. | The STIG kernel params (`audit=1`, `audit_backlog_limit`, `slub_debug=P`, `page_poison=1`, `vsyscall=none`, `init_on_alloc/free`) are baked into the **signed UKI cmdline** ([../scripts/extract-artifacts.sh](../scripts/extract-artifacts.sh)); the cmdline is immutable once signed (Secure Boot covers it), which is stronger than an editable GRUB config. |
| 3 | **AIDE / file integrity** (`package_aide_installed`, `aide_build_database`, `aide_periodic_cron_checking`, `aide_scan_notification`, `aide_verify_*`) | The root is read-only and cryptographically verified; an on-disk AIDE db would also be non-deterministic and break the reproducible verity hash. | **dm-verity** verifies the whole root filesystem against a signed root hash on every read — continuous, hardware-backed integrity that supersedes AIDE's periodic scan. |
| 4 | **Firewall** (`service_firewalld_enabled`, `package_firewalld_installed`, `set_firewalld_default_zone`) | Currently `firewall --disabled`; auto-enabling firewalld/nftables risks the NVMe/TCP (`:4420`) and NetworkManager paths during bring-up. | Isolated boot LAN; egress controlled at the serving-stack / network firewall layer. **Future hardening** — re-enable with explicit allow rules once the secure path is verified. |
| 5 | **FIPS** (`enable_fips_mode`, `grub2_enable_fips_mode`, `package_dracut-fips_installed`) | Decision: off for the lab phase (interacts with the still-unsigned UKI / Secure Boot story). | Revisit with signing. No weakened crypto is configured in the meantime. |
| 6 | **Remote audit offload** (`auditd_audispd_configure_remote_server`, `..._encrypt_sent_records`, `..._network_failure_action`, `rsyslog_remote_loghost`) **+ rsyslog TLS offload** (`rsyslog_encrypt_offload_defaultnetstreamdriver`, `..._actionsendstreamdrivermode`, `..._actionsendstreamdriverauthmode`) | Offload is done via **rsyslog over TCP** (simpler, container-friendly receiver); the `audisp-remote` native-protocol rules stay waived. The **TLS-offload rules are waived** because the isolated lab boot LAN is plaintext (mirrors HTTP boot / no MACsec) and applying them forces `gtls` + x509 peer auth + a CA file, which **breaks** the plaintext `2f` forward (no CA, plaintext `:514`). | **Wired both ends, plaintext.** Sender (image, provisioner 2f, `audit_host`): audisp-syslog → LOCAL6 + rsyslog `omfwd` (disk-assisted queue) → host-generic `audit.fleet.lab`. Receiver: `serving/audit-collector/` (rsyslog TCP/514, per-source-host files). No per-host state; isolated LAN. **Production (tracked in docs/TODO.md #2):** issue the collector a TLS cert, bundle the CA in the image, switch `2f` + the collector to `gtls`/6514, then **re-select** the three `rsyslog_encrypt_offload_*` rules (drop the waivers) so they pass. |
| 7 | **Smart card login** (`require_smart_card_login`, `sssd_enable_smartcard`, `install_smartcard_packages`) | Diskless hosts have no smartcard reader; admin access is SSH key-based. Enforcing smartcard login would lock the host out. | Admin auth is an SSH **key-only** account; root is locked; password auth disabled. |
| 8 | **NOPASSWD sudo** (`sudo_remove_nopasswd`) | The baked admin is a **passwordless, key-only** account, so `sudo` cannot prompt for a password — NOPASSWD is required for it to function at all. | Single fleet-generic admin, key-only, `wheel`; no interactive password exists to protect. Tracked for the eventual per-host/rotatable access model (SSH CA / config-agent). |
| 9 | **GUI / autologin / GDM** (gnome screensaver, GDM banner, automatic-login rules) | No GNOME/GDM is installed; the kiosk is an **intentional** autologin single-app (weston + Chromium) session on kiosk builds. | Kiosk user is unprivileged and passwordless; the session runs a single locked-down browser (no shell, no nav). Applies to kiosk builds only; non-kiosk builds don't install the GUI stack. |
| 10 | **Password aging / faillock on the key-only APPLIANCE ACCOUNTS** (`accounts_*_age_*`, `account_disable_post_pw_expiration`, `pam_faillock` account phase) — applied to the *system* but **exempted for the `fleetadmin` and `kiosk` users** | These two accounts are **passwordless / key-only**. The hardened PAM *account* phase (faillock + password aging/inactivity) treats a passwordless account as expired/lockable, which **locked the fleet out**: the kiosk autologin session never opened (getty kept tty1) and sshd denied the valid key with `Permission denied (publickey)` (pubkey auth succeeds, then `pam_acct_mgmt` fails). Fixed 2026-06-18. | The exemption is **scoped to these two appliance accounts only** (post-STIG `chage -M -1 -m 0 -I -1 -E -1` + `faillock --reset`; kiosk uses a dedicated `/etc/pam.d/kiosk`; sshd gets an `account sufficient pam_succeed_if.so user = fleetadmin` bypass as its first account rule). Every other account keeps the full STIG password policy + faillock. These accounts have **no password to age** and (key-only, `PasswordAuthentication no`) **no password-auth path for faillock to protect**. Folds into the per-host/rotatable access model (SSH CA / config-agent). |
| 11 | **Login banner content** (`banner_etc_issue`, `banner_etc_issue_net`, `banner_etc_motd`) | STIG requires the verbatim DoD/USG "you are accessing a U.S. Government Information System" text. This is a fictional starship fleet, not a USG system, so `/etc/issue{,.net}` + `/etc/motd` carry the fleet (ISV AURELIAN) notice instead (provisioner step 2e, `files/banner/fleet-issue`). The content rules therefore fail by design. | The banner **keeps its security function**: it still states authorized-crew-only, that the terminal is monitored/logged, and that unauthorized access is prohibited — only the wording is themed. `sshd_enable_warning_banner` (Banner is shown) still passes. |
## 12. Report triage 2026-06-19 (scan `app-9f7d8d1`)
Triage of the 21 REVIEW rows from the post-fix hardened scan (398 pass / 12 fail
/ 9 notchecked — a valid scan). All but one are dispositioned below; the rule IDs
are unselected in `diskless-tailoring.xml` (or made to pass / N/A) so they no
longer surface as REVIEW. **Remaining open: `selinux_context_elevation_for_sudo`**
(see end).
**Waived — covered by an existing register entry (Bucket A):**
- `encrypt_partitions`, `auditd_audispd_configure_sufficiently_large_partition` —
single erofs+tmpfs root, no partitions (#1).
- `grub2_password` — UKI, no GRUB (#2).
- `sysctl_crypto_fips_enabled`, `harden_sshd_macs_openssh_conf_crypto_policy`,
`harden_sshd_macs_opensshserver_conf_crypto_policy` — FIPS off (#5).
- `configured_firewalld_default_deny` — firewall off for the lab boot LAN (#4).
- `sssd_enable_certmap`, `sssd_has_trust_anchor` — no SSSD/PKI/smartcard; local
key-only host, no domain join (#7).
**Waived — architecturally absent, N/A (Bucket B):** `account_temp_expire_date`
(no temporary accounts), `accounts_user_home_paths_only` (appliance accounts,
fixed home layout), `libreswan_approved_tunnels` (no IPsec/libreswan),
`security_patches_up_to_date` (rebuild-from-current model — a scanner can't assess
patch currency against a future rebuild), `ssh_keys_passphrase_protected` (no
private keys stored on the host; host keys regenerate per boot).
**Waived — decisions (Bucket C):**
- `installed_OS_is_vendor_supported` — Rocky **is** vendor-supported; the rule
fails only because the RHEL datastream's CPE checks for an RHEL subscription.
- `network_configure_name_resolution` — single DHCP-provided resolver by design
on the boot LAN; the multi-DNS requirement is N/A to the netboot model.
- `sysctl_user_max_user_namespaces_no_remediation` — **KEEP user namespaces
enabled**: the Chromium kiosk sandbox requires them. Disabling userns would
break the kiosk.
- `accounts_authorized_local_users` — the authorized-users check doesn't know the
key-only appliance accounts (`fleetadmin`, `kiosk`); they are authorized (see
#10). *Production note: prefer setting the SSG authorized-users regex variable
to assert these two explicitly rather than waiving the whole check.*
- `package_postfix_installed` — **postfix removed** (provisioner 2c-STIG): the
appliance sends no mail and ships audit off-box via rsyslog (2f), so a local
MTA is dead attack surface. This also makes `postfix_prevent_unrestricted_relay`
N/A. Audit-failure notification relies on the remote offload, not email
(`auditd ... action_mail_acct` stays configured but is not the alerting path).
- `auditd_data_retention_space_left_action` — **consequence of removing postfix.**
STIG sets `space_left_action = email`, but with no MTA auditd logs *"Email
option is specified but /usr/lib/sendmail doesn't seem executable"* at every
boot and can't send. 2c-STIG repoints **`space_left_action = SYSLOG`** so the
75%-full alert rides the remote audit offload (2f) instead; this rule wants
`email`, so it's waived. `admin_space_left_action` stays `single` (the critical
action, unaffected) and `action_mail_acct = root` stays set.
**Remediated to pass (not waived):**
- `configure_usbguard_auditbackend` + `usbguard_allow_hid` / `usbguard_allow_hub`
/ `usbguard_allow_hid_and_hub` — **decision: ENABLE USBGuard with enforcement.**
usbguard was already installed + enabled, but its policy is generated on the
build VM (no real USB), so under the default **block** target a physical console
keyboard/mouse would be blocked. The provisioner now sets
`AuditBackend=LinuxAudit` and writes a **hardware-independent HID + hub
allowlist** (`allow with-interface match-all { 03:*:* }`, `{ 09:00:* }`, and the
composite `{ 03:*:* 09:00:* }`) so console input survives while mass storage and
other device classes are blocked. **⚠ BENCH-VERIFY physical console input on
real hardware** — if a platform's input device doesn't match the HID/hub
classes, fall back to a per-platform allow rule or re-waive
`configure_usbguard_auditbackend`.
**Remediated with confinement (decision 2026-06-20) — `selinux_context_elevation_for_sudo`:**
SSG ships no automated fix; the STIG manual remediation
(`... TYPE=sysadm_t ROLE=sysadm_r ...`) requires the admin to reach the
`sysadm_r` role, which `unconfined_u` cannot. So the provisioner (2c-STIG) now
(1) rewrites the admin sudoers rule to `<admin> ALL=(ALL) TYPE=sysadm_t
ROLE=sysadm_r NOPASSWD:ALL` (keeps NOPASSWD — the admin is key-only, #8 — and is
what the OVAL check matches), and (2) maps the admin to the confined **`staff_u`**
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, 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).
## 13. Ctrl+Alt+Del console reboot hotkey kept enabled (decision 2026-06-20)
The RHEL 9 STIG disables the console reboot hotkey via two rules, both **waived**
in the tailoring:
- `disable_ctrlaltdel_reboot` — masks `ctrl-alt-del.target` (single Ctrl+Alt+Del).
- `disable_ctrlaltdel_burstaction` — sets `CtrlAltDelBurstAction=none` in
`/etc/systemd/system.conf` (the rapid 7× burst reboot).
**Why waived:** the STIG intent is to stop an **unauthenticated console user from
causing an unplanned reboot/outage**. On this fleet that risk is structurally
absent — the hosts are **diskless and stateless** (immutable erofs+dm-verity root,
tmpfs volatile overlay), so a console reboot loses nothing and persists nothing,
and a **field technician needs to recover a wedged kiosk from the keyboard when
SSH is unavailable**. The two rules are unselected so `--remediate` leaves the
systemd default (the hotkey reboots).
**Compensating control / scope:** the hotkey only triggers a **clean stateless
reboot** — it cannot exfiltrate data, alter the read-only root, or bypass auth to
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;
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)
Not a STIG-rule waiver — a **minimal local `allow` module** shipped to keep a
legitimate function working under enforcing SELinux. Source:
`files/stig/chrony-dhcp-dispatcher.te`; built + `semodule -i`'d in the 2c-STIG
provisioner.
**The one rule:** `allow chronyd_t NetworkManager_dispatcher_custom_t:unix_dgram_socket sendto;`
**Why:** the NM dispatcher script `10-chrony-dhcp-ntp` loads DHCP option-42 NTP
servers by running `chronyc reload sources`. That `chronyc` runs in the confined
custom-dispatcher domain (`NetworkManager_dispatcher_custom_t`), so its reply
socket carries that label; the shipped policy has no rule for `chronyd_t` to
`sendto` it. Under enforcing, chronyd processes the reload but its **ack is
denied** — `chronyc` reports failure and the AVC floods the SIEM (seen
2026-07-03). An **`allow`** (not `dontaudit`) is used deliberately: it both stops
the noise and lets the reload ack succeed, so `chronyc` returns cleanly.
**Scope:** one direction, one class/perm, two well-known types. It grants no
network/file access and nothing beyond the chronyd↔dispatcher reply the DHCP-NTP
path needs. **BENCH-VERIFY:** (a) on an *enforcing* boot the AVC is gone and
`chronyc reload sources` from the dispatcher returns 0 (confirm a DHCP-handed NTP
server appears in `chronyc sources`); (b) no follow-on AVC (extend the `.te` if
one shows); (c) **reproducibility** — `semodule -i` rebuilds the policy store, so
re-check the verity hash is stable across two builds (same concern as the
`semanage` step, #12/sudo elevation). NB: only shipped when `enable_stig=true`
(the module install lives in the STIG-gated provisioner); a non-STIG *enforcing*
build would still see the AVC.
## 15. Local SELinux policy module `fapolicyd_tmpfs` (added 2026-07-03)
Not a STIG-rule waiver — a **minimal local `allow` module** shipped to close an
**exec-guard gap** under enforcing SELinux. Source: `files/stig/fapolicyd-tmpfs.te`;
built + `semodule -i`'d in the 2c-STIG provisioner (right after #14).
**The one rule:** `allow fapolicyd_t tmpfs_t:filesystem getattr;`
**Why:** fapolicyd is the app-allowlisting **exec guard for the writable RAM
overlay** — dm-verity covers only the read-only erofs lower, so a binary dropped
into the tmpfs overlay upper is exactly what fapolicyd must catch (this is why we
KEEP fapolicyd; see "Things that are NOT deviations"). fapolicyd `statfs`'s mounts
to decide which to watch, and **tmpfs is in its default `watch_fs`**. This image's
root is `systemd.volatile=overlay`, so `statfs("/")` returns the overlay **upper**
— a tmpfs (audit: `dev=tmpfs ino=1 name="/"`, `fapolicyd_t → tmpfs_t:filesystem
getattr`, `permissive=0`, seen 2026-07-03). The shipped policy has no such rule
(no ordinary host has its root on tmpfs), so under enforcing the `getattr` is
**denied**, fapolicyd cannot fully mark the tmpfs mounts (`/`, `/tmp`, `/dev/shm`),
and its guard over the writable overlay is **incomplete** — a real gap, not just
SIEM noise. An **`allow`** (not `dontaudit`): we *want* fapolicyd to see the tmpfs.
**Scope:** one perm (`getattr`) on one class (`filesystem`), two well-known types.
Grants no file read/write, only the `statfs` fapolicyd needs to classify + watch
the mount. **BENCH-VERIFY:** (a) the AVC is gone on an enforcing boot; (b) fapolicyd
**blocks a non-RPM executable dropped in `/tmp`** (proves the guard now covers the
overlay tmpfs) while the trusted kiosk/appliance binaries still run; (c)
**reproducibility** — `semodule -i` rebuilds the policy store, so re-check verity
hash stability across two builds (same concern as #12/#14). NB: only shipped when
`enable_stig=true`; a non-STIG enforcing build would still see the AVC and the gap.
**UPDATE 2026-07-03 — the getattr allow alone did NOT close the exec gap; the real
enabler is `watch_fs`.** Bench test on `comet-beacon-54`: with the `.te` installed
(AVC gone, `fapolicyd active`, `permissive=0`), a **novel** non-RPM executable in
`/tmp` (hash NOT in the trust db) still **ran** (`exit=0`). Root cause: the diskless
root is **overlayfs** (`stat -f /` → `overlayfs`, `/tmp` too), but the shipped
`watch_fs = ext2,ext3,ext4,tmpfs,xfs,vfat,iso9660,btrfs` has **no `overlay`**.
fapolicyd marks mounts by fs type, so it never marks the overlay = it intercepts
**no exec on `/`** — the whole root, `/usr` and `/tmp` alike. So the exec guard was
effectively **OFF at runtime** on every diskless host. Fix: the 2c-STIG fapolicyd
step now appends **`overlay` to `watch_fs`** in `fapolicyd.conf` (idempotent) so
fapolicyd watches the overlay root; trust is path-based (`integrity=none`) so overlay
paths resolve against the rpmdb trust normally. The `fapolicyd_tmpfs` `.te` above is
still needed — it supports fapolicyd statfs'ing the overlay's tmpfs upper. **This
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). 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`,
`watch_fs = ...,overlay` (the fix took), `stat -f / /tmp` → both `overlayfs` (so the
test hits the overlay rule, not the pre-existing tmpfs one). A novel-hash binary
(`cp /bin/true /tmp/n; printf '\0' >>/tmp/n`) was **denied**: `bash: /tmp/n: Operation
not permitted`, `exit=126`, and the audit record confirms fapolicyd did it —
`type=FANOTIFY ... resp=2` (FAN_DENY) `obj_trust=0` (target not in trust db). The
**deny sweep was empty** (`journalctl -t fapolicyd -b | grep dec=deny` → nothing),
i.e. **zero false denials**: kiosk-session/weston/chromium + the NM dispatcher scripts
all ran trusted; admin SSH + `sudo` worked under `staff_u:staff_r:staff_t`. So the
exec guard went from **effectively OFF** (pre-fix, overlay unwatched) to **enforcing
on the whole overlay root** with nothing legit broken. My NM-dispatcher false-denial
worry did not materialise.
**Scope caveat — this closes the UNTRUSTED-PATH half, not the MODIFIED-TRUSTED-PATH
half.** With `integrity = none`, trust is by **path**, so watching the overlay blocks
a *new* file at an *untrusted* path (`/tmp/n`) — proven above. It does **not** catch a
*trusted* path whose **content** was swapped: writing to e.g. `/usr/bin/bash` triggers
an overlayfs **copy-up** into the tmpfs upper — same path, new inode, **outside
dm-verity's coverage** (verity only protects the erofs lower, which is now shadowed) —
and the path-based rule still trusts it. Closing that residual gap needs either
`integrity = sha256` (fapolicyd re-hashes at exec, content ≠ trust-db hash → deny; the
rpmdb backend already carries SHA-256 digests, so it's a one-line `fapolicyd.conf`
change — cost is per-exec userspace hashing, partly redundant with verity on the
un-tampered lower) **or** an **un-shadowable `/usr`** so copy-up can't happen at all
(read-only mount → write fails EROFS, no tmpfs copy is ever created; path-trust becomes
sound again). The latter is tracked as **TODO #15** ("scope root overlay to per-subtree
tmpfs — un-shadowable `/usr`") and is the preferred fix since it removes the attack
surface rather than detecting abuse of it. Requires a write primitive to a root-owned
path either way (already-privileged context), which is fapolicyd's defense-in-depth
threat model — not an unprivileged bypass.
## 16. Local SELinux policy module `fleet_agent_chronylog` (added 2026-07-10)
Not a STIG-rule waiver — a **minimal local read-only `allow` module** so the
`fleet-agent` telemetry client can read chrony's tracking log under enforcing
SELinux. Source: `files/stig/fleet-agent-chronylog.te`; built + `semodule -i`'d in
the 2c-STIG provisioner (after #15).
**The rules:** `allow unconfined_service_t chronyd_var_log_t:file { open read
getattr };` (+ `dir { search getattr open }` to reach the file).
**Why:** `fleet-agent` reports clock health (source/stratum/offset) by reading
`/var/log/chrony/tracking.log` — deliberately a FILE read rather than a query to
chronyd's command interface, which the hardened agent can't use (owner-write-only
unix socket; STIG's `cmdport 0` closes the UDP port). But chrony's log dir + files
are labeled `chronyd_var_log_t`, which the shipped policy confines to `chronyd_t` +
log-admin roles. So although the file is mode `0644`, under enforcing the agent
(`unconfined_service_t`) is **denied** by type enforcement — DAC allows, TE does
not. Verified 2026-07-10 on `comet-beacon-54`: with SELinux **permissive** a
non-root read of `tracking.log` succeeds; flip to **enforcing** and even a
STIG-confined admin (`staff_t`) gets `Permission denied` (`ls -Z` on the file is
itself blocked) — the signature of a type-enforcement block, not a DAC one.
**Scope:** read-only (`open`/`read`/`getattr`) on one log type + dir search to
reach it. No write, no other chrony access, and — unlike the earlier abandoned
approach — **no domain transition** (the agent reads a file; it does not exec
`chronyc`). This replaces the never-shipped `fleet_agent_chronyc` nnp-transition
module (that whole command-socket path was dropped for reading the log instead).
**This is the SELinux half of a two-layer requirement.** `/var/log/chrony` is
mode `0750 chrony:chrony` (others have no access), so DAC also blocks the agent's
`DynamicUser` from traversing the dir — a denial that produces **no AVC** (it's
DAC, not TE). The DAC half is therefore the **`chrony` supplementary group** on
`fleet-agent.service` (traverse the `0750` dir; the log file itself is `0644`).
Both this module and that group are needed; verified 2026-07-10 — with the group
but no module the read is TE-denied, and with the module but no group it's
DAC-denied (silently, no AVC — which is what made this the hardest layer to spot).
**BENCH-VERIFY:** (a) the manager's Monitor chrony row populates on an enforcing
boot; (b) no other chrony-log AVC (extend here if one appears); (c)
**reproducibility** — `semodule -i` rebuilds the policy store, re-check verity hash
stability (same concern as #14/#15). Only shipped when `enable_stig=true`.
## Things that are NOT deviations (kept / enforced)
- **SELinux enforcing** (`selinux --enforcing`; labels preserved into the erofs).
- **fapolicyd** kept **enabled** (NOT waived) — it's the real exec-allowlist guard
for the writable RAM overlay, which dm-verity does **not** cover. The only
adjustment: the few non-RPM executables we ship are added to fapolicyd's trust DB
at build (provisioner 2c-STIG-postfix) so they run; everything else stays
RPM-trust-only. Trusted custom binaries: `/usr/local/bin/kiosk-session`,
`/usr/local/bin/fleet-sb-enroll`, `/usr/local/bin/fleet-hostapi` (the loopback
host-info agent, built from `files/hostapi`). Each is built into the immutable,
dm-verity-protected root, so trusting it doesn't weaken the overlay exec guard.
(fapolicyd blocking `kiosk-session` was the original kiosk-won't-start cause.)
- **auditd** enabled; STIG audit rules applied; logs locally.
- **USBGuard** enabled + enforcing (`block` target) with a HID/hub allowlist and
`AuditBackend=LinuxAudit` (see #12) — blocks USB mass storage / unknown device
classes at the console while keeping keyboard/mouse working.
- **sshd hardening**: root login off, password & keyboard-interactive auth off,
key-only (drop-in `50-fleet-hardening.conf`); STIG ciphers/MACs/KEX applied.
- **No per-host state**: SSH host keys stripped at build, regenerated at first
boot into the overlay; `machine-id` empty.
- Password-policy / faillock rules remain **selected and applied system-wide**
(kept honest in the report); they are exempted only for the two key-only
appliance accounts — see deviation #10.
## How to read the generated report
`scripts/stig-deviations.sh stig-arf.xml` emits a table of every rule whose
result is not `pass`. Each row is tagged:
- **intentional** — matches an unselected rule / area in this register.
- **REVIEW** — a real finding to triage (fix it, or add a justified entry here).