CIS490/scripts/issue-cis490-client-cert-wrapper.sh
max a93a3ff221 bootstrap: auto-issue mTLS leaves to enrolled lab hosts (closes #9, refs #3)
Adds a pull-based cert distribution path so install-lab-host.sh can
fetch its own leaf cert without operator intervention. Removes the
ssh-from-Pi requirement that blocked elliott-lab.

How the chicken-and-egg gets solved: a freshly wg-enrolled lab host
already has WG access (gate kept by iptmonads at L4) and trusts the
Caddy local CA (bundled in this repo at etc/caddy-root.crt). It
makes a single TLS call to https://bootstrap.wg/v1/cert/<host_id>
— no mTLS — gets back a tar of {ca.crt, leaf.pem, leaf.key},
extracts to /etc/cis490/certs/, and the shipper unblocks. Trust
boundary is "reached :443 over WG"; no operator action needed.

bootstrap/
  app.py        Starlette: GET /v1/cert/{host_id}, GET /v1/health.
                Validates host_id charset, rate-limits per source IP,
                logs every mint with the X-Real-IP Caddy injects.
  __main__.py   uvicorn launcher; runs as root because the wg-pki CA
                private key is root-only.

etc/cis490-bootstrap.service
  systemd unit on 127.0.0.1:8446 with ProtectSystem=strict +
  narrow ReadWritePaths=/var/lib/wg-pki. ProtectHome=no because
  systemd's read-only mode hides /home contents (the issuer script
  the wrapper exec's lives there).

scripts/issue-cis490-client-cert-wrapper.sh
  Adapter the bootstrap service shells out to. Resolves the actual
  wg-pki issuer script across the three plausible install layouts
  (/opt/wg-pki, /home/max/wg-pki, /home/max/.env/wg-pki) so a single
  copy of the unit file works on any operator's box. Forces
  --out-dir to /var/lib/wg-pki/issued so writes stay inside the
  service's narrow ReadWritePaths.

scripts/install-lab-host.sh
  After scaffolding lab-host.toml, if /etc/cis490/certs/lab-host.pem
  is absent, curls bootstrap.wg with --cacert etc/caddy-root.crt
  (no chicken-and-egg), extracts, chowns/chmods. Skips silently if
  bootstrap.wg is unreachable so manual hand-carry remains possible.

scripts/install-receiver.sh
  Drops cis490-bootstrap.service alongside cis490-receiver and
  prints both as "enable --now" candidates. cis490-bootstrap is the
  thing that makes lab hosts self-provisioning.

etc/caddy-root.crt
  Bundled copy of wg-pki's published Caddy local CA root, so the
  bootstrap fetch can verify TLS without depending on a wg-pki
  clone that may or may not be on the lab host yet.

Verified live on the Pi:
  $ curl --cacert etc/caddy-root.crt https://bootstrap.wg/v1/cert/elliott-lab -o /tmp/x.tar
  HTTP 200 size=10240
  $ tar tf /tmp/x.tar
  ca.crt
  elliott-lab.key
  elliott-lab.pem
  $ openssl verify -CAfile … elliott-lab.pem
  /tmp/.../elliott-lab.pem: OK
  $ openssl x509 -subject … -noout
  subject=CN=elliott-lab

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:30:29 -05:00

50 lines
1.7 KiB
Bash
Executable file

#!/usr/bin/env bash
# Wrapper that re-points the wg-pki issuer script's relative-path
# assumption (PWD-derived publish dir, $REPO_ROOT/issued/) to the
# absolute /var/lib/wg-pki/issued/ that the bootstrap service uses.
#
# wg-pki ships the actual issuer at
# /home/max/.env/wg-pki/scripts/issue-cis490-client-cert.sh, which
# computes paths relative to its own location. This wrapper sets
# WG_PKI_STATE so the CA key is found in /var/lib/wg-pki, and forces
# --out-dir to a path under /var/lib so cis490-bootstrap (with
# ProtectHome=tmpfs) can write the resulting tarballs.
set -euo pipefail
# Resolve issuer path: prefer the install-time copy at /opt/wg-pki/,
# fall back to whatever wg-pki clone the operator has under /home.
ISSUER="${WG_PKI_ISSUER:-}"
if [[ -z "$ISSUER" ]]; then
for cand in \
/opt/wg-pki/scripts/issue-cis490-client-cert.sh \
/home/max/wg-pki/scripts/issue-cis490-client-cert.sh \
/home/max/.env/wg-pki/scripts/issue-cis490-client-cert.sh; do
if [[ -x "$cand" ]]; then ISSUER="$cand"; break; fi
done
fi
if [[ -z "$ISSUER" || ! -x "$ISSUER" ]]; then
echo "wrapper: no issue-cis490-client-cert.sh found; tried /opt/wg-pki, /home/max/wg-pki, /home/max/.env/wg-pki" >&2
exit 2
fi
OUT_ROOT="/var/lib/wg-pki/issued"
if [[ $# -lt 1 ]]; then
echo "usage: $0 <host_id> [--out-dir DIR] [--days N]" >&2
exit 2
fi
HOST_ID="$1"; shift
# Pull off any --out-dir already passed; we override.
EXTRA=()
while [[ $# -gt 0 ]]; do
case "$1" in
--out-dir) shift 2 ;; # drop, we set it ourselves
*) EXTRA+=("$1"); shift ;;
esac
done
mkdir -p "$OUT_ROOT/$HOST_ID"
exec env WG_PKI_STATE=/var/lib/wg-pki \
"$ISSUER" "$HOST_ID" --out-dir "$OUT_ROOT/$HOST_ID" "${EXTRA[@]}"