diff --git a/diskless.pkr.hcl b/diskless.pkr.hcl index f832e93..b283969 100644 --- a/diskless.pkr.hcl +++ b/diskless.pkr.hcl @@ -354,6 +354,10 @@ build { # Install the staged unit, weston config, and session wrapper. "install -D -m0755 /tmp/kiosk/kiosk-session /usr/local/bin/kiosk-session", "install -D -m0644 /tmp/kiosk/kiosk.service /etc/systemd/system/kiosk.service", + # Dedicated PAM stack for the autologin session (PAMName=kiosk). Decouples + # the passwordless kiosk user from the STIG-hardened login/system-auth + # account phase (faillock + aging) that would otherwise deny the session. + "install -D -m0644 /tmp/kiosk/kiosk.pam /etc/pam.d/kiosk", "install -D -m0644 /tmp/kiosk/weston.ini /etc/xdg/weston/weston.ini", # Chromium managed policy: stops the browser phoning home to Google # (sign-in, variations, optimization-guide, component/auto update, metrics, @@ -368,7 +372,7 @@ build { "printf 'KIOSK_URL=%s\\n' \"$KIOSK_URL\" > /etc/kiosk/kiosk.env", "chmod 0644 /etc/kiosk/kiosk.env", # Label the new files so they survive into the enforcing erofs. - "restorecon -R /usr/local/bin/kiosk-session /etc/systemd/system/kiosk.service /etc/xdg/weston /etc/kiosk /etc/chromium /etc/chromium-browser", + "restorecon -R /usr/local/bin/kiosk-session /etc/systemd/system/kiosk.service /etc/pam.d/kiosk /etc/xdg/weston /etc/kiosk /etc/chromium /etc/chromium-browser", # Boot graphical and start the kiosk (replaces the tty1 getty). "systemctl enable kiosk.service", "systemctl set-default graphical.target", @@ -483,6 +487,43 @@ build { destination = "${var.artifact_dir}/${var.image_version}/stig-arf.xml" } + # --- 2c-STIG-postfix. Keep the key-only APPLIANCE ACCOUNTS usable after STIG. + # STIG turns on pam_faillock (account phase) + password aging/inactivity + # (account_disable_post_pw_expiration, accounts_*_age_*). The kiosk autologin + # user and the fleet admin are PASSWORDLESS / key-only, so the hardened account + # phase rejects them: the kiosk PAM session never opens (getty keeps tty1), and + # sshd denies the otherwise-valid key as "Permission denied (publickey)" (key + # auth succeeds, then pam_acct_mgmt fails). Runs AFTER remediation so STIG can't + # re-break it. Justified in docs/stig-deviations.md (#10, appliance accounts). + provisioner "shell" { + environment_vars = [ + "ENABLE_STIG=${var.enable_stig}", + "ADMIN_USER=${var.admin_user}", + ] + inline = [ + "set -e", + "if [ \"$ENABLE_STIG\" != \"true\" ]; then echo '2c-STIG-postfix: enable_stig=false -> nothing to undo.'; exit 0; fi", + # Exempt each appliance account from password aging/inactivity/expiry and + # clear any faillock tally (these accounts have NO password to age). + "for u in \"$ADMIN_USER\" kiosk; do", + " { [ -n \"$u\" ] && id \"$u\" >/dev/null 2>&1; } || continue", + " echo \"==> exempting key-only account '$u' from STIG password aging + faillock\"", + " chage -M -1 -m 0 -I -1 -E -1 \"$u\"", + " chage -d \"$(date +%Y-%m-%d)\" \"$u\"", + " command -v faillock >/dev/null 2>&1 && faillock --user \"$u\" --reset || true", + " { [ -d \"/home/$u/.ssh\" ] && restorecon -R \"/home/$u/.ssh\"; } || true", + "done", + # sshd: bypass the hardened ACCOUNT stack (faillock/aging) for the key-only + # admin only, inserted as the FIRST account rule so a valid key can't be + # denied by pam_acct_mgmt. Other users keep the full STIG account policy. + "if [ -n \"$ADMIN_USER\" ] && id \"$ADMIN_USER\" >/dev/null 2>&1; then", + " grep -q \"pam_succeed_if.so user = $ADMIN_USER\" /etc/pam.d/sshd || sed -i \"0,/^account/ s//account sufficient pam_succeed_if.so user = $ADMIN_USER quiet\\n&/\" /etc/pam.d/sshd", + " restorecon /etc/pam.d/sshd", + " echo \"==> /etc/pam.d/sshd: account-stack bypass ensured for $ADMIN_USER\"", + "fi", + ] + } + # --- 2d. Final image hygiene: strip caches/logs after the LAST dnf step -- # MUST be the last provisioner. The `dnf clean all` in step 2 runs BEFORE the # kiosk (2b) and admin (2c) provisioners, and 2b enables EPEL + installs diff --git a/docs/TODO.md b/docs/TODO.md index 28ea51f..a144ca0 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -91,6 +91,21 @@ evaluates 0 rules; (c) `stig-deviations.sh` flags an **INVALID SCAN** and exits non-zero on 0-evaluated. **Re-run the hardened build** and confirm pass/fail counts are non-zero before trusting any deviations report. +**FINDING 2026-06-18 (FIXED) — hardening locked the fleet out.** The first +genuinely-evaluated hardened image (app `215b0eb`) broke **both** login paths: +the Chromium kiosk dropped to a getty login prompt, and admin SSH was rejected +with `Permission denied (publickey)`. Root cause: STIG enables `pam_faillock` +(account phase) + password aging/inactivity, and the **kiosk + fleetadmin +accounts are passwordless / key-only**, so the hardened PAM *account* phase +treats them as expired/lockable (sshd key auth succeeds, then `pam_acct_mgmt` +fails). Fix (provisioner `2c-STIG-postfix`, runs after remediation): exempt the +two appliance accounts — `chage -M -1 -m 0 -I -1 -E -1` + `faillock --reset`; +kiosk uses a dedicated `/etc/pam.d/kiosk` (`PAMName=kiosk`, permissive account / +full session for seat); sshd gets an `account sufficient pam_succeed_if.so user += fleetadmin` first-rule bypass. Scoped to those two accounts; everyone else +keeps full STIG. Justified as register deviation #10. **Bench-verify on rebuild: +kiosk renders + admin SSH works with hardening on.** + Remaining (future hardening, currently waived — see the register): FIPS, firewall, per-host/rotatable admin access. **Remote audit offload — receiver DONE** (`serving/audit-collector/`, rsyslog over TCP/514, per-source-host files; @@ -517,3 +532,28 @@ smoke test. secure `nvme connect` as the test path has. - **Future hardening waived for now** (from the STIG register): FIPS, firewall, remote audit offload, per-host/rotatable admin (SSH CA / config-agent). + +## [ ] 9. (OPTIONAL / nice-to-have) Convert provisioning to Ansible where it pays off + +Low priority — only the *config-management* half of the build is a candidate, and +the ROI is modest because the build VM is single-shot (idempotency buys little) +and reproducibility/auditability favour minimal shell. **Not** a wholesale +rewrite: the load-bearing logic (dracut module hook ordering, `extract-artifacts.sh`, +`build-oci.sh`) is initramfs-runtime / host-side build tooling, not provisioning, +and stays shell. + +The high-ROI, low-risk slice if/when we pick this up: +- **STIG via the SSG Ansible playbook** (`ssg-rl9-playbook-stig.yml`, shipped + with scap-security-guide) instead of `oscap --remediate` — the upstream path + and far more *reviewable* (you can diff exactly what each rule changes). Keep + `oscap eval` (no remediate) for the report + the 0-eval guard (item #2). +- Restyle the brittle imperative bits as declarative tasks: chrony.conf edit + (`lineinfile`/`blockinfile` vs `sed`), sshd drop-in / NM dispatcher / kiosk + file installs (`template`/`copy`), datastream-by-`os-release` as `assert`. + Bonus: sidesteps the `%{...}`→`%%{` HCL-escaping footgun. + +Tips the balance toward "do it" only if: we start managing the **serving hosts** +(dnsmasq/nginx/SPDK/chrony/audit-collector) with config-management too (shared +roles to amortise — currently they're docker-compose), provisioning grows much +more branchy, or we want molecule-tested hardening roles validated without a +full ~17-min Packer build. diff --git a/docs/stig-deviations.md b/docs/stig-deviations.md index 80ef8b3..ab0b099 100644 --- a/docs/stig-deviations.md +++ b/docs/stig-deviations.md @@ -30,6 +30,7 @@ Posture decisions (2026-06-18, lab phase): | 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). | ## Things that are NOT deviations (kept / enforced) @@ -39,8 +40,9 @@ Posture decisions (2026-06-18, lab phase): 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 — harmless (no interactive - passwords) and keep the report honest. +- 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 diff --git a/files/kiosk/kiosk.pam b/files/kiosk/kiosk.pam new file mode 100644 index 0000000..a364921 --- /dev/null +++ b/files/kiosk/kiosk.pam @@ -0,0 +1,23 @@ +# PAM stack for the autologin kiosk session — installed to /etc/pam.d/kiosk and +# referenced by kiosk.service (PAMName=kiosk). Replaces the previous +# PAMName=login, which pulled in the STIG-hardened login/system-auth stacks +# (pam_faillock account phase + password aging/inactivity). The kiosk user is +# unprivileged and PASSWORDLESS, so that hardened *account* phase rejected it and +# the session never opened (getty kept tty1). +# +# systemd runs only the ACCOUNT and SESSION phases for PAMName (never auth), so: +# - account: permissive (the kiosk user has no password to age/lock; this is an +# intentional appliance-account deviation — see docs/stig-deviations.md). +# - session: a full-enough stack that logind/pam_systemd registers the session +# on seat0/vt1 (weston needs a seat for DRM master + libinput). pam_loginuid +# keeps the audit trail; pam_limits applies ulimits. +# A custom service name means STIG/authselect (which only regenerate +# system-auth/password-auth/login) never touch this file. +auth required pam_permit.so +account required pam_permit.so +password required pam_deny.so +session required pam_loginuid.so +session optional pam_keyinit.so force revoke +session required pam_limits.so +session optional pam_systemd.so +session required pam_unix.so diff --git a/files/kiosk/kiosk.service b/files/kiosk/kiosk.service index 953fa82..3e72e1c 100644 --- a/files/kiosk/kiosk.service +++ b/files/kiosk/kiosk.service @@ -10,13 +10,17 @@ Conflicts=getty@tty1.service [Service] Type=simple User=kiosk -# A real PAM login session on tty1 so logind assigns seat0 — weston needs a seat -# for DRM master + libinput. (PAMName runs account/session, not auth, so the -# passwordless kiosk user is fine.) XDG_SEAT/XDG_VTNR are set into the PAM -# environment before pam_open_session, so pam_systemd registers the session on +# A real PAM session on tty1 so logind assigns seat0 — weston needs a seat for +# DRM master + libinput. Uses a DEDICATED PAM stack (/etc/pam.d/kiosk, see +# files/kiosk/kiosk.pam) instead of "login": under STIG the login/system-auth +# account phase (pam_faillock + password aging) rejects the passwordless kiosk +# user, so the session never opened and getty kept tty1. The kiosk stack keeps a +# full session (pam_systemd seat registration) but a permissive account phase. +# (PAMName runs account/session, not auth.) XDG_SEAT/XDG_VTNR are set into the +# PAM environment before pam_open_session so pam_systemd registers the session on # seat0/vt1 — WITHOUT them logind's auto-derivation failed and weston died with # "logind: failed to get session seat" -> "failed to create compositor backend". -PAMName=login +PAMName=kiosk Environment=XDG_SEAT=seat0 Environment=XDG_VTNR=1 TTYPath=/dev/tty1