thermograph/infra/deploy/secrets/seed-from-live.sh

96 lines
4.3 KiB
Bash
Raw Normal View History

#!/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."