diff --git a/.gitea/workflows/gitea-main-ci.yaml b/.gitea/workflows/gitea-main-ci.yaml index fadcf82..47af6f3 100644 --- a/.gitea/workflows/gitea-main-ci.yaml +++ b/.gitea/workflows/gitea-main-ci.yaml @@ -15,6 +15,8 @@ # Required Gitea repo secrets: # REGISTRY_USERNAME - registry user for `oci_image` (oci_push=true) # REGISTRY_TOKEN - registry password/token +# SSH_AUTHORIZED_KEY - public key for the sudo admin user; empty => no admin +# user + root left unlocked (lab). Set it to lock root. # # SB_KEY, SB_CERT - (future) Secure Boot signing key + cert for signed UKIs # # Runner: needs KVM (qemu accelerator=kvm) and the build-host tooling installed @@ -153,4 +155,5 @@ jobs: -var oci_push=true \ -var disable_dot1x=${{ env.DISABLE_DOT1X }} \ -var nvmet_traddr="${{ env.NVMET_TRADDR }}" \ + -var ssh_authorized_key="${{ secrets.SSH_AUTHORIZED_KEY }}" \ . diff --git a/diskless.pkr.hcl b/diskless.pkr.hcl index eb68350..caa24a1 100644 --- a/diskless.pkr.hcl +++ b/diskless.pkr.hcl @@ -100,6 +100,18 @@ variable "nvmet_traddr" { default = "nvme.fleet.lab" } +variable "ssh_authorized_key" { + type = string + description = "Single SSH public key authorized for the sudo-enabled admin user. Fleet-generic (same on every host — not per-host state). Baked into the immutable rootfs, so rotating it needs a rebuild. EMPTY = no admin user is created and root is left UNLOCKED (lab convenience, avoids a no-way-in image)." + default = "" +} + +variable "admin_user" { + type = string + description = "Username of the sudo-enabled, key-only admin account baked into the image (wheel group, NOPASSWD sudo since the account has no password). Created only when ssh_authorized_key is set." + default = "fleetadmin" +} + variable "sb_key" { type = string description = "Secure Boot signing key (PEM) for the UKI. Empty = unsigned lab build. Production: CI/HSM-held; never enters the build VM." @@ -232,6 +244,46 @@ build { ] } + # --- 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 + # skip the whole thing AND leave root unlocked, so a lab build can't end up + # with no way in. Runs LAST: locking root is safe because Packer drives + # shutdown over the already-authenticated SSH session (no re-auth as root). + provisioner "shell" { + environment_vars = [ + "SSH_AUTHORIZED_KEY=${var.ssh_authorized_key}", + "ADMIN_USER=${var.admin_user}", + ] + inline = [ + "set -e", + "if [ -z \"$SSH_AUTHORIZED_KEY\" ]; then echo 'WARNING: ssh_authorized_key empty -> not creating admin user, leaving root UNLOCKED (lab build).'; exit 0; fi", + "id \"$ADMIN_USER\" >/dev/null 2>&1 || useradd -m -G wheel \"$ADMIN_USER\"", + "install -d -m0700 -o \"$ADMIN_USER\" -g \"$ADMIN_USER\" \"/home/$ADMIN_USER/.ssh\"", + "printf '%s\\n' \"$SSH_AUTHORIZED_KEY\" > \"/home/$ADMIN_USER/.ssh/authorized_keys\"", + "chmod 0600 \"/home/$ADMIN_USER/.ssh/authorized_keys\"", + "chown \"$ADMIN_USER:$ADMIN_USER\" \"/home/$ADMIN_USER/.ssh/authorized_keys\"", + "restorecon -R \"/home/$ADMIN_USER/.ssh\"", + # Key-only account (useradd leaves no password), so sudo must not prompt + # for one. NOPASSWD is a STIG finding (tracked in TODO #2) — acceptable for + # a key-only admin; reconcile when hardening lands. + "printf '%s ALL=(ALL) NOPASSWD:ALL\\n' \"$ADMIN_USER\" > \"/etc/sudoers.d/$ADMIN_USER\"", + "chmod 0440 \"/etc/sudoers.d/$ADMIN_USER\"", + "visudo -cf \"/etc/sudoers.d/$ADMIN_USER\"", + "restorecon \"/etc/sudoers.d/$ADMIN_USER\"", + # Image sshd: no root login, key-only. A drop-in (read via Include before + # the main config) overrides the ks's PermitRootLogin yes. Takes effect on + # the next boot only (sshd isn't restarted), so the live build session is + # unaffected. + "printf 'PermitRootLogin no\\nPasswordAuthentication no\\nKbdInteractiveAuthentication no\\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", + # Lock root in the image. Done last; admin access is via the key user. + "passwd -l root", + "echo \"root locked; admin user $ADMIN_USER created (wheel + NOPASSWD sudo, key-only).\"", + ] + } + # --- 3. Extract kernel, initramfs, rootfs from the built disk ------------ # --- 4. Assemble + sign the UKI; build the OCI release-artifact image ---- post-processor "shell-local" { diff --git a/docs/TODO.md b/docs/TODO.md index 94e758e..9ccc58d 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -56,6 +56,14 @@ and cleanup. report-only. - Note: orthogonal to the `rd.dot1x=0` test toggle, but a full STIG production posture wants 802.1X re-enabled. +- **Credentials (partly done):** root is now **locked** and access is via a + baked sudo-enabled key-only admin user (`var.ssh_authorized_key` / + `var.admin_user`, `wheel` + NOPASSWD sudo), with sshd drop-in + `PermitRootLogin no` / `PasswordAuthentication no`. Two items for STIG to + reconcile: **NOPASSWD sudo** is a finding (acceptable for a key-only account, + but document/deviate), and the admin key/user are **fleet-wide baked** — for + per-host/rotatable access without rebuilds, layer the signed HTTP/SMBIOS + config-agent or an SSH CA (`TrustedUserCAKeys`) discussed in session. ## [ ] 3. Roll the errata into design doc v3