| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188 |
- #!/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 <env> [<output-dir>]
- #
- # Examples:
- # scripts/cert-manager/ca-init.sh dev
- # scripts/cert-manager/ca-init.sh prod /secure/pki
- #
- # Outputs (under <output-dir>/<env>/):
- # 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 <env> [<output-dir>]" >&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" <<EOF
- {
- "env": "$ENV_NAME",
- "generated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
- "root": {
- "cert_pem": "$ROOT_CRT",
- "fingerprint_sha256": "$ROOT_FP",
- "not_after": "$ROOT_NOT_AFTER",
- "path": "$(realpath "$ROOT_CRT")"
- },
- "intermediate": {
- "key_pem": "$INT_KEY",
- "cert_pem": "$INT_CRT",
- "chain_pem": "$INT_CHAIN",
- "fingerprint_sha256": "$INT_FP",
- "serial_hex": "$INT_SERIAL",
- "not_after": "$INT_NOT_AFTER",
- "path": "$(realpath "$INT_CRT")"
- },
- "next_steps": [
- "1. Move $ROOT_KEY and $ROOT_CRT to offline storage. Delete from this machine if not air-gapped.",
- "2. Import $INT_KEY, $INT_CRT, and $ROOT_CRT into a K8s Secret sealed-secrets (or external-secrets).",
- "3. Apply the cert-manager ClusterIssuer manifest (deploy/cert-manager/cluster-issuer-$ENV_NAME.yaml).",
- "4. Issue serving certs: kubectl apply -f deploy/cert-manager/serving-certs.yaml",
- "5. Run scripts/cert-manager/ca-rotate-intermediate.sh in ~364 days to rotate."
- ]
- }
- EOF
- chmod 644 "$OUT/ca-bundle.json"
- echo
- echo "=== done ==="
- echo "files:"
- ls -la "$OUT"
- echo
- echo "rotated-by: $(date -u -d '+365 days' +%Y-%m-%d 2>/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"
|