--- name: key-gaps description: 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 = [= ...] ``` `` 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: ```sh 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. ssh -i $K agent@169.58.46.181 'sudo grep -oE "^[A-Z_]+=" /etc/thermograph.env' > /tmp/prod.keys # prod ssh -i $K agent@75.119.132.91 'sudo grep -oE "^[A-Z_]+=" /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.