Fix stig over hardening of kiosk account
This commit is contained in:
+42
-1
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user