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>
112 lines
4.5 KiB
Bash
Executable file
112 lines
4.5 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Install / refresh the CIS490 receiver role on the central WG node
|
|
# (the Pi5 in our setup). Idempotent — safe to re-run.
|
|
#
|
|
# Steps:
|
|
# 1. Verify prereqs (python3.11+, systemd).
|
|
# 2. Create the cis490 service user + /var/lib/cis490 layout.
|
|
# 3. Sync the repo into /opt/cis490 and build a venv.
|
|
# 4. Install cis490-receiver.service.
|
|
# 5. Drop /etc/cis490/receiver.toml on first install.
|
|
#
|
|
# This script does NOT:
|
|
# - configure Caddy. Add a `collector.wg` block to your spectral/caddy
|
|
# config to terminate TLS and reverse-proxy to 127.0.0.1:8443.
|
|
# - issue server / client certs. wg-pki owns CA + leaf issuance.
|
|
# - open firewall ports. iptmonads owns the WG-side ruleset.
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
INSTALL_ROOT="${INSTALL_ROOT:-/opt/cis490}"
|
|
DATA_ROOT="${DATA_ROOT:-/var/lib/cis490}"
|
|
ETC_ROOT="${ETC_ROOT:-/etc/cis490}"
|
|
SERVICE_USER="${SERVICE_USER:-cis490}"
|
|
|
|
log() { printf '[install-receiver] %s\n' "$*" >&2; }
|
|
die() { log "FATAL: $*"; exit 1; }
|
|
|
|
# --- 1. prereqs --------------------------------------------------------
|
|
log "checking prereqs"
|
|
if [[ $EUID -ne 0 ]]; then
|
|
die "must run as root"
|
|
fi
|
|
command -v systemctl >/dev/null || die "systemd not found"
|
|
command -v python3 >/dev/null || die "python3 not on PATH"
|
|
|
|
PY_VER="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
|
|
if ! python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3,11) else 1)'; then
|
|
die "python >=3.11 required, found $PY_VER"
|
|
fi
|
|
|
|
USE_UV=0
|
|
if command -v uv >/dev/null; then USE_UV=1; fi
|
|
|
|
# --- 2. user + layout --------------------------------------------------
|
|
log "ensuring service user $SERVICE_USER"
|
|
if ! id -u "$SERVICE_USER" >/dev/null 2>&1; then
|
|
useradd --system --no-create-home --shell /usr/sbin/nologin \
|
|
--home-dir "$INSTALL_ROOT" "$SERVICE_USER"
|
|
fi
|
|
|
|
install -d -o root -g root -m 0755 "$ETC_ROOT" "$ETC_ROOT/certs"
|
|
install -d -o "$SERVICE_USER" -g "$SERVICE_USER" -m 0755 \
|
|
"$DATA_ROOT" "$DATA_ROOT/episodes" "$DATA_ROOT/incoming"
|
|
install -d -o "$SERVICE_USER" -g "$SERVICE_USER" -m 0750 "$DATA_ROOT"
|
|
# Pre-create the index file so the first PUT doesn't race on creation.
|
|
sudo -u "$SERVICE_USER" -- touch "$DATA_ROOT/index.jsonl"
|
|
|
|
# --- 3. repo + venv ----------------------------------------------------
|
|
log "syncing repo into $INSTALL_ROOT"
|
|
install -d -o "$SERVICE_USER" -g "$SERVICE_USER" -m 0755 "$INSTALL_ROOT"
|
|
cp -aT "$REPO_ROOT" "$INSTALL_ROOT"
|
|
chown -R "$SERVICE_USER":"$SERVICE_USER" "$INSTALL_ROOT"
|
|
|
|
log "building venv"
|
|
if [[ "$USE_UV" -eq 1 ]]; then
|
|
sudo -u "$SERVICE_USER" -- env HOME="$INSTALL_ROOT" \
|
|
uv sync --project "$INSTALL_ROOT"
|
|
else
|
|
sudo -u "$SERVICE_USER" -- python3 -m venv "$INSTALL_ROOT/.venv"
|
|
sudo -u "$SERVICE_USER" -- "$INSTALL_ROOT/.venv/bin/pip" install \
|
|
--quiet --upgrade pip
|
|
sudo -u "$SERVICE_USER" -- "$INSTALL_ROOT/.venv/bin/pip" install \
|
|
--quiet starlette 'uvicorn[standard]'
|
|
fi
|
|
|
|
# --- 4. systemd --------------------------------------------------------
|
|
log "installing systemd units (receiver + bootstrap)"
|
|
install -m 0644 "$REPO_ROOT/etc/cis490-receiver.service" \
|
|
/etc/systemd/system/cis490-receiver.service
|
|
install -m 0644 "$REPO_ROOT/etc/cis490-bootstrap.service" \
|
|
/etc/systemd/system/cis490-bootstrap.service
|
|
systemctl daemon-reload
|
|
|
|
# --- 5. config template (only on first install) -----------------------
|
|
if [[ ! -f "$ETC_ROOT/receiver.toml" ]]; then
|
|
log "writing $ETC_ROOT/receiver.toml (template)"
|
|
install -m 0640 -o root -g "$SERVICE_USER" \
|
|
"$REPO_ROOT/etc/receiver.toml.example" "$ETC_ROOT/receiver.toml"
|
|
log ""
|
|
log "FIRST-INSTALL NEXT STEPS:"
|
|
log " 1. Verify $ETC_ROOT/receiver.toml paths."
|
|
log " 2. Add a collector.wg block to your spectral/caddy config."
|
|
log " Example:"
|
|
log " collector.wg {"
|
|
log " tls internal"
|
|
log " reverse_proxy 127.0.0.1:8443"
|
|
log " }"
|
|
log " (mTLS to clients is enforced by the wg-pki CA bundle on"
|
|
log " the receiver side once leaf certs are issued.)"
|
|
log " 3. Open the WG-side port via iptmonads."
|
|
log " 4. systemctl enable --now cis490-receiver cis490-bootstrap"
|
|
log " 5. From a lab host: cis490-shipper --ping"
|
|
log ""
|
|
log "Bootstrap endpoint (cis490-bootstrap on :8446 + Caddy bootstrap.wg)"
|
|
log "lets enrolled lab hosts auto-fetch their leaf certs. Without it,"
|
|
log "operators have to hand-carry tarballs via deploy-cis490-cert.sh."
|
|
else
|
|
log "$ETC_ROOT/receiver.toml exists; leaving in place"
|
|
fi
|
|
|
|
log "receiver install complete."
|