ca-init.sh 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. #!/usr/bin/env bash
  2. # ca-init.sh — Generate the broad-announce internal PKI: an offline root
  3. # CA and an intermediate CA ready to be imported into cert-manager as a
  4. # ClusterIssuer. Run once per environment (dev, staging, prod). The
  5. # root key/cert stay offline; only the intermediate is imported into K8s.
  6. #
  7. # Usage:
  8. # scripts/cert-manager/ca-init.sh <env> [<output-dir>]
  9. #
  10. # Examples:
  11. # scripts/cert-manager/ca-init.sh dev
  12. # scripts/cert-manager/ca-init.sh prod /secure/pki
  13. #
  14. # Outputs (under <output-dir>/<env>/):
  15. # root-ca.key — root CA private key (4096-bit RSA). KEEP OFFLINE.
  16. # root-ca.crt — root CA certificate (10y)
  17. # intermediate-ca.key — intermediate CA private key (ECDSA P-256). Goes into K8s.
  18. # intermediate-ca.crt — intermediate CA certificate (1y, signed by root)
  19. # intermediate-ca-chain.pem — intermediate + root, for serving cert chain
  20. # ca-bundle.json — metadata for cert-manager (k8s secret manifest + JSON)
  21. #
  22. # Security:
  23. # - The root key is the trust anchor. If it leaks, the entire PKI is
  24. # compromised. Generate it on an air-gapped machine or encrypted USB.
  25. # - The intermediate key is what cert-manager uses. It can be in K8s
  26. # but should be sealed-secrets / external-secrets encrypted at rest.
  27. # - This script prompts for an AES-256 passphrase to encrypt both
  28. # private keys on disk. Use a password manager. Do NOT commit.
  29. #
  30. # Requires: openssl 3.x, jq (for ca-bundle.json).
  31. set -euo pipefail
  32. ENV_NAME="${1:-}"
  33. OUTPUT_DIR="${2:-./pki}"
  34. if [[ -z "$ENV_NAME" ]]; then
  35. echo "usage: $0 <env> [<output-dir>]" >&2
  36. echo " env: dev, staging, prod (or any tag you want to distinguish)" >&2
  37. exit 1
  38. fi
  39. # Pre-flight
  40. for bin in openssl jq; do
  41. if ! command -v "$bin" >/dev/null 2>&1; then
  42. echo "FATAL: $bin not found in PATH" >&2
  43. exit 1
  44. fi
  45. done
  46. OUT="$OUTPUT_DIR/$ENV_NAME"
  47. mkdir -p "$OUT"
  48. chmod 700 "$OUT"
  49. echo "=== broad-announce CA init ==="
  50. echo "env: $ENV_NAME"
  51. echo "output dir: $OUT"
  52. echo
  53. echo "WARNING: the root key generated here is the trust anchor for the"
  54. echo "entire PKI. Store it offline (encrypted USB, air-gapped machine)."
  55. echo "Anyone with this key can mint certificates your systems will trust."
  56. echo
  57. # --- Root CA (offline) -----------------------------------------------------
  58. ROOT_KEY="$OUT/root-ca.key"
  59. ROOT_CRT="$OUT/root-ca.crt"
  60. if [[ -f "$ROOT_KEY" && -f "$ROOT_CRT" ]]; then
  61. echo "Root CA already exists at $OUT — refusing to overwrite."
  62. echo "Move/delete the existing dir first, or pass a fresh output dir."
  63. exit 1
  64. fi
  65. echo "[1/4] Generating root CA (RSA 4096, 10y, AES-256 encrypted on disk)..."
  66. # Passphrase strategy:
  67. # - If BA_CA_PASSPHRASE is set (env), use it. This is the dev/test path
  68. # and the CI path. NEVER set it in prod — use the interactive prompt
  69. # below or load it from a hardware token.
  70. # - If unset, openssl will prompt interactively (the correct prod path).
  71. if [[ -n "${BA_CA_PASSPHRASE:-}" ]]; then
  72. openssl genrsa -aes-256-cbc -passout "env:BA_CA_PASSPHRASE" -out "$ROOT_KEY" 4096 2>/dev/null
  73. else
  74. openssl genrsa -aes-256-cbc -out "$ROOT_KEY" 4096
  75. fi
  76. chmod 600 "$ROOT_KEY"
  77. if [[ -n "${BA_CA_PASSPHRASE:-}" ]]; then
  78. ROOT_PASS_ARGS=(-passin "env:BA_CA_PASSPHRASE")
  79. else
  80. ROOT_PASS_ARGS=()
  81. fi
  82. openssl req -new -x509 "${ROOT_PASS_ARGS[@]}" -key "$ROOT_KEY" -sha384 -days 3650 \
  83. -subj "/CN=broad-announce Root CA ($ENV_NAME)/O=broad-announce/OU=PKI" \
  84. -addext "basicConstraints=critical,CA:TRUE" \
  85. -addext "keyUsage=critical,keyCertSign,cRLSign" \
  86. -addext "subjectKeyIdentifier=hash" \
  87. -out "$ROOT_CRT"
  88. chmod 644 "$ROOT_CRT"
  89. # --- Intermediate CA (in-cluster) ------------------------------------------
  90. INT_KEY="$OUT/intermediate-ca.key"
  91. INT_CRT="$OUT/intermediate-ca.crt"
  92. INT_CSR="$OUT/intermediate-ca.csr"
  93. INT_CHAIN="$OUT/intermediate-ca-chain.pem"
  94. echo "[2/4] Generating intermediate CA (ECDSA P-256, 1y)..."
  95. openssl ecparam -name prime256v1 -genkey -noout -out "$INT_KEY"
  96. chmod 600 "$INT_KEY"
  97. openssl req -new -sha256 -key "$INT_KEY" \
  98. -subj "/CN=broad-announce Intermediate CA ($ENV_NAME)/O=broad-announce/OU=PKI" \
  99. -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
  100. -addext "keyUsage=critical,keyCertSign,cRLSign,digitalSignature" \
  101. -addext "extendedKeyUsage=serverAuth,clientAuth" \
  102. -addext "subjectKeyIdentifier=hash" \
  103. -out "$INT_CSR"
  104. echo "[3/4] Signing intermediate with root (1y validity)..."
  105. openssl x509 -req -in "$INT_CSR" -CA "$ROOT_CRT" -CAkey "$ROOT_KEY" "${ROOT_PASS_ARGS[@]}" \
  106. -CAcreateserial -sha384 -days 365 \
  107. -extfile <(cat <<'EOF'
  108. basicConstraints=critical,CA:TRUE,pathlen:0
  109. keyUsage=critical,keyCertSign,cRLSign,digitalSignature
  110. extendedKeyUsage=serverAuth,clientAuth
  111. subjectKeyIdentifier=hash
  112. authorityKeyIdentifier=keyid:always
  113. EOF
  114. ) \
  115. -out "$INT_CRT"
  116. chmod 644 "$INT_CRT"
  117. # Bundle: intermediate + root, for serving cert chain validation
  118. cat "$INT_CRT" "$ROOT_CRT" > "$INT_CHAIN"
  119. chmod 644 "$INT_CHAIN"
  120. # --- Metadata for cert-manager ---------------------------------------------
  121. echo "[4/4] Writing ca-bundle.json for cert-manager import..."
  122. ROOT_FP=$(openssl x509 -in "$ROOT_CRT" -noout -fingerprint -sha256 | cut -d'=' -f2)
  123. INT_FP=$(openssl x509 -in "$INT_CRT" -noout -fingerprint -sha256 | cut -d'=' -f2)
  124. INT_SERIAL=$(openssl x509 -in "$INT_CRT" -noout -serial | cut -d'=' -f2)
  125. INT_NOT_AFTER=$(openssl x509 -in "$INT_CRT" -noout -enddate | cut -d'=' -f2)
  126. ROOT_NOT_AFTER=$(openssl x509 -in "$ROOT_CRT" -noout -enddate | cut -d'=' -f2)
  127. cat > "$OUT/ca-bundle.json" <<EOF
  128. {
  129. "env": "$ENV_NAME",
  130. "generated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
  131. "root": {
  132. "cert_pem": "$ROOT_CRT",
  133. "fingerprint_sha256": "$ROOT_FP",
  134. "not_after": "$ROOT_NOT_AFTER",
  135. "path": "$(realpath "$ROOT_CRT")"
  136. },
  137. "intermediate": {
  138. "key_pem": "$INT_KEY",
  139. "cert_pem": "$INT_CRT",
  140. "chain_pem": "$INT_CHAIN",
  141. "fingerprint_sha256": "$INT_FP",
  142. "serial_hex": "$INT_SERIAL",
  143. "not_after": "$INT_NOT_AFTER",
  144. "path": "$(realpath "$INT_CRT")"
  145. },
  146. "next_steps": [
  147. "1. Move $ROOT_KEY and $ROOT_CRT to offline storage. Delete from this machine if not air-gapped.",
  148. "2. Import $INT_KEY, $INT_CRT, and $ROOT_CRT into a K8s Secret sealed-secrets (or external-secrets).",
  149. "3. Apply the cert-manager ClusterIssuer manifest (deploy/cert-manager/cluster-issuer-$ENV_NAME.yaml).",
  150. "4. Issue serving certs: kubectl apply -f deploy/cert-manager/serving-certs.yaml",
  151. "5. Run scripts/cert-manager/ca-rotate-intermediate.sh in ~364 days to rotate."
  152. ]
  153. }
  154. EOF
  155. chmod 644 "$OUT/ca-bundle.json"
  156. echo
  157. echo "=== done ==="
  158. echo "files:"
  159. ls -la "$OUT"
  160. echo
  161. echo "rotated-by: $(date -u -d '+365 days' +%Y-%m-%d 2>/dev/null || date -u -v+365d +%Y-%m-%d)"
  162. echo "ROOT FINGERPRINT (verify before trusting): $ROOT_FP"
  163. echo "INTERMEDIATE FINGERPRINT: $INT_FP"
  164. echo
  165. echo "next: import the intermediate into cert-manager — see ca-bundle.json"