adding ssh host signing

This commit is contained in:
Audiobuzz
2026-07-11 16:20:21 +09:30
parent b5d6c790c4
commit 2e318fba00
24 changed files with 1626 additions and 90 deletions
+60 -22
View File
@@ -8,19 +8,24 @@
# shell-local post-processors extract artifacts, build the UKI (ukify), and
# build+push the OCI release image.
#
# Signed when the SB_KEY/SB_CERT secrets are set; otherwise an UNSIGNED (lab)
# build where build-oci.sh emits its loud unsigned-UKI warning. Signing runs
# shell-local on the runner (not in the build VM); the key is staged to
# RUNNER_TEMP and shredded after. See docs/secure-boot-keys.md.
# This is the PRODUCTION build profile and it FAILS CLOSED (POA&M P-03): a
# "Production build gate" step aborts the run unless SB_KEY, SB_CERT and FLEET_CA
# are set — there is no unsigned fallback and no placeholder fleet CA. STIG
# hardening is mandatory (enforced by the Packer template's variable validation).
# Signing runs shell-local on the runner (not in the build VM); the key is staged
# to RUNNER_TEMP and shredded after. See docs/secure-boot-keys.md.
#
# 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 - Secure Boot signing: the `db` PRIVATE key (PEM) + its
# cert (PEM). Empty => unsigned lab build. PK/KEK are NOT
# here (offline/HSM). Generate per docs/secure-boot-keys.md.
# SB_KEY, SB_CERT - REQUIRED. Secure Boot signing: the `db` PRIVATE key (PEM)
# + its cert (PEM). PK/KEK are NOT here (offline/HSM).
# Generate per docs/secure-boot-keys.md. Missing => build fails.
# FLEET_CA - REQUIRED. Real fleet step-ca root PEM baked to
# radius-ca.pem (RADIUS EAP-TLS + fleet-agent verify).
# Missing => build fails.
#
# Runner: needs KVM (qemu accelerator=kvm) and the build-host tooling installed
# below (libguestfs, erofs-utils, cryptsetup, jq, ukify, buildah).
@@ -147,6 +152,36 @@ jobs:
echo "image_version=${version}" >> "$GITHUB_OUTPUT"
echo "Building image_version=${version}"
-
name: Production build gate (fail closed on missing security material)
# POA&M P-03 / F-01 (poam-hardening): the pipeline is the production build
# profile, so it FAILS CLOSED. There is no unsigned fallback and no
# placeholder fleet CA — the three security-critical secrets MUST be set:
# SB_KEY / SB_CERT Secure Boot `db` signing material (signed UKI)
# FLEET_CA real fleet step-ca root baked to radius-ca.pem
# (RADIUS EAP-TLS trust anchor + fleet-agent verify)
# enable_stig is enforced true by the Packer template itself (variable
# validation); it is asserted here too so a misconfigured ENABLE_STIG env
# fails before the ~17-min VM build rather than during it.
env:
SB_KEY: ${{ secrets.SB_KEY }}
SB_CERT: ${{ secrets.SB_CERT }}
FLEET_CA: ${{ secrets.FLEET_CA }}
run: |
miss=0
for v in SB_KEY SB_CERT FLEET_CA; do
if [ -z "${!v}" ]; then
echo "::error::required secret ${v} is empty — production builds are signed with a real fleet CA (no fallback). Set it in the Gitea repo secrets."
miss=1
fi
done
if [ "${ENABLE_STIG}" != "true" ]; then
echo "::error::ENABLE_STIG must be 'true' — production builds are hardening-mandatory (POA&M P-03)."
miss=1
fi
[ "$miss" = 0 ] || exit 1
echo "Production gate passed: SB_KEY, SB_CERT, FLEET_CA present; STIG enabled."
-
name: Install build-host tooling
# Tools the shell-local post-processors run on the RUNNER (not the VM):
@@ -197,7 +232,7 @@ jobs:
go-version: '1.26.4'
-
name: Build host Go binaries out-of-VM (fleet-enrol, fleet-hostapi, fleet-agent)
name: Build host Go binaries out-of-VM (fleet-enrol, fleet-hostapi, fleet-agent, fleet-ssh-hostcert)
# Built HERE on the runner, NOT inside the Packer VM, so the Rocky build
# image never carries the Go toolchain or the go-tpm dep tree. Packer
# file-provisions the resulting static binaries into the rootfs. Runs
@@ -223,7 +258,11 @@ jobs:
&& CGO_ENABLED=0 GOTOOLCHAIN=local \
go build -trimpath -buildvcs=false -ldflags '-s -w -buildid=' -o bin/fleet-agent . \
&& CGO_ENABLED=0 go vet ./... && CGO_ENABLED=0 go test ./... )
file files/enrol/bin/fleet-enrol files/hostapi/bin/fleet-hostapi files/agent/bin/fleet-agent
( cd files/ssh-hostcert \
&& CGO_ENABLED=0 GOTOOLCHAIN=local \
go build -trimpath -buildvcs=false -ldflags '-s -w -buildid=' -o bin/fleet-ssh-hostcert . \
&& CGO_ENABLED=0 go vet ./... && CGO_ENABLED=0 go test ./... )
file files/enrol/bin/fleet-enrol files/hostapi/bin/fleet-hostapi files/agent/bin/fleet-agent files/ssh-hostcert/bin/fleet-ssh-hostcert
-
name: TPM ceremony + agent mTLS tests (go-tpm software simulator)
@@ -265,9 +304,8 @@ jobs:
# signing post-processor runs shell-local on THIS runner (not in the build
# VM), so write the secrets to runner-temp files and pass their paths. The
# `db` PRIVATE key is the only signing key here; PK/KEK stay offline (see
# docs/secure-boot-keys.md). If SB_KEY is unset, this no-ops and the build
# falls back to an UNSIGNED lab UKI (the loud warning stays).
if: ${{ secrets.SB_KEY != '' }}
# docs/secure-boot-keys.md). Presence is guaranteed by the Production build
# gate above (fail-closed) — there is no unsigned fallback.
env:
SB_KEY: ${{ secrets.SB_KEY }} # db private key (PEM) — secret
SB_CERT: ${{ secrets.SB_CERT }} # db cert (PEM) — public, kept as a secret for convenience
@@ -284,10 +322,9 @@ jobs:
name: Materialize fleet CA (radius-ca.pem trust anchor)
# The real fleet step-ca root is baked to /etc/dot1x/radius-ca.pem (RADIUS
# EAP-TLS + fleet-agent server-verify). Written to a runner-temp FILE and
# passed as a build-host PATH (var.fleet_ca). If FLEET_CA is unset this
# no-ops and the build falls back to the committed PLACEHOLDER (auth cannot
# succeed — lab only). Never commit real internal CA material (CLAUDE.md).
if: ${{ secrets.FLEET_CA != '' }}
# passed as a build-host PATH (var.fleet_ca). Presence is guaranteed by the
# Production build gate above (fail-closed) — the committed placeholder is
# NOT a usable fallback here. Never commit real internal CA material (CLAUDE.md).
env:
FLEET_CA: ${{ secrets.FLEET_CA }} # fleet step-ca root PEM (root, or root+intermediate)
run: |
@@ -318,9 +355,10 @@ jobs:
-
name: packer build (UKI + OCI release image)
# Signed when SB_KEY/SB_CERT secrets are set (paths exported above);
# UNSIGNED lab build (loud warning) when they're empty. sb_key/sb_cert are
# PATHS, never values — see the materialize step and docs/secure-boot-keys.md.
# Always a SIGNED build: SB_KEY/SB_CERT/FLEET_CA presence is enforced by the
# Production build gate, and enable_stig=true is enforced by the template's
# variable validation. sb_key/sb_cert are PATHS, never values — see the
# materialize step and docs/secure-boot-keys.md.
run: |
packer build -color=false -on-error=abort \
-var image_version="${{ steps.vars.outputs.image_version }}" \
@@ -330,9 +368,9 @@ jobs:
-var disable_dot1x=${{ env.DISABLE_DOT1X }} \
-var nvmet_traddr="${{ env.NVMET_TRADDR }}" \
-var ssh_authorized_key="${{ secrets.SSH_AUTHORIZED_KEY }}" \
-var sb_key="${SB_KEY_FILE:-}" \
-var sb_cert="${SB_CERT_FILE:-}" \
-var fleet_ca="${FLEET_CA_FILE:-files/etc/dot1x/radius-ca.pem}" \
-var sb_key="${SB_KEY_FILE}" \
-var sb_cert="${SB_CERT_FILE}" \
-var fleet_ca="${FLEET_CA_FILE}" \
-var kiosk_url="${{ env.KIOSK_URL }}" \
-var boot_splash=${{ env.BOOT_SPLASH }} \
-var enable_stig=${{ env.ENABLE_STIG }} \
+28 -8
View File
@@ -22,9 +22,10 @@
#
# Runs only when the iPXE inputs change (path filter) or on demand.
#
# Required Gitea repo secrets: PACKAGE_USER, PACKAGE_TOKEN (registry creds).
# Optional: SB_KEY, SB_CERT (db PRIVATE key + cert, PEM). Unset => UNSIGNED lab
# image (loud warning; Secure-Boot-enforcing hosts will reject it). PK/KEK
# Required Gitea repo secrets: PACKAGE_USER, PACKAGE_TOKEN (registry creds),
# SB_KEY, SB_CERT (db PRIVATE key + cert, PEM). SB_KEY/SB_CERT are REQUIRED —
# a "Production build gate" fails the run if either is unset (no unsigned
# fallback; POA&M P-03). Enforcing hosts reject an unsigned snponly.efi. PK/KEK
# never come near CI — only `db` signs (docs/secure-boot-keys.md).
#
@@ -104,12 +105,31 @@ jobs:
echo "push_latest=$push_latest" >> "$GITHUB_OUTPUT"
echo "Using snponly.efi=$SNP tag=$VER (ipxe pkg $RAW, latest=$push_latest)"
-
name: Production build gate (fail closed on missing signing material)
# POA&M P-03 (poam-hardening): the signed iPXE bootloader is a production
# Secure-Boot artifact — an unsigned snponly.efi is rejected by enforcing
# hosts and defeats the chain of trust. SB_KEY/SB_CERT MUST be set; there
# is no unsigned fallback. (FLEET_CA is N/A — iPXE carries no fleet CA.)
env:
SB_KEY: ${{ secrets.SB_KEY }}
SB_CERT: ${{ secrets.SB_CERT }}
run: |
miss=0
for v in SB_KEY SB_CERT; do
if [ -z "${!v}" ]; then
echo "::error::required secret ${v} is empty — the fleet-ipxe bootloader must be signed (no unsigned fallback). Set it in the Gitea repo secrets."
miss=1
fi
done
[ "$miss" = 0 ] || exit 1
echo "Production gate passed: SB_KEY, SB_CERT present."
-
name: Materialize Secure Boot signing material
# Same db key that signs the UKI; written to a runner-temp file and passed
# to build-ipxe-oci.sh as a path. If SB_KEY is unset this no-ops and the
# build produces an UNSIGNED (lab) image with its loud warning.
if: ${{ secrets.SB_KEY != '' }}
# to build-ipxe-oci.sh as a path. Presence is guaranteed by the Production
# build gate above (fail-closed) — there is no unsigned fallback.
env:
SB_KEY: ${{ secrets.SB_KEY }}
SB_CERT: ${{ secrets.SB_CERT }}
@@ -140,8 +160,8 @@ jobs:
"${{ steps.src.outputs.snponly }}" \
"${IPXE_IMAGE}" \
"$VER" \
"${SB_KEY_FILE:-}" \
"${SB_CERT_FILE:-}" \
"${SB_KEY_FILE}" \
"${SB_CERT_FILE}" \
true
# main / release tags also move :latest; dev branches never clobber it.
if [ "${{ steps.src.outputs.push_latest }}" = "true" ]; then