#!/usr/bin/env bash # server-pki.sh — issue the fleet-manager AGENT-plane server cert from the fleet # CA, and stage the fleet root as the agent client-verify CA. # # The agent plane is gRPC + mTLS: enrolled hosts present their TPM identity cert # (fleet CA), and they verify the MANAGER's server cert against the same fleet # root they hold. So the manager's agent-plane server cert MUST be signed by the # fleet CA — this script issues it via `step ca certificate` against the running # fleet step-ca (serving/dot1x-bench/), the same CA that signs enrolment certs. # # NOTE: where the fleet step-ca is CO-LOCATED on this host, prefer the # `agent-cert-renewer` compose sidecar (docker-compose.yml, profile cert-auto) — # it does this issuance AND auto-renews before the CA's ~90-day cap, preserving # SANs, with no `step` CLI on the host. This script is the manual / remote-CA path. # # This does NOT issue: # - the ADMIN-plane UI cert -> owned by the Traefik ingress (Let's Encrypt # DNS-01); the manager never terminates TLS. See DEPLOY.md §2c. # - the manager's BROKER client cert -> ../enrol-broker/admin-pki.sh (admin CA): # ../enrol-broker/admin-pki.sh ../dot1x-bench/pki/broker fleet-manager # # Usage: server-pki.sh # env overrides: # OUT=./pki/agent output dir # AGENT_SANS=manager.fleet.lab,10.0.0.1 SANs hosts dial (DNS/IP, comma-sep) # FLEET_CA_URL=https://ca.fleet.lab:8443 # FLEET_CA_ROOT=../dot1x-bench/pki/root_ca.crt # STEP_PROVISIONER=enrol # STEP_PROVISIONER_PASSWORD_FILE=../dot1x-bench/pki/stepca/secrets/password # CERT_NOT_AFTER=8760h set -euo pipefail OUT=${OUT:-./pki/agent} AGENT_SANS=${AGENT_SANS:-manager.fleet.lab} FLEET_CA_URL=${FLEET_CA_URL:-https://ca.fleet.lab:8443} FLEET_CA_ROOT=${FLEET_CA_ROOT:-../dot1x-bench/pki/root_ca.crt} FLEET_CA_INTERMEDIATE=${FLEET_CA_INTERMEDIATE:-../dot1x-bench/pki/intermediate_ca.crt} STEP_PROVISIONER=${STEP_PROVISIONER:-enrol} STEP_PROVISIONER_PASSWORD_FILE=${STEP_PROVISIONER_PASSWORD_FILE:-../dot1x-bench/pki/stepca/secrets/password} CERT_NOT_AFTER=${CERT_NOT_AFTER:-8760h} command -v step >/dev/null || { echo "error: 'step' CLI not found (smallstep/cli)"; exit 1; } [ -f "$FLEET_CA_ROOT" ] || { echo "error: fleet root not found at $FLEET_CA_ROOT (bootstrap the fleet CA first)"; exit 1; } [ -f "$STEP_PROVISIONER_PASSWORD_FILE" ] || { echo "error: provisioner password not found at $STEP_PROVISIONER_PASSWORD_FILE"; exit 1; } mkdir -p "$OUT" # First SAN is the CN; every SAN (DNS name / IP) hosts might dial goes in --san. primary=${AGENT_SANS%%,*} san_args=() IFS=',' read -ra sans <<<"$AGENT_SANS" for s in "${sans[@]}"; do san_args+=(--san "$s"); done echo "==> issuing agent-plane server cert (CN=$primary, SANs=$AGENT_SANS)" step ca certificate "$primary" "$OUT/server.crt" "$OUT/server.key" \ --ca-url "$FLEET_CA_URL" \ --root "$FLEET_CA_ROOT" \ --provisioner "$STEP_PROVISIONER" \ --provisioner-password-file "$STEP_PROVISIONER_PASSWORD_FILE" \ --not-after "$CERT_NOT_AFTER" \ --force \ "${san_args[@]}" chmod 600 "$OUT/server.key" # fleet-ca.crt = the agent-plane CLIENT CA that verifies host TPM identity certs. # Host certs are stored LEAF-ONLY in TPM NV and are signed by the step-ca # INTERMEDIATE, so this MUST be root + intermediate. Root-only fails two ways: # (1) the manager can't build leaf->intermediate->root, and (2) it advertises only # the root in its TLS CertificateRequest, so a Go client whose leaf is issued by # the intermediate sends NO cert at all ("remote error: tls: certificate # required"). Prefer an explicit intermediate file; else pull it from the chain # step wrote into the just-issued server.crt (every cert after the leaf). intermediate="" if [ -f "$FLEET_CA_INTERMEDIATE" ]; then intermediate=$(cat "$FLEET_CA_INTERMEDIATE") else intermediate=$(awk 'BEGIN{c=0} /-----BEGIN CERTIFICATE-----/{c++} c>1' "$OUT/server.crt") fi { [ -n "$intermediate" ] && printf '%s\n' "$intermediate" cat "$FLEET_CA_ROOT" } > "$OUT/fleet-ca.crt" if [ -z "$intermediate" ]; then echo "WARNING: no intermediate CA found (set FLEET_CA_INTERMEDIATE, or the CA has none) —" >&2 echo " fleet-ca.crt is ROOT-ONLY; leaf-only host certs issued by an intermediate" >&2 echo " will fail mTLS with 'tls: certificate required'." >&2 fi echo echo "done. staged in $OUT :" echo " server.crt / server.key -> FLEET_MGR_AGENT_CERT / _KEY" echo " fleet-ca.crt -> FLEET_MGR_AGENT_CLIENT_CA (verifies host TPM certs)" echo echo "next:" echo " - admin UI cert : owned by the Traefik ingress (LE DNS-01) — DEPLOY.md §2c" echo " - broker client : ../enrol-broker/admin-pki.sh ../dot1x-bench/pki/broker fleet-manager" echo " then: cp -r ../dot1x-bench/pki/broker ./pki/broker"