adding ssh host signing
This commit is contained in:
+77
-45
@@ -136,6 +136,12 @@ variable "enable_agent" {
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "enable_ssh_hostcert" {
|
||||
type = bool
|
||||
description = "Build-time feature flag for the SSH host-certificate client (fleet-ssh-hostcert: at boot, gets this host's per-boot SSH host key signed by the fleet SSH CA via the enrol-broker host plane over TPM-identity mTLS, so admins trust every host via one `@cert-authority` line instead of per-boot TOFU; closes F-02/P-17, docs/ssh-host-certificate-design.md). true (default) installs /usr/local/bin/fleet-ssh-hostcert + ssh-hostcert.service/.timer; false skips it (sshd still boots with its per-boot key). Config is fleet-generic in files/ssh-hostcert/ssh-hostcert.env; set the broker DNS name there per environment."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "boot_splash" {
|
||||
type = bool
|
||||
description = "true (default): bake 'rhgb quiet' into the UKI cmdline so Plymouth shows a graphical boot splash (covers the network-auth + NVMe/TCP + verity phase). false: bake 'plymouth.enable=0' for a fully verbose console boot — use while debugging the boot chain. Plymouth itself is always in the image; this only flips the cmdline. (The cmdline is immutable once signed, so this is a build-time choice.)"
|
||||
@@ -144,14 +150,12 @@ variable "boot_splash" {
|
||||
|
||||
variable "enable_stig" {
|
||||
type = bool
|
||||
description = "true (default): apply DISA STIG hardening (OpenSCAP + SSG) with a diskless/UKI/kiosk tailoring (files/stig/diskless-tailoring.xml), bake STIG kernel params into the UKI cmdline, and emit a deviations report. false: skip hardening entirely — escape hatch if a build regresses the boot/secure-path bench. FIPS is OFF and audit offload is LOCAL-only regardless (see docs/stig-deviations.md)."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "debug_shell" {
|
||||
type = bool
|
||||
description = "DEBUG/LAB ONLY. true: enable systemd's debug-shell.service — a passwordless root /bin/sh on tty2 (Ctrl+Alt+F2) that bypasses login/PAM entirely. (Placed on tty2, with the login getty there masked, because constrained consoles often expose only tty1-6; tty1 stays the kiosk.) Use to get into a hardened image when login is broken (locked root + key-only accounts). This is a deliberate security hole; NEVER ship it. Default false."
|
||||
description = "MUST be true (enforced). Applies DISA STIG hardening (OpenSCAP + SSG) with a diskless/UKI/kiosk tailoring (files/stig/diskless-tailoring.xml), bakes STIG kernel params into the UKI cmdline, and emits a deviations report. FIPS is OFF and audit offload is LOCAL-only regardless (see docs/stig-deviations.md). The former enable_stig=false escape hatch is removed for production hardening (POA&M P-03) — a build with enable_stig=false now fails validation. To debug a hardening regression, revert this validation on a throwaway branch, never on a release build."
|
||||
default = true
|
||||
validation {
|
||||
condition = var.enable_stig
|
||||
error_message = "STIG hardening is mandatory: enable_stig must be true. Production builds are hardening-mandatory (POA&M P-03); there is no unhardened build path, so do not set enable_stig=false on a release build."
|
||||
}
|
||||
}
|
||||
|
||||
variable "audit_host" {
|
||||
@@ -294,6 +298,19 @@ build {
|
||||
destination = "/tmp/fleet-agent"
|
||||
}
|
||||
|
||||
# SSH host-certificate client: the systemd unit + timer AND the baked
|
||||
# fleet-generic config (ssh-hostcert.env) ride this module upload to
|
||||
# /tmp/ssh-hostcert (installed in the 2b-ssh-hostcert step, after /tmp/etc-stage
|
||||
# is cleaned — same reason as the agent). Static binary staged separately below.
|
||||
provisioner "file" {
|
||||
source = "files/ssh-hostcert"
|
||||
destination = "/tmp/"
|
||||
}
|
||||
provisioner "file" {
|
||||
source = "files/ssh-hostcert/bin/fleet-ssh-hostcert"
|
||||
destination = "/tmp/fleet-ssh-hostcert"
|
||||
}
|
||||
|
||||
# Custom 'fleet' Plymouth theme (scripted security-handshake splash). Staged to
|
||||
# /tmp/plymouth, installed + set default in the package step before dracut runs.
|
||||
provisioner "file" {
|
||||
@@ -694,6 +711,37 @@ build {
|
||||
]
|
||||
}
|
||||
|
||||
# --- 2b-ssh-hostcert. SSH host-certificate client (Go, TPM-authenticated) --
|
||||
# Installs the OUT-OF-VM-built /usr/local/bin/fleet-ssh-hostcert (staged to
|
||||
# /tmp/fleet-ssh-hostcert) + its systemd unit + renewal timer + baked,
|
||||
# fleet-generic config. At boot it gets this host's PER-BOOT SSH host key signed
|
||||
# by the fleet SSH CA via the enrol-broker host plane (TPM-identity mTLS), so
|
||||
# admins trust every host via one `@cert-authority` line instead of TOFU on a key
|
||||
# that changes each boot (closes F-02 / POA&M P-17; docs/ssh-host-certificate-design.md).
|
||||
# NOTHING host-specific is baked (invariant #1) — the broker DNS name in
|
||||
# ssh-hostcert.env is fleet-generic; the host identity comes from the TPM at
|
||||
# runtime (same 0x81000100 key / 0x01800100 cert as 802.1X + the agent). We
|
||||
# enable the TIMER (not the service) so it fires at boot + renews. Gated on
|
||||
# enable_ssh_hostcert. The binary is fapolicyd-trusted in 2c-STIG-postfix
|
||||
# (non-RPM exec, like hostapi/agent/kiosk-session).
|
||||
provisioner "shell" {
|
||||
environment_vars = [
|
||||
"ENABLE_SSH_HOSTCERT=${var.enable_ssh_hostcert}",
|
||||
]
|
||||
inline = [
|
||||
"set -euxo pipefail",
|
||||
"if [ \"$ENABLE_SSH_HOSTCERT\" != \"true\" ]; then echo 'enable_ssh_hostcert=false -> skipping fleet-ssh-hostcert install.'; exit 0; fi",
|
||||
"install -D -m0755 /tmp/fleet-ssh-hostcert /usr/local/bin/fleet-ssh-hostcert",
|
||||
"install -D -m0644 /tmp/ssh-hostcert/ssh-hostcert.service /etc/systemd/system/ssh-hostcert.service",
|
||||
"install -D -m0644 /tmp/ssh-hostcert/ssh-hostcert.timer /etc/systemd/system/ssh-hostcert.timer",
|
||||
"install -D -m0644 /tmp/ssh-hostcert/ssh-hostcert.env /etc/ssh-hostcert/ssh-hostcert.env",
|
||||
"restorecon -R /usr/local/bin/fleet-ssh-hostcert /etc/systemd/system/ssh-hostcert.service /etc/systemd/system/ssh-hostcert.timer /etc/ssh-hostcert 2>/dev/null || true",
|
||||
"systemctl enable ssh-hostcert.timer",
|
||||
"rm -rf /tmp/ssh-hostcert /tmp/fleet-ssh-hostcert",
|
||||
"echo '==> fleet-ssh-hostcert enabled (SSH host cert from the fleet SSH CA; timer at boot + renew)'",
|
||||
]
|
||||
}
|
||||
|
||||
# --- 2c. Admin access: sudo-enabled key user + locked root --------------
|
||||
# Fleet-generic (same user + key on every host => NOT per-host state). The
|
||||
# key comes from var.ssh_authorized_key (CI secret). If no key is supplied we
|
||||
@@ -727,7 +775,7 @@ build {
|
||||
# unaffected.
|
||||
# Banner: show the fleet (starship) login notice pre-auth. The banner file
|
||||
# itself is written by the '2e. Fleet login banner' step below (after STIG,
|
||||
# so it overrides the DoD banner). Set here too so it works with enable_stig=false.
|
||||
# so it overrides the DoD banner). Set here too as a pre-STIG baseline.
|
||||
"printf 'PermitRootLogin no\\nPasswordAuthentication no\\nKbdInteractiveAuthentication no\\nBanner /etc/issue.net\\n' > /etc/ssh/sshd_config.d/50-fleet-hardening.conf",
|
||||
"chmod 0644 /etc/ssh/sshd_config.d/50-fleet-hardening.conf",
|
||||
"restorecon /etc/ssh/sshd_config.d/50-fleet-hardening.conf",
|
||||
@@ -742,34 +790,13 @@ build {
|
||||
]
|
||||
}
|
||||
|
||||
# --- 2c-debug. Break-glass root shell (DEBUG/LAB ONLY) -------------------
|
||||
# debug_shell=true enables systemd's debug-shell.service: a passwordless root
|
||||
# /bin/sh on tty9 (Ctrl+Alt+F9) that does NOT go through login/PAM — so you can
|
||||
# get into a hardened image even when root is locked, the accounts are key-only,
|
||||
# and login/sshd are broken. This is a deliberate security hole for diagnosing
|
||||
# boot/login regressions; never ship it. The symlink into sysinit.target.wants
|
||||
# guarantees it starts early regardless of the unit's [Install] section.
|
||||
provisioner "shell" {
|
||||
environment_vars = ["DEBUG_SHELL=${var.debug_shell}"]
|
||||
inline = [
|
||||
"set -e",
|
||||
"if [ \"$DEBUG_SHELL\" != \"true\" ]; then echo 'debug_shell=false -> no debug shell baked.'; exit 0; fi",
|
||||
"echo '############################################################'",
|
||||
"echo '## WARNING: debug_shell=true — PASSWORDLESS ROOT on tty2. ##'",
|
||||
"echo '## DEBUG/LAB IMAGE ONLY. Do NOT deploy to production. ##'",
|
||||
"echo '############################################################'",
|
||||
"systemctl enable debug-shell.service 2>/dev/null || true",
|
||||
"mkdir -p /etc/systemd/system/sysinit.target.wants",
|
||||
"ln -sf /usr/lib/systemd/system/debug-shell.service /etc/systemd/system/sysinit.target.wants/debug-shell.service",
|
||||
# Default debug-shell is tty9 (outside the getty range) — but constrained
|
||||
# consoles (noVNC/SPICE) often only expose tty1-6. Move it to tty2, which
|
||||
# IS reachable, and mask the login getty there so they don't fight over the
|
||||
# VT. Result: Ctrl+Alt+F2 = passwordless root shell. (tty1 stays the kiosk.)
|
||||
"mkdir -p /etc/systemd/system/debug-shell.service.d",
|
||||
"printf '[Service]\\nTTYPath=/dev/tty2\\n' > /etc/systemd/system/debug-shell.service.d/tty.conf",
|
||||
"systemctl mask getty@tty2.service autovt@tty2.service 2>/dev/null || true",
|
||||
]
|
||||
}
|
||||
# --- 2c-debug REMOVED (POA&M P-02 / R-02) -------------------------------
|
||||
# The break-glass systemd debug-shell.service (passwordless root on a tty,
|
||||
# bypassing login/PAM) is no longer part of any build. It was an undocumented
|
||||
# privileged access path flagged High-risk in the A&A (irap-poam.md P-02,
|
||||
# ssp.md R-02). There is intentionally NO debug_shell variable and NO provisioner
|
||||
# that enables debug-shell.service. To diagnose a boot/login regression, use a
|
||||
# throwaway branch/serial console — never reintroduce a shipped root shell.
|
||||
|
||||
# --- 2c-STIG. DISA STIG hardening (OpenSCAP + SSG), diskless/UKI/kiosk tailored
|
||||
# Runs after admin (so it hardens the full system incl. the admin/sshd config)
|
||||
@@ -789,7 +816,10 @@ build {
|
||||
]
|
||||
inline = [
|
||||
"set -e",
|
||||
"if [ \"$ENABLE_STIG\" != \"true\" ]; then echo 'enable_stig=false -> skipping STIG hardening.'; mkdir -p /root; echo '<html><body>STIG hardening disabled for this build (enable_stig=false).</body></html>' > /root/stig-report.html; : > /root/stig-arf.xml; exit 0; fi",
|
||||
# enable_stig is enforced true by variable validation (POA&M P-03). This is
|
||||
# a fail-closed backstop: if STIG were ever somehow disabled we abort the
|
||||
# build rather than silently ship an unhardened image masquerading as one.
|
||||
"if [ \"$ENABLE_STIG\" != \"true\" ]; then echo 'FATAL: ENABLE_STIG != true — production builds are hardening-mandatory (no unhardened path).' >&2; exit 1; fi",
|
||||
"echo '==> Installing OpenSCAP + SCAP Security Guide'",
|
||||
"dnf install -y openscap-scanner scap-security-guide",
|
||||
# Pick the datastream that MATCHES THIS DISTRO. The RHEL9 datastream's CPE
|
||||
@@ -855,8 +885,8 @@ build {
|
||||
# sysadm_u) so SSH login isn't gated by the ssh_sysadm_login bool.
|
||||
# ⚠ BENCH-VERIFY: admin SSH login succeeds AND `sudo` works AND every
|
||||
# privileged op the admin needs runs under the sysadm_t transition. If
|
||||
# this locks the admin out, enable_stig=false is the escape hatch and
|
||||
# this rule reverts to a justified deviation. ⚠ REPRODUCIBILITY: confirm
|
||||
# this locks the admin out, revert this rule to a justified deviation in
|
||||
# the tailoring (NOT by disabling STIG — that path is gone). ⚠ REPRODUCIBILITY: confirm
|
||||
# the verity hash is stable across two builds — semanage rebuilds the
|
||||
# SELinux policy store and may write nondeterministic content; if it
|
||||
# drifts, normalize in 2d.
|
||||
@@ -876,8 +906,8 @@ build {
|
||||
# GUARD: a scan with 0 evaluated rules (everything notapplicable/notselected)
|
||||
# means the datastream/CPE didn't match this host — NO hardening happened and
|
||||
# the report would be falsely 'clean'. Fail the build rather than ship an
|
||||
# unhardened image masquerading as STIG-compliant. (enable_stig=false is the
|
||||
# intentional way to skip hardening.)
|
||||
# unhardened image masquerading as STIG-compliant. (There is no skip-hardening
|
||||
# path — enable_stig is enforced true, POA&M P-03.)
|
||||
"evaluated=$(grep -oE '<result>(pass|fixed|fail)</result>' /root/stig-arf.xml | wc -l)",
|
||||
"[ \"$evaluated\" -gt 0 ] || { echo 'ERROR: STIG scan evaluated 0 rules (all notapplicable/notselected) — SSG datastream/CPE mismatch for this distro. Hardening did NOT run; aborting.' >&2; exit 1; }",
|
||||
"echo \"==> STIG scan evaluated $evaluated rules (pass/fixed/fail)\"",
|
||||
@@ -960,7 +990,8 @@ build {
|
||||
]
|
||||
inline = [
|
||||
"set -e",
|
||||
"if [ \"$ENABLE_STIG\" != \"true\" ]; then echo '2c-STIG-postfix: enable_stig=false -> nothing to undo.'; exit 0; fi",
|
||||
# enable_stig is enforced true (POA&M P-03); fail closed if it ever isn't.
|
||||
"if [ \"$ENABLE_STIG\" != \"true\" ]; then echo 'FATAL: 2c-STIG-postfix reached with ENABLE_STIG != true.' >&2; exit 1; 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",
|
||||
@@ -1001,9 +1032,9 @@ build {
|
||||
# provide). Can drop lower after measuring `fapolicyd-cli --dump | wc -l` on
|
||||
# the bench; see docs/TODO.md.
|
||||
" if [ -f /etc/fapolicyd/fapolicyd.conf ]; then sed -ri 's/^[[:space:]]*#?[[:space:]]*db_max_size[[:space:]]*=.*/db_max_size = 50/' /etc/fapolicyd/fapolicyd.conf; grep -q '^db_max_size' /etc/fapolicyd/fapolicyd.conf || printf 'db_max_size = 50\\n' >> /etc/fapolicyd/fapolicyd.conf; echo '==> fapolicyd: db_max_size set to 50 MiB (was EL9 default ~100)'; fi",
|
||||
" for f in /usr/local/bin/kiosk-session /usr/local/bin/fleet-sb-enroll /usr/local/bin/fleet-hostapi /usr/local/bin/fleet-agent /usr/sbin/fleet-enrol; do { [ -f \"$f\" ] && fapolicyd-cli --file add \"$f\"; } || true; done",
|
||||
" for f in /usr/local/bin/kiosk-session /usr/local/bin/fleet-sb-enroll /usr/local/bin/fleet-hostapi /usr/local/bin/fleet-agent /usr/local/bin/fleet-ssh-hostcert /usr/sbin/fleet-enrol; do { [ -f \"$f\" ] && fapolicyd-cli --file add \"$f\"; } || true; done",
|
||||
" fapolicyd-cli --update 2>/dev/null || true",
|
||||
" echo '==> fapolicyd: trusted custom binaries (kiosk-session, fleet-sb-enroll, fleet-hostapi, fleet-agent) + rebuilt trust db'",
|
||||
" echo '==> fapolicyd: trusted custom binaries (kiosk-session, fleet-sb-enroll, fleet-hostapi, fleet-agent, fleet-ssh-hostcert) + rebuilt trust db'",
|
||||
# watch_fs: the diskless root is OVERLAYFS (systemd.volatile=overlay), but the
|
||||
# shipped watch_fs lacks 'overlay', so fapolicyd marks NO overlay mount and
|
||||
# therefore intercepts NO exec on / — the exec guard for the writable overlay
|
||||
@@ -1017,7 +1048,8 @@ build {
|
||||
# for the WHOLE root at runtime for the first time: boot + kiosk render + admin
|
||||
# SSH + sudo + TPM enrolment must ALL still work (nothing legit is untrusted),
|
||||
# a novel /tmp binary must now be BLOCKED (EPERM), and watch for new AVCs.
|
||||
# enable_stig=false is the escape hatch if it regresses the boot/secure path.
|
||||
# If it regresses the boot/secure path, waive the offending rule in the
|
||||
# tailoring (a justified deviation) — not by disabling STIG (that path is gone).
|
||||
" if [ -f /etc/fapolicyd/fapolicyd.conf ]; then",
|
||||
" grep -Eq '^watch_fs.*overlay' /etc/fapolicyd/fapolicyd.conf || sed -ri '/^watch_fs *=/ s/$/,overlay/' /etc/fapolicyd/fapolicyd.conf",
|
||||
" grep -Eq '^watch_fs.*overlay' /etc/fapolicyd/fapolicyd.conf || { echo 'ERROR: failed to add overlay to fapolicyd watch_fs' >&2; exit 1; }",
|
||||
|
||||
Reference in New Issue
Block a user