thermograph/infra/.claude/skills/key-gaps/SKILL.md
emi ca84e0ce95
Some checks failed
Deploy frontend to beta VPS / deploy (push) Failing after 8s
Sync infra to hosts / sync-beta (push) Successful in 7s
Deploy backend to beta VPS / deploy (push) Failing after 13s
secrets-guard / encrypted (push) Successful in 12s
Sync infra to hosts / sync-prod (push) Successful in 14s
shell-lint / shellcheck (push) Successful in 12s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 57s
Build + push frontend image (Forgejo registry) / build-push (push) Successful in 1m2s
Promote dev → main (frontend QA batch → beta) (#71)
2026-07-24 23:20:16 +00:00

4.6 KiB

name description
key-gaps Audit which secret keys are missing, partially configured, or drifting across Thermograph environments (prod, beta, dev) — comparing the live /etc/thermograph.env on each box and/or the SOPS vault files (deploy/secrets/*.yaml). Use when asked about "key gaps", "missing keys/secrets", "what's not set", cross-environment secret drift, or as a pre-deploy / pre-cutover safety check. Reads key NAMES only, never values.

Key-gap audit

Reports, per environment and across them, where secret keys are missing or inconsistent — with special weight on the failure modes that bite silently here.

Run the deterministic script; don't eyeball it:

python3 .claude/skills/key-gaps/key_gaps.py <env>=<source> [<env>=<source> ...]

<source> is either a SOPS file (deploy/secrets/prod.yaml) or a key-list file (one KEY per line, or KEY=/KEY: lines). Keys are readable in a SOPS file even encrypted, so no age key or decryption is needed. Exit status is non-zero if any CRITICAL or required gap is found (so it also works as a CI/pre-deploy gate).

Audit the SOPS vault (offline, no SSH)

python3 .claude/skills/key-gaps/key_gaps.py \
  prod=deploy/secrets/prod.yaml beta=deploy/secrets/beta.yaml

Audit the LIVE boxes (read-only, key names only)

Gather the key names over SSH (never the values), then audit. Hosts/keys per INFRA.md:

K=~/.ssh/thermograph_agent_ed25519
# sudo: /etc/thermograph.env is root-owned (0640). On prod `agent` can read it
# directly too, but sudo works uniformly on both boxes.
#
# The character class must allow DIGITS. This was `^[A-Z_]+=` until 2026-07-24,
# which silently skipped every key whose name contains a digit — in this estate
# that is all six *_S3_* keys. Prod audited as 26 keys against 32 real ones, and
# the audit reported the S3 credentials as missing from both hosts. They were
# present the whole time, and the phantom was independently "confirmed" twice by
# people re-running the same pattern. An audit that under-reports is worse than
# no audit: the invented finding sends someone to provision a credential that
# already exists, and it was briefly recorded as a root cause of a real bug.
# The match still stops at the `=`, so no value is ever read.
ssh -i $K agent@169.58.46.181 'sudo grep -oE "^[A-Z][A-Z0-9_]*=" /etc/thermograph.env' > /tmp/prod.keys  # prod
ssh -i $K agent@75.119.132.91  'sudo grep -oE "^[A-Z][A-Z0-9_]*=" /etc/thermograph.env' > /tmp/beta.keys  # beta
python3 .claude/skills/key-gaps/key_gaps.py prod=/tmp/prod.keys beta=/tmp/beta.keys

Verify a SOPS cutover matches live

Before flipping a host onto the vault, confirm the rendered set equals the live set — mix the two source types for the same box:

python3 .claude/skills/key-gaps/key_gaps.py live=/tmp/prod.keys vault=deploy/secrets/prod.yaml

The Cross-environment drift section should report none for tracked keys.

Reading the output

  • CRITICAL — a required, self-generating secret is missing (THERMOGRAPH_AUTH_SECRET, THERMOGRAPH_VAPID_PRIVATE_KEY/_PUBLIC_KEY). Missing here is the worst case: the app silently mints a new value on boot, invalidating every login session and push subscription. Fix before deploying.
  • MISSING — a required secret is absent (POSTGRES_PASSWORD, REGISTRY_TOKEN).
  • Feature GAP — a feature is partially configured (e.g. discord-account-linking has the app id but not the client secret), so it's silently broken. A fully-unset feature is "off", not a gap.
  • Dependent-key GAP — an optional key is set but the other key it needs to do anything isn't (e.g. THERMOGRAPH_DISCORD_WEATHER_CHANNEL set with no THERMOGRAPH_DISCORD_BOT_TOKEN — discord.py gates the post on both). Asymmetric, unlike a feature group: the prerequisite key is fine set alone (the bot token alone already enables DMs).
  • drift — a tracked key is in some environments but not others; often intentional, but worth a glance.

Keeping it current

The secret manifest (which keys are required / self-generating / grouped into features / dependent on another key) lives at the top of key_gaps.py in REQUIRED, OPTIONAL_SELF_GEN, FEATURE_GROUPS, DEPENDENT_OPTIONAL, and STANDALONE_OPTIONAL. When a new THERMOGRAPH_* credential is added to deploy/thermograph.env.example / deploy/secrets/, add it there too — check the code path that reads it (os.environ.get call site) to see whether it's truly required, self-generating, part of an all-or-nothing feature, or only meaningful alongside another key, rather than guessing from the name.