thermograph/infra/deploy/secrets/seed-from-live.sh
emi d138f00a20
Some checks failed
Sync infra to hosts / sync-beta (push) Has been skipped
Sync infra to hosts / sync-prod (push) Has been skipped
Sync infra to hosts / sync-dev (push) Failing after 6s
secrets-guard / encrypted (push) Successful in 6s
shell-lint / shellcheck (push) Successful in 13s
Validate observability stack / validate (push) Successful in 17s
PR build (required check) / changes (pull_request) Successful in 6s
secrets-guard / encrypted (pull_request) Successful in 5s
PR build (required check) / build-backend (pull_request) Has been skipped
shell-lint / shellcheck (pull_request) Successful in 6s
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Successful in 18s
PR build (required check) / gate (pull_request) Successful in 2s
infra: split the estate into vps1/vps2 — beta joins prod, dev gets a home (#103)
2026-07-26 06:56:38 +00:00

95 lines
4.3 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Seed (or re-seed) an encrypted secrets file from an environment's live env file.
# Run on the operator's machine — it reads production secrets, so it is deliberately
# NOT something the agent runs for you. Requires: sops + age on PATH, the age private
# key at ~/.config/sops/age/keys.txt (or SOPS_AGE_KEY_FILE), and SSH access to the box.
#
# deploy/secrets/seed-from-live.sh prod agent@169.58.46.181 ~/.ssh/thermograph_agent_ed25519
# deploy/secrets/seed-from-live.sh beta agent@169.58.46.181 ~/.ssh/thermograph_agent_ed25519
# deploy/secrets/seed-from-live.sh dev agent@75.119.132.91 ~/.ssh/thermograph_agent_ed25519
#
# Note prod and beta share a box (vps2) and therefore differ only by which env
# FILE is read: /etc/thermograph.env vs /etc/thermograph-beta.env. That path is
# resolved from deploy/env-topology.sh rather than hardcoded — hardcoding it
# meant "seed beta" would read PROD's live secrets and write them into
# beta.yaml, which is how one environment silently ends up holding another's
# credentials.
#
# Writes deploy/secrets/<env>.yaml ENCRYPTED (exact copy of the live env, so the
# deploy render is faithful), then verifies the render round-trips to the same
# KEY=VALUE set. Never prints secret values. See README.md.
set -euo pipefail
# Anchor to the repo that holds this script, so it works when invoked by absolute path
# from any cwd (a different checkout, your home dir, etc.) — OUT and .sops.yaml resolve
# relative to here, not to wherever you happened to run it.
cd "$(dirname "$(readlink -f "$0")")/../.."
ENV_NAME="${1:?usage: seed-from-live.sh <env> <ssh-target> [ssh-key]}"
SSH_TARGET="${2:?ssh target, e.g. agent@169.58.46.181}"
SSH_KEY="${3:-$HOME/.ssh/thermograph_agent_ed25519}"
OUT="deploy/secrets/${ENV_NAME}.yaml"
# shellcheck source=infra/deploy/env-topology.sh
. deploy/env-topology.sh
thermograph_topology "$ENV_NAME"
REMOTE_ENV_FILE="$TG_ENV_FILE"
command -v sops >/dev/null || { echo "!! sops not on PATH" >&2; exit 1; }
tmp="$(mktemp)"; trap 'rm -f "$tmp" "$tmp.env"' EXIT
umask 077
echo "==> Pulling ${SSH_TARGET}:${REMOTE_ENV_FILE} (sudo)"
ssh -i "$SSH_KEY" "$SSH_TARGET" "sudo cat $REMOTE_ENV_FILE" > "$tmp"
n=$(grep -cE '^[A-Za-z_][A-Za-z0-9_]*=' "$tmp" || true)
[ "$n" -gt 0 ] || { echo "!! no KEY=VALUE lines read (permission? path?)" >&2; exit 1; }
echo " ${n} vars"
echo "==> Writing ${OUT} (flat YAML) and encrypting"
python3 - "$tmp" "$OUT" "$ENV_NAME" <<'PY'
import json, sys
src, dst, env = sys.argv[1:4]
pairs = {}
for line in open(src, encoding="utf-8"):
s = line.rstrip("\n").strip()
if not s or s.startswith("#") or "=" not in s:
continue
k, _, v = s.partition("=")
k = k.strip()
if not all(c.isalnum() or c == "_" for c in k) or not k[0].isalpha() and k[0] != "_":
continue
pairs[k] = v
with open(dst, "w", encoding="utf-8") as fh:
fh.write(f"# SOPS-encrypted — {env} host secrets, seeded from that environments live env file.\n")
for k, v in pairs.items():
fh.write(f"{k}: {json.dumps(v)}\n") # JSON-escaped scalar is valid YAML
print(f" {len(pairs)} keys")
PY
sops -e -i "$OUT"
grep -q 'ENC\[AES256_GCM' "$OUT" || { echo "!! $OUT did not encrypt — refusing to keep it" >&2; rm -f "$OUT"; exit 1; }
echo "==> Verifying the render round-trips to the same KEY=VALUE set"
sops -d --input-type yaml --output-type dotenv "$OUT" > "$tmp.env"
python3 - "$tmp" "$tmp.env" <<'PY'
import sys
def load(p):
d = {}
for line in open(p, encoding="utf-8"):
s = line.rstrip("\n").strip()
if not s or s.startswith("#") or "=" not in s:
continue
k, _, v = s.partition("=")
d[k.strip()] = v
return d
live, rendered = load(sys.argv[1]), load(sys.argv[2])
missing = sorted(set(live) - set(rendered))
extra = sorted(set(rendered) - set(live))
diff = sorted(k for k in live if k in rendered and live[k] != rendered[k])
if missing or extra or diff:
if missing: print(" !! keys lost in render:", missing)
if extra: print(" !! keys added in render:", extra)
if diff: print(" !! values changed (keys only):", diff)
sys.exit(1)
print(f" OK — {len(live)} keys render identically")
PY
echo "==> ${OUT} is ready. Review with: sops ${OUT} (or git diff --stat)"
echo " Then commit + push; the agent can take it from there."