Merge remote-tracking branch 'origin/main' into poam-hardening

# Conflicts:
#	CLAUDE.md
This commit is contained in:
Audiobuzz
2026-07-12 05:54:41 +00:00
18 changed files with 1277 additions and 84 deletions
+54 -13
View File
@@ -36,6 +36,14 @@ files/hostapi/ Go host-info agent (fleet-hostapi): re
loopback :9099 /v1/host for the kiosk SPA
(HOST-API.md contract). Built in 2b-hostapi,
fapolicyd-trusted, golang removed in 2d.
files/ssh-hostcert/ Go SSH host-cert client (fleet-ssh-hostcert):
at boot signs this host's per-boot SSH host
key with the fleet SSH CA via the enrol-broker
host plane (TPM-identity mTLS), so admins trust
via one @cert-authority line instead of per-boot
TOFU (F-02/P-17). Built in 2b-ssh-hostcert,
fapolicyd-trusted; enable_ssh_hostcert flag.
docs/ssh-host-certificate-design.md.
scripts/extract-artifacts.sh qcow2 -> kernel/initramfs/erofs+verity/cmdline
scripts/build-oci.sh ukify + sbsign + roothash gate + OCI image
scripts/build-ipxe-oci.sh sbsign snponly.efi -> FROM-scratch fleet-ipxe image
@@ -156,7 +164,11 @@ instead of working around it.
1. **Signing keys never enter the build VM or this repo.** `sb_key` /
`sb_cert` are CI/HSM-provided paths on the build host. Never commit key
material, never copy keys into provisioner steps, never echo them in
logs. Unsigned builds are lab-only and must keep their loud warning.
logs. Unsigned builds are **local-lab-only** (a bare `packer build` without
keys) and must keep their loud warning; **CI fails closed** — the
`gitea-main-ci.yaml`/`ipxe-image.yaml` "Production build gate" aborts unless
`SB_KEY`/`SB_CERT` (and `FLEET_CA` for the UKI build) are set, so no unsigned
or placeholder-CA artifact is producible via CI (POA&M P-03/P-05).
1. **Reproducibility of the root image is a security property.**
`mkfs.erofs` runs with fixed UUID and `-T 0`; the verity root hash must
be a pure function of the input tree. Don’t introduce timestamps,
@@ -214,8 +226,10 @@ instead of working around it.
are evaluated, and `stig-deviations.sh` flags an INVALID SCAN + exits 2. Also
confirm the `...content_profile_stig` profile id exists in the chosen
datastream; (b) remediation must not break
boot/login (NM/NVMe, key-only sshd, kiosk, SELinux enforcing) — `enable_stig=false`
is the escape hatch; (c) **reproducibility**: build twice and confirm identical
boot/login (NM/NVMe, key-only sshd, kiosk, SELinux enforcing) — waive the
offending rule in the tailoring as a justified deviation (STIG is now
**enforced-true** by Packer variable validation — `enable_stig=false` is no
longer a build path; POA&M P-03); (c) **reproducibility**: build twice and confirm identical
`root_hash` in verity.json — if it drifts, oscap wrote something
nondeterministic (timestamp/uuid/key); find + normalize it in 2d hygiene;
(d) read the generated `stig-deviations.md` — every REVIEW row is a real finding
@@ -247,13 +261,36 @@ instead of working around it.
**re-check reproducibility** (semanage rebuilds the policy store → possible
verity-hash drift). The triage waivers/decisions live in the tailoring +
docs/stig-deviations.md #12; the generated report should show 0–1 REVIEW.
- NTP via DHCP option 42 → chrony (no public-pool egress): confirm on a booted
host `chronyc sources` lists the DHCP-handed lab server (not `*.pool.ntp.org`).
Assumes (a) NM exports the lease's NTP servers to the dispatcher as
`$DHCP4_NTP_SERVERS`, and (b) RHEL 9 `/etc/chrony.conf` honours
`sourcedir /run/chrony-dhcp` (the provisioner appends it if absent). If sources
are empty, the dispatcher didn't fire or the env var name differs — check
`journalctl -u NetworkManager` and `ls /run/chrony-dhcp/`.
- **NTP is `secure_ntp=true` by default (NTS, RFC 8915) — CA-authenticated time**
(closes F-03/P-18; docs/secure-ntp-design.md). **No FQDN is baked:** the NM
dispatcher (`10-chrony-dhcp-ntp`, mode-aware) derives the server name at runtime
as `<ntp_prefix>.<DHCP-provided-domain>` (default `ntp`.`$DHCP4_DOMAIN_NAME`,
e.g. `ntp.fleet.lab`), writes `server … iburst nts` into `sourcedir
/run/chrony-dhcp`, and chrony verifies the NTS-KE TLS cert against the **fleet
CA** (`/etc/dot1x/radius-ca.pem` — reused; baked as `ntstrustedcerts`). Only the
`ntp` label + the CA are baked, so the **site domain can change without a
rebuild** (new DHCP domain + reissued NTS cert). A DHCP/NTP/DNS spoofer can only
DoS (no fleet-CA-signed cert for the name it redirects to ⇒ NTS fails ⇒ no sync),
never shift the clock. Serving needs: DHCP **option 15** (domain-name) handed to
hosts, a **fleet-signed** NTS cert with DNS SAN `ntp.<domain>`
(`serving/chrony/nts-cert.sh`), and **tcp/4460** (NTS-KE) open + udp/123. Confirm
on a booted host: `chronyc -N authdata` shows the source in `NTS` mode, cookies
> 0. `nocerttimecheck 1` lets a cold boot clock (no RTC) bootstrap the first sync.
**Bench finding (2026-07-11, FIXED):** the chrony RPM ships its OWN NM dispatcher
`/usr/lib/NetworkManager/dispatcher.d/20-chrony-dhcp`; NM merges `/etc` + `/usr/lib`
dispatchers by name, so it ran AFTER our `10-chrony-dhcp-ntp` and overwrote the NTS
line in the shared `/run/chrony-dhcp/<iface>.sources` with the plain option-42
server — silently defeating NTS (authdata Mode `-`, Cook 0). The provisioner now
`rm`s it (NTS then reached Cook 8 on the bench). Don't re-add a competing writer to
that file. **Second bench finding (2026-07-12, FIXED):** removing that distro script
also removed the `restorecon` that had been labelling the sources — the dispatcher's
`mkdir -p` leaves `/run/chrony-dhcp` + files as generic `var_run_t`, which
`chronyd_t` is SELinux-DENIED reading (loads 0 sources, `authdata` empty). Fixed by
`/etc/tmpfiles.d/chrony-dhcp.conf` pre-creating the dir with the stock
`chronyd_var_run_t` fcontext at sysinit, so the dispatcher's files inherit a readable
label (no allow rules). The dispatcher also now skips `lo` (was writing a spurious
duplicate). **Toggle:** `secure_ntp=false` reverts the dispatcher to the legacy
unauthenticated **DHCP option 42** path (plain `server <ip> iburst`).
- TLS audit offload (2f → `serving/audit-collector`, gtls/6514, since
2026-07-12): the plaintext-514 path was bench-verified; the TLS upgrade is
NOT yet. On a booted host confirm the sender is ESTABLISHED to **:6514**
@@ -265,6 +302,7 @@ instead of working around it.
same ufw trap as 514), image built with the real fleet CA. The three
`rsyslog_encrypt_offload_*` STIG rules are enforced (waivers dropped) and
must PASS in the report.
- `fleet` Plymouth theme: confirm (a) the script plugin package actually
installed (`/usr/lib64/plymouth/script.so` — else the build fell back to
`spinner`, logged as a WARNING), (b) the scripted theme + a font got bundled
into the initramfs (`lsinitrd | grep -E 'fleet.script|script.so'`), and (c) it
@@ -320,7 +358,7 @@ These bit us bringing the build up on Rocky 9.8; they are easy to regress.
stack stays in the image, dormant. The matching lab DHCP/HTTP/NVMe-oF serving
stack lives in `serving/` (docker compose). Re-enable security by dropping the
var / setting `DISABLE_DOT1X: "false"` in the CI workflow.
- **STIG toggle**: `enable_stig=true` (default) applies DISA STIG via
- **STIG toggle** (now **enforced-true**, not just default): `enable_stig=true` applies DISA STIG via
OpenSCAP/SSG in a provisioner, tailored for the diskless/UKI/kiosk architecture
(`files/stig/diskless-tailoring.xml` unselects the N/A or
dangerous-to-remediate rules; every waiver is justified in
@@ -332,8 +370,11 @@ These bit us bringing the build up on Rocky 9.8; they are easy to regress.
**report-only**: it publishes `stig-report.html` + `stig-arf.xml` +
`stig-deviations.md` (generated by `scripts/stig-deviations.sh`) as artifacts
and never fails on score. The scanner tooling is removed from the shipped image
(2d hygiene). `enable_stig=false` is the escape hatch if hardening regresses
the boot/secure-path bench. NEVER let a waiver hide a real finding — anything
(2d hygiene). **`enable_stig` is enforced `true` by Packer variable validation
(POA&M P-03) — a build with it false fails `packer validate`/`build`; there is
no unhardened build path.** If hardening regresses the boot/secure-path bench,
waive the offending rule in the tailoring as a justified deviation (never by
disabling STIG). NEVER let a waiver hide a real finding — anything
non-pass that isn't in the register shows as **REVIEW** in the report.
- **Boot splash toggle**: `boot_splash=true` (default) bakes `rhgb quiet` for the
Plymouth splash; `false` bakes `plymouth.enable=0` for a verbose console boot —