#!/usr/bin/env bash # ca-init.sh — Generate the broad-announce internal PKI: an offline root # CA and an intermediate CA ready to be imported into cert-manager as a # ClusterIssuer. Run once per environment (dev, staging, prod). The # root key/cert stay offline; only the intermediate is imported into K8s. # # Usage: # scripts/cert-manager/ca-init.sh [] # # Examples: # scripts/cert-manager/ca-init.sh dev # scripts/cert-manager/ca-init.sh prod /secure/pki # # Outputs (under //): # root-ca.key — root CA private key (4096-bit RSA). KEEP OFFLINE. # root-ca.crt — root CA certificate (10y) # intermediate-ca.key — intermediate CA private key (ECDSA P-256). Goes into K8s. # intermediate-ca.crt — intermediate CA certificate (1y, signed by root) # intermediate-ca-chain.pem — intermediate + root, for serving cert chain # ca-bundle.json — metadata for cert-manager (k8s secret manifest + JSON) # # Security: # - The root key is the trust anchor. If it leaks, the entire PKI is # compromised. Generate it on an air-gapped machine or encrypted USB. # - The intermediate key is what cert-manager uses. It can be in K8s # but should be sealed-secrets / external-secrets encrypted at rest. # - This script prompts for an AES-256 passphrase to encrypt both # private keys on disk. Use a password manager. Do NOT commit. # # Requires: openssl 3.x, jq (for ca-bundle.json). set -euo pipefail ENV_NAME="${1:-}" OUTPUT_DIR="${2:-./pki}" if [[ -z "$ENV_NAME" ]]; then echo "usage: $0 []" >&2 echo " env: dev, staging, prod (or any tag you want to distinguish)" >&2 exit 1 fi # Pre-flight for bin in openssl jq; do if ! command -v "$bin" >/dev/null 2>&1; then echo "FATAL: $bin not found in PATH" >&2 exit 1 fi done OUT="$OUTPUT_DIR/$ENV_NAME" mkdir -p "$OUT" chmod 700 "$OUT" echo "=== broad-announce CA init ===" echo "env: $ENV_NAME" echo "output dir: $OUT" echo echo "WARNING: the root key generated here is the trust anchor for the" echo "entire PKI. Store it offline (encrypted USB, air-gapped machine)." echo "Anyone with this key can mint certificates your systems will trust." echo # --- Root CA (offline) ----------------------------------------------------- ROOT_KEY="$OUT/root-ca.key" ROOT_CRT="$OUT/root-ca.crt" if [[ -f "$ROOT_KEY" && -f "$ROOT_CRT" ]]; then echo "Root CA already exists at $OUT — refusing to overwrite." echo "Move/delete the existing dir first, or pass a fresh output dir." exit 1 fi echo "[1/4] Generating root CA (RSA 4096, 10y, AES-256 encrypted on disk)..." # Passphrase strategy: # - If BA_CA_PASSPHRASE is set (env), use it. This is the dev/test path # and the CI path. NEVER set it in prod — use the interactive prompt # below or load it from a hardware token. # - If unset, openssl will prompt interactively (the correct prod path). if [[ -n "${BA_CA_PASSPHRASE:-}" ]]; then openssl genrsa -aes-256-cbc -passout "env:BA_CA_PASSPHRASE" -out "$ROOT_KEY" 4096 2>/dev/null else openssl genrsa -aes-256-cbc -out "$ROOT_KEY" 4096 fi chmod 600 "$ROOT_KEY" if [[ -n "${BA_CA_PASSPHRASE:-}" ]]; then ROOT_PASS_ARGS=(-passin "env:BA_CA_PASSPHRASE") else ROOT_PASS_ARGS=() fi openssl req -new -x509 "${ROOT_PASS_ARGS[@]}" -key "$ROOT_KEY" -sha384 -days 3650 \ -subj "/CN=broad-announce Root CA ($ENV_NAME)/O=broad-announce/OU=PKI" \ -addext "basicConstraints=critical,CA:TRUE" \ -addext "keyUsage=critical,keyCertSign,cRLSign" \ -addext "subjectKeyIdentifier=hash" \ -out "$ROOT_CRT" chmod 644 "$ROOT_CRT" # --- Intermediate CA (in-cluster) ------------------------------------------ INT_KEY="$OUT/intermediate-ca.key" INT_CRT="$OUT/intermediate-ca.crt" INT_CSR="$OUT/intermediate-ca.csr" INT_CHAIN="$OUT/intermediate-ca-chain.pem" echo "[2/4] Generating intermediate CA (ECDSA P-256, 1y)..." openssl ecparam -name prime256v1 -genkey -noout -out "$INT_KEY" chmod 600 "$INT_KEY" openssl req -new -sha256 -key "$INT_KEY" \ -subj "/CN=broad-announce Intermediate CA ($ENV_NAME)/O=broad-announce/OU=PKI" \ -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \ -addext "keyUsage=critical,keyCertSign,cRLSign,digitalSignature" \ -addext "extendedKeyUsage=serverAuth,clientAuth" \ -addext "subjectKeyIdentifier=hash" \ -out "$INT_CSR" echo "[3/4] Signing intermediate with root (1y validity)..." openssl x509 -req -in "$INT_CSR" -CA "$ROOT_CRT" -CAkey "$ROOT_KEY" "${ROOT_PASS_ARGS[@]}" \ -CAcreateserial -sha384 -days 365 \ -extfile <(cat <<'EOF' basicConstraints=critical,CA:TRUE,pathlen:0 keyUsage=critical,keyCertSign,cRLSign,digitalSignature extendedKeyUsage=serverAuth,clientAuth subjectKeyIdentifier=hash authorityKeyIdentifier=keyid:always EOF ) \ -out "$INT_CRT" chmod 644 "$INT_CRT" # Bundle: intermediate + root, for serving cert chain validation cat "$INT_CRT" "$ROOT_CRT" > "$INT_CHAIN" chmod 644 "$INT_CHAIN" # --- Metadata for cert-manager --------------------------------------------- echo "[4/4] Writing ca-bundle.json for cert-manager import..." ROOT_FP=$(openssl x509 -in "$ROOT_CRT" -noout -fingerprint -sha256 | cut -d'=' -f2) INT_FP=$(openssl x509 -in "$INT_CRT" -noout -fingerprint -sha256 | cut -d'=' -f2) INT_SERIAL=$(openssl x509 -in "$INT_CRT" -noout -serial | cut -d'=' -f2) INT_NOT_AFTER=$(openssl x509 -in "$INT_CRT" -noout -enddate | cut -d'=' -f2) ROOT_NOT_AFTER=$(openssl x509 -in "$ROOT_CRT" -noout -enddate | cut -d'=' -f2) cat > "$OUT/ca-bundle.json" </dev/null || date -u -v+365d +%Y-%m-%d)" echo "ROOT FINGERPRINT (verify before trusting): $ROOT_FP" echo "INTERMEDIATE FINGERPRINT: $INT_FP" echo echo "next: import the intermediate into cert-manager — see ca-bundle.json"