Merge pull request 'poam-hardening' (#22) from poam-hardening into main

Reviewed-on: https://git.bitasylum.org/audiobuzz/diskless-workstation/pulls/22
This commit is contained in:
audiobuzz
2026-07-17 05:12:10 +00:00
4 changed files with 62 additions and 32 deletions
+8 -3
View File
@@ -933,8 +933,13 @@ build {
# SELinux context. SSG ships no auto-fix. Decision (2026-06-20): # SELinux context. SSG ships no auto-fix. Decision (2026-06-20):
# REMEDIATE WITH CONFINEMENT. Two parts: # REMEDIATE WITH CONFINEMENT. Two parts:
# 1. Rewrite the admin sudoers rule to add the role/type transition # 1. Rewrite the admin sudoers rule to add the role/type transition
# (KEEP NOPASSWD — the admin is key-only; #8). This is what the # (KEEP NOPASSWD — the admin is key-only; #8). The rule MUST be a
# OVAL check looks for, so it makes the rule pass. # **%group rule** (%wheel — the admin is a wheel member): the SSG
# OVAL patterns are `^\s*%\w+.*TYPE=(\w+)` / `...ROLE=(\w+)`, so a
# per-USER rule never matches and the scan fails even though the
# runtime transition works (confirmed on scan poam-hardening-5bc6d27:
# bench host elevated staff_u->sysadm_r fine, rule still `fail`).
# This replaces the per-user rule 2c wrote (same file, overwritten).
# 2. Map the admin to the confined **staff_u** SELinux user so the # 2. Map the admin to the confined **staff_u** SELinux user so the
# staff_r->sysadm_r sudo transition is actually authorized at # staff_r->sysadm_r sudo transition is actually authorized at
# runtime (unconfined_u cannot reach sysadm_r). staff_u (NOT # runtime (unconfined_u cannot reach sysadm_r). staff_u (NOT
@@ -949,7 +954,7 @@ build {
"if [ -n \"$ADMIN_USER\" ] && id \"$ADMIN_USER\" >/dev/null 2>&1; then", "if [ -n \"$ADMIN_USER\" ] && id \"$ADMIN_USER\" >/dev/null 2>&1; then",
" echo \"==> supplemental: sudo SELinux elevation — confining $ADMIN_USER to staff_u + sudoers role/type\"", " echo \"==> supplemental: sudo SELinux elevation — confining $ADMIN_USER to staff_u + sudoers role/type\"",
" command -v semanage >/dev/null 2>&1 || dnf install -y policycoreutils-python-utils", " command -v semanage >/dev/null 2>&1 || dnf install -y policycoreutils-python-utils",
" printf '%s ALL=(ALL) TYPE=sysadm_t ROLE=sysadm_r NOPASSWD:ALL\\n' \"$ADMIN_USER\" > \"/etc/sudoers.d/$ADMIN_USER\"", " printf '%%wheel ALL=(ALL) TYPE=sysadm_t ROLE=sysadm_r NOPASSWD:ALL\\n' > \"/etc/sudoers.d/$ADMIN_USER\"",
" chmod 0440 \"/etc/sudoers.d/$ADMIN_USER\"", " chmod 0440 \"/etc/sudoers.d/$ADMIN_USER\"",
" visudo -cf \"/etc/sudoers.d/$ADMIN_USER\"", " visudo -cf \"/etc/sudoers.d/$ADMIN_USER\"",
" restorecon \"/etc/sudoers.d/$ADMIN_USER\"", " restorecon \"/etc/sudoers.d/$ADMIN_USER\"",
+18 -12
View File
@@ -207,15 +207,19 @@ pass / N/A.
mouse on real hardware — the build-VM-generated policy + default block target mouse on real hardware — the build-VM-generated policy + default block target
would otherwise lock out console input). would otherwise lock out console input).
- **`selinux_context_elevation_for_sudo` — REMEDIATED WITH CONFINEMENT - **`selinux_context_elevation_for_sudo` — REMEDIATED WITH CONFINEMENT
(decision 2026-06-20).** 2c-STIG now rewrites the admin sudoers to (decision 2026-06-20; scan-fix 2026-07-12).** 2c-STIG rewrites the admin
`TYPE=sysadm_t ROLE=sysadm_r NOPASSWD:ALL` and maps the admin to **`staff_u`** sudoers drop-in to `%wheel ALL=(ALL) TYPE=sysadm_t ROLE=sysadm_r NOPASSWD:ALL`
(`semanage login`) so the sudo role transition is authorized. ⚠ **TWO BENCH and maps the admin to **`staff_u`** (`semanage login`) so the sudo role
GATES:** (a) admin SSH login + sudo + privileged ops must work under `staff_u` transition is authorized. **Must be a %group rule:** the first cut used a
confinement (if it locks out, waive the offending rule in the tailoring as a per-user rule — runtime elevation worked (bench-verified 2026-07-12,
justified deviation — `enable_stig=false` is no longer available, STIG is `comet-beacon-54`) but the SSG OVAL (`^\s*%\w+.*TYPE=`) only matches %group
enforced-true); (b) **reproducibility** — `semanage` rebuilds the policy store; rules, so the scan stayed `fail` (the 1 REVIEW row on `poam-hardening-5bc6d27`).
confirm the verity hash is stable across two builds (normalize in 2d if it ⚠ **BENCH GATES:** (a) admin SSH login + sudo under `staff_u` confinement —
drifts). Needs `policycoreutils-python-utils` (installed in 2c-STIG if absent). VERIFIED 2026-07-12 (re-confirm after the %wheel change; same account via its
group, behaviour should be identical); (b) **reproducibility** — `semanage`
rebuilds the policy store; confirm the verity hash is stable across two builds
(normalize in 2d if it drifts). Needs `policycoreutils-python-utils`
(installed in 2c-STIG if absent).
**Expected next-build report: 0–1 REVIEW** (0 if both postfix→N/A and **Expected next-build report: 0–1 REVIEW** (0 if both postfix→N/A and
usbguard→pass land and sudo confinement holds; the triage was validated by usbguard→pass land and sudo confinement holds; the triage was validated by
@@ -1000,10 +1004,12 @@ smoke test.
record **drained losslessly** after the real cert was restored (original record **drained losslessly** after the real cert was restored (original
timestamp preserved); STIG scan of record: all three timestamp preserved); STIG scan of record: all three
`rsyslog_encrypt_offload_*` rules **pass**, 401 pass / 1 fail, 0 new `rsyslog_encrypt_offload_*` rules **pass**, 401 pass / 1 fail, 0 new
REVIEW rows (the 1 remaining REVIEW is the pre-existing REVIEW rows (the 1 remaining REVIEW was the pre-existing
`selinux_context_elevation_for_sudo` — runtime behaviour verified working `selinux_context_elevation_for_sudo` — runtime behaviour verified working
on the same host, likely an OVAL word-order artifact: the check may want on the same host; ROOT CAUSE found in the ARF and FIXED 2026-07-12: the SSG
`ROLE=... TYPE=...` where 2c-STIG writes `TYPE=... ROLE=...` — follow-up). OVAL only matches **%group** sudoers rules (`^\s*%\w+.*TYPE=`), and 2c-STIG
wrote a per-user rule — now writes `%wheel ...`; next scan should be 0
REVIEW).
Bench gotcha worth keeping: the smallstep password file location varies by Bench gotcha worth keeping: the smallstep password file location varies by
image version (`$STEPPATH/password` vs `$STEPPATH/secrets/password`) — image version (`$STEPPATH/password` vs `$STEPPATH/secrets/password`) —
issue-cert.sh probes both (fixed 2026-07-12 after hitting it live). issue-cert.sh probes both (fixed 2026-07-12 after hitting it live).
+29 -16
View File
@@ -103,18 +103,22 @@ private keys stored on the host; host keys regenerate per boot).
SSG ships no automated fix; the STIG manual remediation SSG ships no automated fix; the STIG manual remediation
(`... TYPE=sysadm_t ROLE=sysadm_r ...`) requires the admin to reach the (`... 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 `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 (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 is ROLE=sysadm_r NOPASSWD:ALL` (keeps NOPASSWD — the admin is key-only, #8), and
what the OVAL check matches), and (2) maps the admin to the confined **`staff_u`** (2) maps the admin to the confined **`staff_u`** SELinux user (`semanage login`)
SELinux user (`semanage login`) so the `staff_r`→`sysadm_r` sudo transition is so the `staff_r`→`sysadm_r` sudo transition is authorized at runtime. `staff_u`
authorized at runtime. `staff_u` (not `sysadm_u`) is used so SSH login isn't gated (not `sysadm_u`) is used so SSH login isn't gated by the `ssh_sysadm_login`
by the `ssh_sysadm_login` boolean. **⚠ Two bench gates before trusting:** (a) boolean. **Scan-vs-runtime finding (2026-07-12, scan `poam-hardening-5bc6d27`):**
admin SSH login + `sudo` + every privileged op must work under `staff_u` the first cut wrote a per-USER rule (`<admin> ALL=...`), which elevates fine at
confinement — if it locks out, waive the offending rule in the tailoring as a runtime (bench host `comet-beacon-54`: `sudo id` →
justified deviation (STIG is now enforced-true — there is no `enable_stig=false` `staff_u:sysadm_r:sysadm_t`) but the rule still scanned **fail** — the SSG OVAL
escape hatch, POA&M P-03); (b) reproducibility — `semanage` rebuilds the patterns (`^\s*%\w+.*TYPE=(\w+)` / `...ROLE=(\w+)`) only match **%group**
SELinux policy store, so confirm the verity hash is stable across two builds (and rules. Fixed by writing the rule for `%wheel` (the admin is a wheel member);
normalize in 2d hygiene if it drifts). 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) ## 13. Ctrl+Alt+Del console reboot hotkey kept enabled (decision 2026-06-20)
@@ -294,11 +298,20 @@ boot; (b) no other chrony-log AVC (extend here if one appears); (c)
**reproducibility** — `semodule -i` rebuilds the policy store, re-check verity hash **reproducibility** — `semodule -i` rebuilds the policy store, re-check verity hash
stability (same concern as #14/#15). Only shipped when `enable_stig=true`. 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) ## 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` **Rules waived:** `chronyd_specify_remote_server`, `chronyd_or_ntpd_set_maxpoll`,
(unselected in `diskless-tailoring.xml`; plus a belt-and-suspenders re-strip in `chronyd_server_directive` (unselected in `diskless-tailoring.xml`; plus a
`2c-STIG-postfix`). 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** **Why:** these rules remediate by writing a **static, unauthenticated**
`server <pool> maxpoll 16` into `/etc/chrony.conf`. The fleet instead configures `server <pool> maxpoll 16` into `/etc/chrony.conf`. The fleet instead configures
+7 -1
View File
@@ -181,10 +181,16 @@
DNS/NTP spoofer could shift the clock through it — the exact F-03 risk). DNS/NTP spoofer could shift the clock through it — the exact F-03 risk).
Unselect so remediation adds no plain server; 2c-STIG-postfix also Unselect so remediation adds no plain server; 2c-STIG-postfix also
re-strips any server/pool when secure_ntp=true (belt-and-suspenders). re-strips any server/pool when secure_ntp=true (belt-and-suspenders).
The STIG intent (time IS synchronised, authenticated under NTS) is met. chronyd_server_directive is the CHECK-ONLY companion (requires a literal
`^server` line in the baked chrony.conf/chrony.d — impossible under
secure_ntp, where the NTS source lands in /run/chrony-dhcp at runtime;
surfaced as the 1 REVIEW on scan poam-hardening-4e9ab45, the first
post-secure-ntp full scan). The STIG intent (time IS synchronised,
authenticated under NTS) is met.
Justified in docs/stig-deviations.md #17. --> Justified in docs/stig-deviations.md #17. -->
<xccdf:select idref="xccdf_org.ssgproject.content_rule_chronyd_specify_remote_server" selected="false"/> <xccdf:select idref="xccdf_org.ssgproject.content_rule_chronyd_specify_remote_server" selected="false"/>
<xccdf:select idref="xccdf_org.ssgproject.content_rule_chronyd_or_ntpd_set_maxpoll" selected="false"/> <xccdf:select idref="xccdf_org.ssgproject.content_rule_chronyd_or_ntpd_set_maxpoll" selected="false"/>
<xccdf:select idref="xccdf_org.ssgproject.content_rule_chronyd_server_directive" selected="false"/>
</xccdf:Profile> </xccdf:Profile>
</xccdf:Tailoring> </xccdf:Tailoring>