#!/bin/bash # init-stepca.sh — provision the PROD step-ca ACME endpoint on tsys-ca [#800] # # Run ON tsys-ca as root, from /opt/stepca-prod (this script + compose.yaml # live there; data/ is created). Refuses to touch an existing data/ unless # --reinit. # # What it does: # 1. scaffolds a step home (docker one-shot init; throwaway self-signed CA) # 2. generates an ECDSA P-256 ACME intermediate and signs it with the # FLEET ROOT (/root/ca-root) — same constraints as ca-init.sh # 3. swaps in the fleet chain (root cert + intermediate), adds an ACME # provisioner, writes the runtime password # The root KEY never leaves /root/ca-root. The ACME intermediate key and the # runtime password live only in /opt/stepca-prod (0700, root-owned). set -euo pipefail BASE="/opt/stepca-prod" DATA="$BASE/data" ROOT_CRT="/root/ca-root/root.crt" ROOT_KEY="/root/ca-root/root.key" IMAGE="smallstep/step-ca@sha256:e9e8fa3262bf37b130962ffddbf6a64ac188f0bbb80959cf3ddc04c6bf294c3d" TAILNET_IP="$(tailscale ip -4)" if [ ! -f "$ROOT_CRT" ] || [ ! -f "$ROOT_KEY" ]; then echo "FAIL: fleet root not found at $ROOT_CRT/$ROOT_KEY" >&2; exit 1 fi [ -n "$TAILNET_IP" ] || { echo "FAIL: no tailscale IP" >&2; exit 1; } if [ -d "$DATA" ] && [ "${1:-}" != "--reinit" ]; then echo "FAIL: $DATA exists (pass --reinit to wipe and redo)" >&2; exit 1 fi mkdir -p "$BASE" "$BASE/secrets" chmod 700 "$BASE"; chmod 711 "$BASE/secrets" rm -rf "$DATA"; mkdir -p "$DATA/certs" "$DATA/secrets" "$DATA/config" "$DATA/db" # scaffold + runtime run as container uid 1000 (step) — tree must be writable by it chown -R 1000:1000 "$DATA" # runtime + provisioner password (generated once; container uid 1000 must read it) if [ ! -s "$BASE/secrets/password" ]; then openssl rand -base64 18 > "$BASE/secrets/password" fi chown 1000:1000 "$BASE/secrets/password" chmod 600 "$BASE/secrets/password" # 1. scaffold (throwaway self-signed CA; replaced below) docker run --rm -v "$DATA:/home/step" -v "$BASE/secrets:/secrets:ro" \ --entrypoint /bin/sh "$IMAGE" -c ' step ca init --name "KNEL Fleet CA" \ --dns "localhost" --dns "tsys-ca.knel.net" --dns "'"$TAILNET_IP"'" \ --address ":9000" --provisioner "admin" \ --password-file /secrets/password --provisioner-password-file /secrets/password \ --deployment-type standalone >/dev/null' echo "scaffold done" # 2. ACME intermediate signed by the fleet root (ECDSA P-256, pathlen 0) openssl ecparam -name prime256v1 -genkey -noout -out /tmp/acme-int.key chmod 400 /tmp/acme-int.key openssl req -new -key /tmp/acme-int.key -out /tmp/acme-int.csr -sha256 \ -subj "/C=US/ST=Texas/O=Known Element Enterprises/OU=TechOps/CN=KNEL Fleet ACME Intermediate" openssl x509 -req -in /tmp/acme-int.csr -CA "$ROOT_CRT" -CAkey "$ROOT_KEY" \ -CAcreateserial -days 1825 -sha256 -out /tmp/acme-int.crt \ -extfile <(printf 'basicConstraints=critical,CA:TRUE,pathlen:0\nkeyUsage=critical,keyCertSign,cRLSign\nsubjectKeyIdentifier=hash\nauthorityKeyIdentifier=keyid:always') openssl verify -CAfile "$ROOT_CRT" /tmp/acme-int.crt rm -f /tmp/acme-int.csr # 3. swap in the fleet chain (root KEY stays offline in /root/ca-root) cp "$ROOT_CRT" "$DATA/certs/root_ca.crt" cp /tmp/acme-int.crt "$DATA/certs/intermediate_ca.crt" mv /tmp/acme-int.key "$DATA/secrets/intermediate_ca_key" rm -f "$DATA/secrets/root_ca_key" # 4. ACME provisioner (JWK password re-read from the same file) docker run --rm -v "$DATA:/home/step" -v "$BASE/secrets:/secrets:ro" \ --entrypoint /bin/sh "$IMAGE" -c ' step ca provisioner add acme --type ACME --password-file /secrets/password >/dev/null' # 5. runtime password where the container entrypoint expects it cp "$BASE/secrets/password" "$DATA/secrets/password" # ownership to uid 1000 (step) chown -R 1000:1000 "$DATA" chmod 400 "$DATA/secrets/intermediate_ca_key" echo "init complete: $DATA ready — bring up with: cd $BASE && docker compose -f compose.yaml up -d"