diff --git a/deploy/tls/.gitignore b/deploy/tls/.gitignore new file mode 100644 index 0000000..9609bc6 --- /dev/null +++ b/deploy/tls/.gitignore @@ -0,0 +1,6 @@ +# Private keys and the deploy-specific server certificate never go to git. +# Only the public CA certificate (ca.crt) is versioned, because clients embed it. +*.key +*.csr +*.srl +server.crt diff --git a/deploy/tls/README.md b/deploy/tls/README.md new file mode 100644 index 0000000..b4d4298 --- /dev/null +++ b/deploy/tls/README.md @@ -0,0 +1,56 @@ +# Bus TLS — self-signed CA and server certificate + +The unibus data plane (NATS) is encrypted with TLS using the project's own +self-signed CA. The bus is exposed publicly, protected by auth + TLS, so the CA +is private (not Let's Encrypt) and every client we control embeds the public +`ca.crt`; the server presents `server.crt`/`server.key`. + +## Files + +| File | Secret? | Goes where | +|---|---|---| +| `ca.crt` | no (public) | versioned in git; embedded/distributed to every client | +| `ca.key` | **yes** | stays on the machine that mints certs; gitignored | +| `server.crt` | no | deployed to the bus host; gitignored (deploy-specific SANs) | +| `server.key` | **yes** | deployed to the bus host over a secure channel; gitignored | + +Only `ca.crt` is committed. `ca.key`, `server.key`, `server.crt`, and any +`*.csr`/`*.srl` are gitignored — see `.gitignore`. + +## Generate + +```bash +cd deploy/tls +./generate-certs.sh # CA (if missing) + server cert with default SANs +./generate-certs.sh --force # also regenerate the CA (invalidates pinned clients) +``` + +The server certificate's SANs cover the public IP, the WireGuard IP, the om +hostname, plus `localhost`/`127.0.0.1` for on-host smoke tests. Override the +defaults via environment variables: + +```bash +UNIBUS_PUBLIC_IP=135.125.201.30 UNIBUS_WG_IP=10.42.0.1 UNIBUS_HOSTNAME=om ./generate-certs.sh +``` + +Verify the SANs: + +```bash +openssl x509 -in server.crt -noout -text | grep -A1 'Subject Alternative Name' +``` + +## Use + +- **Server** (`membershipd`, phase 0001e): point it at `server.crt`/`server.key` + so the embedded NATS presents the certificate and requires TLS. Built with + `busauth.ServerTLSConfig(certPath, keyPath)`. +- **Clients** (Go peers, mobile binding, gateway): pin `ca.crt` with + `busauth.LoadCATLSConfig(caPath)` and pass the result as `client.Options.TLS`. + +## Rotation + +The CA is long-lived (10 years). Rotate the server certificate (825 days) by +re-running `generate-certs.sh` (without `--force`) and redeploying +`server.crt`/`server.key`; clients are unaffected because they pin the CA, not +the server cert. Rotating the CA (`--force`) requires redistributing `ca.crt` to +every client. diff --git a/deploy/tls/ca.crt b/deploy/tls/ca.crt new file mode 100644 index 0000000..305e35a --- /dev/null +++ b/deploy/tls/ca.crt @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBfTCCASOgAwIBAgIUW2HZJDDlixxw/DgNP/IDIrJ7MeMwCgYIKoZIzj0EAwIw +FDESMBAGA1UEAwwJdW5pYnVzLWNhMB4XDTI2MDYwNzEwNDIyNloXDTM2MDYwNDEw +NDIyNlowFDESMBAGA1UEAwwJdW5pYnVzLWNhMFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAEe2by5l9dcEbqKB11yJtPIH9S/01XNhuFnBB/IpDevO2fWLLV+muqoB8C +ADH1wKleq8jF5D0sSlK2DCuYrjAjPqNTMFEwHQYDVR0OBBYEFABX+UI7bXICRF4l +WmmDR/rUtxnrMB8GA1UdIwQYMBaAFABX+UI7bXICRF4lWmmDR/rUtxnrMA8GA1Ud +EwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgCAeOYTKvA6SBB8xMdMdqNrp1 +20OPyi2BwFovW6vTCLMCIQC1qRi8SGRHTui8BVqIvp/DFJaZ/U8ocAg/qedLdy+R +/w== +-----END CERTIFICATE----- diff --git a/deploy/tls/generate-certs.sh b/deploy/tls/generate-certs.sh new file mode 100755 index 0000000..a8a6a42 --- /dev/null +++ b/deploy/tls/generate-certs.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# +# generate-certs.sh — mint the unibus bus's self-signed CA and the NATS server +# certificate. Run once on a trusted machine; distribute ca.crt to clients and +# server.crt/server.key to the bus host (server.key by a secure channel, never +# git). Re-running regenerates the server cert; pass --force to also regenerate +# the CA (which invalidates every client that pinned the old ca.crt). +# +# SANs cover the public IP, the WireGuard IP, the om hostname, plus localhost so +# the operator can smoke-test the TLS handshake on the box. Override via env: +# UNIBUS_PUBLIC_IP (default 135.125.201.30) +# UNIBUS_WG_IP (default 10.42.0.1) +# UNIBUS_HOSTNAME (default om) +# +# Key material: EC P-256 (widely supported by Go's crypto/tls and nats-server). +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$DIR" + +PUBLIC_IP="${UNIBUS_PUBLIC_IP:-135.125.201.30}" +WG_IP="${UNIBUS_WG_IP:-10.42.0.1}" +HOSTNAME_OM="${UNIBUS_HOSTNAME:-om}" +DAYS_CA=3650 +DAYS_SRV=825 + +force=0 +[[ "${1:-}" == "--force" ]] && force=1 + +# --- CA (long-lived; only the cert is public) --- +if [[ ! -f ca.crt || ! -f ca.key || $force -eq 1 ]]; then + echo "==> generating CA" + openssl ecparam -name prime256v1 -genkey -noout -out ca.key + chmod 600 ca.key + openssl req -x509 -new -key ca.key -sha256 -days "$DAYS_CA" \ + -subj "/CN=unibus-ca" -out ca.crt +else + echo "==> reusing existing CA (pass --force to regenerate)" +fi + +# --- server certificate, signed by the CA, with the bus SANs --- +echo "==> generating server certificate (SAN: $PUBLIC_IP, $WG_IP, $HOSTNAME_OM, localhost, 127.0.0.1)" +openssl ecparam -name prime256v1 -genkey -noout -out server.key +chmod 600 server.key +openssl req -new -key server.key -subj "/CN=unibus-bus" -out server.csr + +cat > server.ext < done:" +echo " ca.crt -> embed/distribute to every client (public)" +echo " server.crt -> deploy to the bus host" +echo " server.key -> deploy to the bus host over a secure channel (NEVER git)" +echo +echo "verify SANs with:" +echo " openssl x509 -in server.crt -noout -text | grep -A1 'Subject Alternative Name'"