Its OVAL requires a literal ^server line in the baked chrony.conf, which can never exist under secure_ntp: the fail-closed re-strip removes every static server and the NTS source is derived at runtime into /run/chrony-dhcp. Surfaced as the single REVIEW row on scan poam-hardening-4e9ab45 (the first full scan of the post-secure-ntp merge).
31 KiB
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).
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 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; offload updated 2026-07-12):
- FIPS: OFF — revisit alongside UKI signing / Secure Boot.
- Audit offload: REMOTE over syslog-TLS — audisp-syslog + rsyslog
omfwd(gtls :6514, fleet-CA x509/name) toserving/audit-collector(see row 6; was LOCAL-only until 2026-06-19, plaintext until 2026-07-12). - CI: report-only —
oscap evalpublishes 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); 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) |
Offload is done via rsyslog over TLS rather than the audisp-remote native protocol (simpler, container-friendly receiver), so the audisp-remote rules + rsyslog_remote_loghost (whose remediation would inject a bogus loghost) stay waived — the intent of all four is met by the rsyslog path. The three rsyslog_encrypt_offload_* rules are NO LONGER waived (re-selected 2026-07-12): the offload is encrypted end-to-end and all three pass in the scan of record (build poam-hardening-5bc6d27; rsyslog+rsyslog-gnutls installed before the scan; remediation writes the gtls/x509 globals). Bench-verified same day incl. evil-twin rejection + lossless spool drain (TODO #8). |
Wired both ends, TLS. Sender (image, provisioner 2f, audit_host): audisp-syslog → LOCAL6 + rsyslog omfwd over gtls :6514, server verified x509/name against the baked fleet CA (/etc/dot1x/radius-ca.pem, var.fleet_ca), disk-assisted queue → host-generic audit.fleet.lab. Receiver: serving/audit-collector/ (gtls listener :6514, server cert from the fleet step-ca via issue-cert.sh; legacy plaintext :514 kept only for pre-TLS images during migration — close it once the fleet is on TLS images). No client certs: hosts are fleet-generic (invariant #1 — a baked fleet-wide client key would be shared secret material), and client mTLS with the per-host TPM identity was considered + deferred 2026-07-12 (redundant with 802.1X/MACsec on the secure path, which authenticates the same TPM credential at L2; rsyslog can't use a non-exportable TPM key without a delegated-cert or PKCS#11-tunnel contraption; hard-failing the forensic safety net on client-auth errors darkens exactly the hosts whose logs matter most — full rationale + revisit trigger in serving/audit-collector/README.md). Sender access constrained by the isolated boot LAN + serving-host firewall. No per-host state. |
| 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 makespostfix_prevent_unrestricted_relayN/A. Audit-failure notification relies on the remote offload, not email (auditd ... action_mail_acctstays configured but is not the alerting path).auditd_data_retention_space_left_action— consequence of removing postfix. STIG setsspace_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 repointsspace_left_action = SYSLOGso the 75%-full alert rides the remote audit offload (2f) instead; this rule wantsemail, so it's waived.admin_space_left_actionstayssingle(the critical action, unaffected) andaction_mail_acct = rootstays 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 setsAuditBackend=LinuxAuditand 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-waiveconfigure_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 drop-in to %wheel ALL=(ALL) TYPE=sysadm_t ROLE=sysadm_r NOPASSWD:ALL (keeps NOPASSWD — the admin is key-only, #8), 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. Scan-vs-runtime finding (2026-07-12, scan poam-hardening-5bc6d27):
the first cut wrote a per-USER rule (<admin> ALL=...), which elevates fine at
runtime (bench host comet-beacon-54: sudo id →
staff_u:sysadm_r:sysadm_t) but the rule still scanned fail — the SSG OVAL
patterns (^\s*%\w+.*TYPE=(\w+) / ...ROLE=(\w+)) only match %group
rules. Fixed by writing the rule for %wheel (the admin is a wheel member);
next scan should pass. Bench status: (a) SSH login + sudo under staff_u
confinement VERIFIED 2026-07-12 (re-check after the %wheel change — behaviour
should be identical, the rule targets the same account via its group); (b)
reproducibility of the verity hash across two builds (semanage rebuilds the
policy store) — still open.
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— masksctrl-alt-del.target(single Ctrl+Alt+Del).disable_ctrlaltdel_burstaction— setsCtrlAltDelBurstAction=nonein/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.
17. STIG NTP-server rules unselected under secure_ntp (added 2026-07-11; +chronyd_server_directive 2026-07-12)
Rules waived: chronyd_specify_remote_server, chronyd_or_ntpd_set_maxpoll,
chronyd_server_directive (unselected in diskless-tailoring.xml; plus a
belt-and-suspenders re-strip in 2c-STIG-postfix).
Addendum 2026-07-12 (scan poam-hardening-4e9ab45 — the first full scan of the
post-secure-ntp merge): chronyd_server_directive is the check-only companion
of the two rules above — its OVAL requires a literal ^server line in the baked
/etc/chrony.conf (or /etc/chrony.d/*.conf). Under secure_ntp that can never
exist: the re-strip removes every static server (each would be an unauthenticated
fallback defeating NTS fail-closure), and the real NTS source is derived from the
DHCP domain at runtime into /run/chrony-dhcp (a path the OVAL doesn't inspect).
Same rationale as below applies; added to the waiver set.
Why: these rules remediate by writing a static, unauthenticated
server <pool> maxpoll 16 into /etc/chrony.conf. The fleet instead configures
chrony's time source dynamically at runtime — a fleet-CA-authenticated NTS
server derived from the DHCP domain (secure_ntp=true, RFC 8915) or DHCP option-42
(false) — via the NM dispatcher writing sourcedir /run/chrony-dhcp, with
ntstrustedcerts/nocerttimecheck baked. The STIG intent (time is synchronised,
and under NTS authenticated) is fully met; the rules only fail because they
look for a literal baked server line the fleet deliberately doesn't have.
The security reason this is not just cosmetic (F-03/P-18): the ordering is the
trap — 2c-STIG runs after the secure_ntp chrony step, so an un-waived
chronyd_specify_remote_server re-injects a plain server 0.us.pool.ntp.mil maxpoll 16 that the earlier step already commented out. That plain server is a
silent unauthenticated fallback: with it present, chrony will happily sync
non-NTS time, so a DHCP/DNS/NTP spoofer could shift the clock — defeating the whole
point of NTS. Leaving it in means the host is not fail-closed. Observed on
comet-beacon-54 2026-07-11: NTS working (chronyc authdata cookies > 0) and the
STIG 0.us.pool.ntp.mil line still in chrony.conf and syncing in parallel.
Fix: unselect the two rules (so remediation injects nothing) and, when
secure_ntp=true, re-comment any server/pool in 2c-STIG-postfix (in case a
future rule adds one) so only the NTS sourcedir + ntstrustedcerts remain.
Under secure_ntp=false the re-strip does not run (the DHCP option-42 dispatcher
path needs the handed servers).
BENCH-VERIFY: on a clean secure_ntp build, grep -E '^(server|pool)' /etc/chrony.conf returns nothing (only sourcedir/ntstrustedcerts), and
chronyc -N authdata shows the NTS source with cookies > 0 as the only selected
source (no 0.us.pool.ntp.mil fallback). Only applies 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 fromfiles/hostapi). Each is built into the immutable, dm-verity-protected root, so trusting it doesn't weaken the overlay exec guard. (fapolicyd blockingkiosk-sessionwas the original kiosk-won't-start cause.) - auditd enabled; STIG audit rules applied; logs locally.
- USBGuard enabled + enforcing (
blocktarget) with a HID/hub allowlist andAuditBackend=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-idempty. - 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).