#!/usr/bin/env bash # # switchboard-ca.sh — Certificate provisioning for Switchboard Core mTLS. # # Wraps openssl to generate a cluster CA, node certificates (ServerAuth + # ClientAuth), and user certificates (ClientAuth only). All output is PEM. # # Usage: # switchboard-ca init # switchboard-ca issue-node --name --san # switchboard-ca issue-user --cn [--email ] # # Files are written to the current directory under ca/, nodes/, users/. set -euo pipefail CA_DIR="./ca" NODES_DIR="./nodes" USERS_DIR="./users" CA_DAYS=3650 # 10 years for CA NODE_DAYS=365 # 1 year for nodes USER_DAYS=90 # 90 days for users # ── Helpers ────────────────────────────────────────────────────────── die() { echo "error: $*" >&2; exit 1; } require_openssl() { command -v openssl >/dev/null 2>&1 || die "openssl not found in PATH" } require_ca() { [ -f "$CA_DIR/cluster-ca.key" ] || die "CA not initialized. Run: $0 init" [ -f "$CA_DIR/cluster-ca.crt" ] || die "CA not initialized. Run: $0 init" } # ── init ───────────────────────────────────────────────────────────── cmd_init() { require_openssl mkdir -p "$CA_DIR" if [ -f "$CA_DIR/cluster-ca.key" ]; then die "CA already exists at $CA_DIR/cluster-ca.key — remove it first to reinitialize" fi # Generate ECDSA P-256 CA key openssl ecparam -genkey -name prime256v1 -noout -out "$CA_DIR/cluster-ca.key" 2>/dev/null # Self-signed CA certificate openssl req -new -x509 \ -key "$CA_DIR/cluster-ca.key" \ -out "$CA_DIR/cluster-ca.crt" \ -days "$CA_DAYS" \ -subj "/CN=Switchboard Cluster CA" \ -addext "basicConstraints=critical,CA:TRUE" \ -addext "keyUsage=critical,keyCertSign,cRLSign" \ 2>/dev/null echo "CA initialized:" echo " $CA_DIR/cluster-ca.crt" echo " $CA_DIR/cluster-ca.key" echo "" echo "Keep cluster-ca.key secure. It never needs to be on a running node." } # ── issue-node ─────────────────────────────────────────────────────── cmd_issue_node() { require_openssl require_ca local name="" local san="" while [[ $# -gt 0 ]]; do case "$1" in --name) name="$2"; shift 2 ;; --san) san="$2"; shift 2 ;; *) die "unknown option: $1" ;; esac done [ -n "$name" ] || die "usage: $0 issue-node --name --san " [ -n "$san" ] || die "usage: $0 issue-node --name --san " mkdir -p "$NODES_DIR" # Generate node key openssl ecparam -genkey -name prime256v1 -noout -out "$NODES_DIR/$name.key" 2>/dev/null # Build SAN entries from comma-separated list local san_entries="" IFS=',' read -ra ADDRS <<< "$san" for addr in "${ADDRS[@]}"; do addr=$(echo "$addr" | xargs) # trim whitespace if [[ "$addr" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then san_entries="${san_entries}IP:${addr}," else san_entries="${san_entries}DNS:${addr}," fi done san_entries="${san_entries%,}" # remove trailing comma # Create CSR openssl req -new \ -key "$NODES_DIR/$name.key" \ -out "$NODES_DIR/$name.csr" \ -subj "/CN=$name" \ 2>/dev/null # Create extensions config local ext_file ext_file=$(mktemp) cat > "$ext_file" </dev/null rm -f "$NODES_DIR/$name.csr" "$ext_file" echo "Node cert issued ($NODE_DAYS days):" echo " $NODES_DIR/$name.crt" echo " $NODES_DIR/$name.key" echo "" echo "Deploy to the node along with $CA_DIR/cluster-ca.crt" } # ── issue-user ─────────────────────────────────────────────────────── cmd_issue_user() { require_openssl require_ca local cn="" local email="" while [[ $# -gt 0 ]]; do case "$1" in --cn) cn="$2"; shift 2 ;; --email) email="$2"; shift 2 ;; *) die "unknown option: $1" ;; esac done [ -n "$cn" ] || die "usage: $0 issue-user --cn [--email ]" mkdir -p "$USERS_DIR" # Generate user key openssl ecparam -genkey -name prime256v1 -noout -out "$USERS_DIR/$cn.key" 2>/dev/null # Build subject local subj="/CN=$cn" local san_line="" if [ -n "$email" ]; then san_line="email:${email}" fi # Create CSR openssl req -new \ -key "$USERS_DIR/$cn.key" \ -out "$USERS_DIR/$cn.csr" \ -subj "$subj" \ 2>/dev/null # Create extensions config local ext_file ext_file=$(mktemp) cat > "$ext_file" <> "$ext_file" fi # Sign with CA openssl x509 -req \ -in "$USERS_DIR/$cn.csr" \ -CA "$CA_DIR/cluster-ca.crt" \ -CAkey "$CA_DIR/cluster-ca.key" \ -CAcreateserial \ -out "$USERS_DIR/$cn.crt" \ -days "$USER_DAYS" \ -extfile "$ext_file" \ 2>/dev/null rm -f "$USERS_DIR/$cn.csr" "$ext_file" echo "User cert issued ($USER_DAYS days):" echo " $USERS_DIR/$cn.crt" echo " $USERS_DIR/$cn.key" echo "" echo "Import both files into the user's browser or CLI tool." } # ── Main dispatch ──────────────────────────────────────────────────── case "${1:-}" in init) shift cmd_init "$@" ;; issue-node) shift cmd_issue_node "$@" ;; issue-user) shift cmd_issue_user "$@" ;; *) echo "Usage: $0 " echo "" echo "Commands:" echo " init Initialize cluster CA" echo " issue-node --name --san Issue node cert (server+client auth)" echo " issue-user --cn [--email ] Issue user cert (client auth only)" exit 1 ;; esac