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
95 lines
4.3 KiB
Bash
Executable file
95 lines
4.3 KiB
Bash
Executable file
#!/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 environment’s 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."
|