verae-staff-iam/scripts/lab-test-ca.sh
George Lambert ca117eb458 feat: LAB_CA_ENABLE opt-in lab mint via lab-test-ca.sh (prod refused)
POST /enroll/csr returns 201 + X-Lab-CA when lab-pki ready; default 501.
2026-10-14 01:08:21 -04:00

278 lines
8.1 KiB
Shell
Executable file

#!/usr/bin/env bash
# lab-test-ca.sh — OpenSSL lab CA for local/dev experiments ONLY.
#
# Creates a self-signed lab root CA and signs a PKCS#10 CSR → leaf PEM.
# Material stays on disk under a lab directory (default lab-pki/, gitignored).
#
# Optional HTTP path (LAB ONLY): when staff-iam is started with LAB_CA_ENABLE=1
# AND this directory has been initialized, POST /enroll/csr may shell-out to
# `sign` and return 201 + cert PEM with header X-Lab-CA: 1.
# Default (LAB_CA_ENABLE unset/off) → POST /enroll/csr stays 501.
# NODE_ENV=production always refuses the lab HTTP path.
#
# NOT FOR PRODUCTION. Do not trust this root on real workstations,
# staging staff fleets, or prod reverse proxies.
#
# Usage:
# scripts/lab-test-ca.sh init [--dir DIR]
# scripts/lab-test-ca.sh sign --csr PATH [--dir DIR] [--out PATH] [--days N] [--cn CN]
# scripts/lab-test-ca.sh warn
#
# Defaults: DIR=./lab-pki (gitignored). Requires openssl on PATH.
set -euo pipefail
SCRIPT_NAME="$(basename "$0")"
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
DEFAULT_DIR="${ROOT_DIR}/lab-pki"
DAYS_DEFAULT=7
warn_banner() {
cat >&2 <<'EOF'
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
LAB ONLY — verae-staff-iam scripts/lab-test-ca.sh
Self-signed throwaway CA. NOT for production, staging staff, or
any reverse-proxy trust store that faces real operators.
Opt-in HTTP: LAB_CA_ENABLE=1 + init'd lab-pki → POST /enroll/csr may
shell-out here (201 + X-Lab-CA:1). Default off → 501 scaffold.
NODE_ENV=production refuses lab HTTP. NOT a production CA.
Do NOT ship lab-pki/ keys in images or commit private keys.
Live production mint stays on the product backlog (docs/PKI-MINT-BACKLOG.md).
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
EOF
}
usage() {
cat <<EOF
Usage:
$SCRIPT_NAME init [--dir DIR]
Create lab root CA key + self-signed cert under DIR (default: lab-pki/).
$SCRIPT_NAME sign --csr PATH [--dir DIR] [--out PATH] [--days N] [--cn CN]
Sign a PEM CSR with the lab CA → leaf certificate PEM.
--out defaults to DIR/leaf-<timestamp>.crt.pem
--days leaf notAfter (default ${DAYS_DEFAULT}, max 30 for lab)
--cn optional subject CN override in the issued cert (lab only)
$SCRIPT_NAME warn
Print the lab-only warning and exit 0.
Environment:
LAB_PKI_DIR Override default DIR when --dir omitted.
Exit codes: 0 ok · 1 usage/input · 2 missing openssl/CA · 3 sign failure
EOF
}
need_openssl() {
if ! command -v openssl >/dev/null 2>&1; then
echo "error: openssl not found on PATH" >&2
exit 2
fi
}
resolve_dir() {
local d="${1:-}"
if [[ -z "$d" ]]; then
d="${LAB_PKI_DIR:-$DEFAULT_DIR}"
fi
# Absolute path
if [[ "$d" != /* ]]; then
d="$(pwd)/$d"
fi
echo "$d"
}
cmd_init() {
local dir=""
while [[ $# -gt 0 ]]; do
case "$1" in
--dir) dir="${2:-}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "error: unknown init arg: $1" >&2; usage >&2; exit 1 ;;
esac
done
need_openssl
warn_banner
dir="$(resolve_dir "$dir")"
mkdir -p "$dir"
chmod 700 "$dir" 2>/dev/null || true
local key="$dir/lab-root.key.pem"
local crt="$dir/lab-root.crt.pem"
local conf="$dir/lab-root.cnf"
local serial="$dir/lab-root.srl"
if [[ -f "$key" || -f "$crt" ]]; then
echo "error: lab CA already exists under $dir (refusing to overwrite)" >&2
echo " remove lab-root.key.pem / lab-root.crt.pem to re-init" >&2
exit 1
fi
cat >"$conf" <<'EOF'
[req]
distinguished_name = dn
x509_extensions = v3_ca
prompt = no
[dn]
CN = Verae staff-iam LAB root (NOT FOR PRODUCTION)
O = Verae Lab Only
OU = throwaway-test-ca
[v3_ca]
basicConstraints = critical,CA:true,pathlen:0
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
EOF
openssl genrsa -out "$key" 4096 2>/dev/null
chmod 600 "$key"
openssl req -new -x509 -days 3650 -key "$key" -out "$crt" -config "$conf"
# Fresh serial file for leaf issuance
openssl rand -hex 8 | tr '[:lower:]' '[:upper:]' >"$serial"
chmod 600 "$serial" 2>/dev/null || true
cat >"$dir/README.txt" <<EOF
LAB PKI material — NOT FOR PRODUCTION
Generated by scripts/lab-test-ca.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ)
Root cert: lab-root.crt.pem (public)
Root key: lab-root.key.pem (PRIVATE — never commit, never production trust)
With LAB_CA_ENABLE=1 the staff-iam server may shell-out sign against this dir.
Default (flag off) POST /enroll/csr stays 501. NEVER production.
See docs/TEST-CA-DESIGN.md, ENROLL.md, docs/PKI-MINT-BACKLOG.md.
EOF
echo "lab CA created:"
echo " dir: $dir"
echo " cert: $crt"
echo " key: $key (keep private)"
openssl x509 -in "$crt" -noout -subject -dates 2>/dev/null || true
warn_banner
}
cmd_sign() {
local dir="" csr="" out="" days="$DAYS_DEFAULT" cn=""
while [[ $# -gt 0 ]]; do
case "$1" in
--dir) dir="${2:-}"; shift 2 ;;
--csr) csr="${2:-}"; shift 2 ;;
--out) out="${2:-}"; shift 2 ;;
--days) days="${2:-}"; shift 2 ;;
--cn) cn="${2:-}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "error: unknown sign arg: $1" >&2; usage >&2; exit 1 ;;
esac
done
if [[ -z "$csr" ]]; then
echo "error: --csr PATH is required" >&2
usage >&2
exit 1
fi
if [[ ! -f "$csr" ]]; then
echo "error: CSR not found: $csr" >&2
exit 1
fi
if ! [[ "$days" =~ ^[0-9]+$ ]] || [[ "$days" -lt 1 || "$days" -gt 30 ]]; then
echo "error: --days must be 1..30 (lab short-lived leaves)" >&2
exit 1
fi
need_openssl
warn_banner
dir="$(resolve_dir "$dir")"
local key="$dir/lab-root.key.pem"
local ca="$dir/lab-root.crt.pem"
local serial="$dir/lab-root.srl"
local ext="$dir/lab-leaf-ext.cnf"
if [[ ! -f "$key" || ! -f "$ca" ]]; then
echo "error: lab CA missing under $dir — run: $SCRIPT_NAME init --dir $dir" >&2
exit 2
fi
if [[ ! -f "$serial" ]]; then
openssl rand -hex 8 | tr '[:lower:]' '[:upper:]' >"$serial"
fi
# Light shape check (headers only); openssl does the real parse on sign.
if ! grep -q "BEGIN CERTIFICATE REQUEST" "$csr" || ! grep -q "END CERTIFICATE REQUEST" "$csr"; then
echo "error: CSR must be PEM PKCS#10 (BEGIN/END CERTIFICATE REQUEST)" >&2
exit 1
fi
if [[ -z "$out" ]]; then
out="$dir/leaf-$(date -u +%Y%m%dT%H%M%SZ).crt.pem"
fi
cat >"$ext" <<EOF
basicConstraints = critical,CA:false
keyUsage = critical,digitalSignature,keyEncipherment
extendedKeyUsage = clientAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
EOF
# Optional CN override via -subj would replace whole subject; keep CSR subject
# unless --cn set, then use openssl -subj for lab convenience only.
local subj_args=()
if [[ -n "$cn" ]]; then
subj_args=(-subj "/CN=${cn}/O=Verae Lab Only/OU=lab-leaf")
fi
if ! openssl x509 -req \
-in "$csr" \
-CA "$ca" \
-CAkey "$key" \
-CAserial "$serial" \
-out "$out" \
-days "$days" \
-sha256 \
-extfile "$ext" \
"${subj_args[@]+"${subj_args[@]}"}" 2>"$dir/last-sign.err"
then
echo "error: openssl sign failed:" >&2
cat "$dir/last-sign.err" >&2 || true
exit 3
fi
rm -f "$dir/last-sign.err"
# Chain file (leaf + lab root) for convenience — still lab only.
local chain="${out%.crt.pem}.chain.pem"
if [[ "$chain" == "$out" ]]; then
chain="${out}.chain.pem"
fi
cat "$out" "$ca" >"$chain"
echo "lab leaf signed (NOT FOR PRODUCTION):"
echo " leaf: $out"
echo " chain: $chain"
openssl x509 -in "$out" -noout -subject -issuer -dates -serial 2>/dev/null || true
echo
echo "Next (lab/dev only): point a *dev* reverse proxy trust store at:"
echo " $ca"
echo "Default POST /enroll/csr is still 501 unless LAB_CA_ENABLE=1."
echo "See docs/TEST-CA-DESIGN.md — LAB ONLY, never production."
warn_banner
}
main() {
local cmd="${1:-}"
if [[ -z "$cmd" ]]; then
warn_banner
usage >&2
exit 1
fi
shift || true
case "$cmd" in
init) cmd_init "$@" ;;
sign) cmd_sign "$@" ;;
warn) warn_banner; exit 0 ;;
-h|--help|help) usage; exit 0 ;;
*)
echo "error: unknown command: $cmd" >&2
usage >&2
exit 1
;;
esac
}
main "$@"