#!/usr/bin/env bash # Second half of bootstrap: KV mount, policies, AppRoles, host credentials. # Run on vps2 with BAO_TOKEN set to a root token. Idempotent — safe to re-run. # # export BAO_ADDR=https://127.0.0.1:8200 BAO_CACERT=/etc/openbao/tls/bao.crt # export BAO_TOKEN= # sudo -E bash infra/openbao/bootstrap-policies.sh # # Split from bootstrap.sh because everything here is safely automatable: none of it # produces a credential a human has to custody. The one thing that does — the recovery # material from `bao operator init` — is in bootstrap.sh and stops for the operator. set -euo pipefail MOUNT="${THERMOGRAPH_BAO_MOUNT:-thermograph}" SELF_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" POLICY_DIR="$SELF_DIR/policies" : "${BAO_TOKEN:?BAO_TOKEN must be set to a root token}" : "${BAO_ADDR:=https://127.0.0.1:8200}" export BAO_ADDR command -v bao >/dev/null 2>&1 || { echo "!! bao not on PATH" >&2; exit 1; } echo "==> KV v2 mount at ${MOUNT}/" if bao secrets list -format=json | grep -q "\"${MOUNT}/\""; then echo " already mounted" else bao secrets enable -path="$MOUNT" -version=2 kv fi echo "==> policies" for p in "$POLICY_DIR"/*.hcl; do name="$(basename "$p" .hcl)" bao policy write "$name" "$p" >/dev/null echo " wrote $name" done echo "==> approle auth" bao auth list -format=json | grep -q '"approle/"' || bao auth enable approle # One role per environment, CIDR-bound to the host that legitimately renders it. # # secret_id_ttl=0 (never expires) is deliberate: a secret-id that silently lapses # mid-quarter is a self-inflicted outage. The short lifetime lives on the TOKEN # instead — it only has to outlive one render. # # The CIDR binding is the part that makes a leaked secret-id close to useless: it is # only accepted from the correct WireGuard address. add_role() { local role="$1" policy="$2" cidr="$3" bao write "auth/approle/role/${role}" \ token_policies="$policy" \ secret_id_bound_cidrs="$cidr" \ token_bound_cidrs="$cidr" \ secret_id_ttl=0 secret_id_num_uses=0 \ token_ttl=5m token_max_ttl=15m token_num_uses=10 >/dev/null echo " role ${role} -> ${policy} (bound ${cidr})" } add_role tg-prod tg-host-prod 10.10.0.1/32 add_role tg-beta tg-host-beta 10.10.0.1/32 add_role tg-dev tg-host-dev 10.10.0.2/32 add_role tg-centralis tg-centralis 10.10.0.1/32 # Install the local (vps2) credentials. prod and beta both render on this box. # # The OWNERSHIP here is a real boundary that does not exist today, and it is the one # concrete isolation win available on a shared host. render-secrets.sh:119-124 records # that beta's CI deploy user is `deploy` while prod's is `agent`. Today both reach the # SAME root-owned age key via `sudo cat`, so there is no deploy-user-level separation # at all. Giving each environment its own 0400 secret-id owned by its own deploy user # creates one. # # Be honest about the limit: root on vps2 reads both files, exactly as root today reads # the one age key. This defends against MISCONFIGURATION, not against intrusion. install_creds() { local role="$1" dest="$2" owner="$3" local rid sid rid=$(bao read -field=role_id "auth/approle/role/${role}/role-id") sid=$(bao write -f -field=secret_id "auth/approle/role/${role}/secret-id") # umask before creation, so the file is never briefly world-readable. ( umask 077; printf '%s\n%s\n' "$rid" "$sid" > "$dest" ) chown "$owner" "$dest" chmod 0400 "$dest" echo " installed $dest (0400 $owner)" } if [ "$(id -u)" = 0 ]; then install -d -o root -g root -m 0755 /etc/thermograph install_creds tg-prod /etc/thermograph/openbao-approle "agent:agent" install_creds tg-beta /etc/thermograph/openbao-approle-beta "deploy:deploy" \ || echo " (beta creds skipped — no 'deploy' user on this box?)" install_creds tg-centralis /etc/thermograph/openbao-approle-centralis "root:root" else echo "!! not root; skipping credential install. Re-run with sudo -E." >&2 fi cat < done. Policies and AppRoles are in place; the vault is still EMPTY. dev's credentials must be installed on vps1, not here. From your own machine: bao read -field=role_id auth/approle/role/tg-dev/role-id bao write -f -field=secret_id auth/approle/role/tg-dev/secret-id # then, on vps1, write both lines to /etc/thermograph/openbao-approle (0400 agent) Prefer response wrapping so the secret-id never lands in shell history or a log: bao write -f -wrap-ttl=5m auth/approle/role/tg-dev/secret-id # gives a wrapping token # on vps1: bao unwrap (single-use: a failed unwrap means interception) Next: seed, then verify parity. Neither cuts anything over. infra/openbao/seed-from-sops.sh --dry-run infra/openbao/seed-from-sops.sh --all infra/openbao/verify-parity.sh --all EOF