#!/usr/bin/env bash # # generate-cluster-certs.sh — mint the TLS material for a unibus 3-node cluster # (issue 0006g). Run ONCE on a trusted machine (e.g. om, which custodies the bus # CA); distribute the per-node output to each node over a secure channel. This # script touches NO remote host. # # It produces two trust roots, kept SEPARATE on purpose (audit 0008 N1-low): # # 1. The CLUSTER route CA (cluster-ca.crt/key, generated here): signs each # node's ROUTE certificate. The route layer authenticates NODES, not bus # users, so it must NOT share the client data-plane CA — a client cert can # then never be presented to the route port. # 2. The CLIENT data-plane CA (../tls/ca.crt/key, the one clients pin): signs # each node's DATA-PLANE server certificate. Reused, not regenerated, so # existing clients keep trusting the bus. # # Per node it emits, under out//: # route-.crt/key route cert (cluster CA), EKU server+clientAuth # (each node is BOTH server and dialer to its peers) # server-.crt/key data-plane cert (client CA), EKU serverAuth # cluster-ca.crt the route CA cert (for --route-tls-ca) # ca.crt the client CA cert (for clients / control-plane TLS) # # SANs per node = its public IP + its WireGuard IP + its hostname + localhost. # # Key material: EC P-256 (Go crypto/tls + nats-server friendly), matching # ../tls/generate-certs.sh. set -euo pipefail DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$DIR" # shellcheck source=/dev/null source ./nodes.env # Refuse to run while any placeholder remains (HUMAN must fill nodes.env first). if grep -q '<[A-Z_]\+>' nodes.env; then echo "ERROR: nodes.env still has values — fill them in first." >&2 grep -n '<[A-Z_]\+>' nodes.env >&2 exit 2 fi CLIENT_CA_CRT="../tls/ca.crt" CLIENT_CA_KEY="../tls/ca.key" if [[ ! -f "$CLIENT_CA_CRT" || ! -f "$CLIENT_CA_KEY" ]]; then echo "ERROR: client data-plane CA not found at ../tls/ca.{crt,key}." >&2 echo " Run ../tls/generate-certs.sh first (it mints the client CA)." >&2 exit 2 fi DAYS_CA=3650 DAYS_CRT=825 force=0 [[ "${1:-}" == "--force" ]] && force=1 # --- cluster route CA (separate trust root) --- if [[ ! -f cluster-ca.crt || ! -f cluster-ca.key || $force -eq 1 ]]; then echo "==> generating cluster route CA (separate from the client CA)" openssl ecparam -name prime256v1 -genkey -noout -out cluster-ca.key chmod 600 cluster-ca.key openssl req -x509 -new -key cluster-ca.key -sha256 -days "$DAYS_CA" \ -subj "/CN=unibus-cluster-ca" -out cluster-ca.crt else echo "==> reusing existing cluster route CA (pass --force to regenerate)" fi # mint mint_cert() { local out_key="$1" out_crt="$2" cn="$3" san="$4" eku="$5" ca_crt="$6" ca_key="$7" local csr ext csr="$(mktemp)" ext="$(mktemp)" openssl ecparam -name prime256v1 -genkey -noout -out "$out_key" chmod 600 "$out_key" openssl req -new -key "$out_key" -subj "/CN=${cn}" -out "$csr" cat > "$ext" < node ${name}: SAN IP:${pub}, IP:${wg}, DNS:${name}, localhost, 127.0.0.1" nodedir="out/${name}" mkdir -p "$nodedir" san="IP:${pub},IP:${wg},DNS:${name},DNS:localhost,IP:127.0.0.1" # Route cert: signed by the cluster CA; server+client auth (mutual routes). mint_cert "${nodedir}/route-${name}.key" "${nodedir}/route-${name}.crt" \ "unibus-route-${name}" "$san" "serverAuth,clientAuth" \ cluster-ca.crt cluster-ca.key # Data-plane server cert: signed by the client CA; serverAuth only. mint_cert "${nodedir}/server-${name}.key" "${nodedir}/server-${name}.crt" \ "unibus-${name}" "$san" "serverAuth" \ "$CLIENT_CA_CRT" "$CLIENT_CA_KEY" # Co-locate the two CA certs each node needs. cp cluster-ca.crt "${nodedir}/cluster-ca.crt" cp "$CLIENT_CA_CRT" "${nodedir}/ca.crt" done rm -f cluster-ca.srl ../tls/ca.srl 2>/dev/null || true echo echo "==> done. Per-node material under out// (KEYS ARE SECRET — never git):" for row in "${CLUSTER_NODES[@]}"; do read -r name _rest <<<"$row" echo " out/${name}/ (route-${name}.*, server-${name}.*, cluster-ca.crt, ca.crt)" done echo echo "verify a SAN with:" echo " openssl x509 -in out//server-.crt -noout -text | grep -A1 'Subject Alternative Name'"