# 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 ` 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, `enable_stig=false` is the escape hatch and this reverts to a justified deviation; (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; `debug_shell` is lab-only and never shipped). If a future deployment wants the hotkey disabled (e.g. a physically exposed terminal), re-select both rule IDs. ## 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).