This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/scripts/switchboard-ca.sh
Jeffrey Smith fb5284f667
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m37s
CI/CD / test-sqlite (push) Successful in 2m48s
CI/CD / build-and-deploy (push) Successful in 52s
Feat v0.6.7 native mtls (#42)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-03-31 18:36:12 +00:00

238 lines
6.5 KiB
Bash
Executable File

#!/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 <node-name> --san <dns1,ip1,...>
# switchboard-ca issue-user --cn <username> [--email <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 <node-name> --san <dns1,ip1,...>"
[ -n "$san" ] || die "usage: $0 issue-node --name <node-name> --san <dns1,ip1,...>"
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" <<EOF
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = serverAuth, clientAuth
subjectAltName = ${san_entries}
EOF
# Sign with CA
openssl x509 -req \
-in "$NODES_DIR/$name.csr" \
-CA "$CA_DIR/cluster-ca.crt" \
-CAkey "$CA_DIR/cluster-ca.key" \
-CAcreateserial \
-out "$NODES_DIR/$name.crt" \
-days "$NODE_DAYS" \
-extfile "$ext_file" \
2>/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 <username> [--email <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" <<EOF
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = clientAuth
EOF
if [ -n "$san_line" ]; then
echo "subjectAltName = ${san_line}" >> "$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 <command>"
echo ""
echo "Commands:"
echo " init Initialize cluster CA"
echo " issue-node --name <n> --san <addrs> Issue node cert (server+client auth)"
echo " issue-user --cn <name> [--email <e>] Issue user cert (client auth only)"
exit 1
;;
esac