116 lines
4.7 KiB
Bash
116 lines
4.7 KiB
Bash
|
|
#!/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=<root 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 <<EOF
|
||
|
|
|
||
|
|
==> 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 <token> (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
|