Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / docs/CREDENTIAL_CONSUMERS.md |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | docs/CREDENTIAL_CONSUMERS.md |
| Original SHA-256 | a78ae5f787cfcae12b4b9253a8f38365fc2ad74c969841b9fe7057feb8729c12 |
| Git blob SHA | d49fdafbfd8db192574fb422c4549ab7dcf2aa9e |
| Reading-copy SHA-256 | a78ae5f787cfcae12b4b9253a8f38365fc2ad74c969841b9fe7057feb8729c12 |
Recovered source transcript
Download complete source text ↓178 lines · 9.9 KB
# CREDENTIAL CONSUMERS: THE COMPLETE INVENTORY
Created 29 July 2026 from incident D6. **References only. No values, ever.**
## Why this file exists
The D5 rotation was planned against a list of five consumers, held in memory. Two
more existed and were found only by breaking:
- the second API node keeps its **own** `MM_DATABASE_URL` in its **own** api.env,
so it crashed on restart after the control plane rotation
- `mm-identity-sync.sh` hardcodes **two** DSNs in **two different clusters**, so a
blanket substitution would have written the control plane password into the
tenant line
A rotation enumerated from memory is a rotation that will miss something. From now
on **every rotation enumerates from this file**, and any consumer discovered
outside it is a defect in this file to be fixed in the same change.
## The clusters, because this is what caused D6
There are **three separate PostgreSQL clusters**, and roles are **cluster scoped**
while databases are not. Two databases sharing a cluster share its roles.
CLUSTER 1 M4 100.94.8.94:5432
databases: missionmatch (PRODUCTION TENANT), missionmatch_staging
THESE TWO SHARE A ROLE NAMESPACE. That is what made a staging
rehearsal change a production credential.
CLUSTER 2 megamind-agi-wsl 100.77.114.40:5433
database: missionmatch (CONTROL PLANE, authoritative identity)
CLUSTER 3 none other in service
## The roles
CLUSTER 1, M4
missionmatch_migrator PRODUCTION TENANT only, from 29 Jul
missionmatch_staging_migrator STAGING only. Created 29 Jul as the D6
structural fix. CONNECT on missionmatch is
REVOKED, proven by refusal.
missionmatch_app production tenant application role
missionmatch_compliance eeo schema, NOBYPASSRLS
CLUSTER 2, control plane
missionmatch_app
missionmatch_migrator
missionmatch_compliance
missionmatch_aioe
## THE CONSUMERS, every one
### Bubbles, 100.90.97.104
| file | variable | role | cluster |
|---|---|---|---|
| `/etc/missionmatch/api.env` | `MM_DATABASE_URL` | app | 2, control plane |
| `/etc/missionmatch/api.env` | `MM_MIGRATOR_DATABASE_URL` | migrator | 1, tenant |
| `/etc/missionmatch/api.env` | `MM_TENANT_DSN_TENANT_ACME` | app | 1, tenant |
| `/etc/missionmatch/api.env` | `MM_TENANT_DSN_TENANT_ACME_COMPLIANCE` | compliance | 1, tenant |
| `/etc/missionmatch/api.env` | `MM_COMPLIANCE_DATABASE_URL` | compliance | 1, tenant |
| `/etc/missionmatch/staging.env` | `MM_MIGRATOR_DATABASE_URL` | **staging_migrator** | 1, staging |
| `/etc/missionmatch/staging.env` | `MM_DATABASE_URL` | app | 1, staging |
| `/etc/missionmatch/staging.env` | `MM_COMPLIANCE_DATABASE_URL` | compliance | 1, staging |
| `/etc/missionmatch/backup.dsn` | bare DSN | migrator | 1, tenant |
| `/etc/missionmatch/sam-sync.env` | `MM_MIGRATOR_DATABASE_URL` | migrator | 1, tenant |
| `/etc/missionmatch/sam-sync.env` | `MM_COMPLIANCE_DATABASE_URL` | compliance | 1, tenant |
| `/etc/mm-alert.env` | `SMTP_PASS` | Gmail app password | n/a |
**The two `sam-sync.env` rows were added 29 July 2026 from finding D7**, where they
were discovered by breaking rather than by reading, which is the same way the last
two missing consumers were found. That file feeds THREE units, `mm-sam-sync`,
`mm-awards-sync` and `mm-rates-sync`, so one stale line there stops three jobs at
once and each one reports the same authentication failure, which reads like three
problems and is one. It also carries `SAM_API_KEY`, `DATA_GOV_API_KEY`,
`PDL_API_KEY`, `BLS_API_KEY`, `KAGGLE_KEY`, `HF_TOKEN` and `ONET_API_KEY`, so it is
a secrets file in its own right and not only a DSN holder.
**`backup.dsn` was already listed here and was still missed by the D5 rotation.** A
complete inventory is necessary and not sufficient: the rotation has to actually
enumerate from it. That is why step 1 of the procedure below says read this file,
and it is now the step that failed rather than a step that was absent.
### megamind-agi WSL, 100.77.114.40
| file | variable | role | cluster |
|---|---|---|---|
| `/etc/missionmatch/api.env` | `MM_DATABASE_URL` | app | 2, control plane |
| `/etc/missionmatch/db.env` | `MM_DATABASE_URL` | app | 2 |
| `/etc/missionmatch/db.env` | `MM_MIGRATOR_DATABASE_URL` | migrator | 2 |
| `/etc/missionmatch/db.env` | `MM_COMPLIANCE_DATABASE_URL` | compliance | 2 |
| `/etc/missionmatch/db.env` | `MM_AIOE_DATABASE_URL` | aioe | 2 |
| `/etc/missionmatch/backup.dsn` | bare DSN | migrator | 2 |
| `/etc/missionmatch/db.env` | `MM_TENANT_MIGRATOR_DSN_TENANT_ACME` | migrator | **1, tenant** |
| `/etc/mm-alert.env` | `SMTP_PASS` | Gmail app password | n/a |
**The trap that used to be here is gone, and it took two months to find.** Until 29
July 2026 `mm-identity-sync.sh` carried both DSNs INLINE, on two adjacent lines, one
per cluster. This file described it as a trap and left it standing: a `sed` across the
file would write the wrong password into one of them and the failure would surface
hours later as a failed sync rather than immediately.
It was never rewritten because nothing could see it. The out of repo secret sweep found
both values on its first clean run, and the script now resolves
`MM_MIGRATOR_DATABASE_URL` and `MM_TENANT_MIGRATOR_DSN_TENANT_ACME` from `db.env`,
failing by NAME if either key is absent. The two DSN rows above are now env file rows
like every other consumer, so the rotation procedure reaches them by enumeration
instead of by memory.
**The same change took both credentials out of `argv`.** The old script passed each DSN
to `psql` and to `sync-tenant-identity` as a command line flag, and `argv` is world
readable through `ps`, so any account on that host could read both migrator passwords
out of the process table for as long as a run lasted. Measured before and after: one
exposure visible during a run of the old script, zero during the repaired one. A flag is
a weaker hiding place than an environment variable, not a stronger one.
### Ops scripts, added 29 July 2026 from finding D10
**This section exists because two of the five broken consumers lived here and this file
did not mention scripts at all.** An inventory that covers env files and not the scripts
beside them is not an inventory, and the repository secret sweep cannot see any of these
paths because they are not in the repository.
| file | how it gets its credential | role | cluster |
|---|---|---|---|
| `/usr/local/sbin/mm-db-backup.sh` (Bubbles) | reads `/etc/missionmatch/backup.dsn` | migrator | 1, tenant |
| `/usr/local/sbin/mm-db-backup.sh` (Bubbles) | ssh key auth to VALKYRIE, root's own key | n/a | n/a |
| `/usr/local/sbin/mm-restore-drill.sh` (Bubbles) | reads `api.env` **since 29 Jul** | migrator | 1, tenant |
| `/usr/local/sbin/mm-identity-sync.sh` (megamind-agi) | reads `db.env` **since 29 Jul**, two keys, no argv | migrator | **1 and 2** |
| `/usr/local/sbin/mm-disk-watch.sh` (Bubbles) | none, calls `mm-alert.sh` | n/a | n/a |
| `/usr/local/sbin/mm-alert.sh` (Bubbles and megamind-agi) | reads `/etc/mm-alert.env` | Gmail app password | n/a |
| `/usr/local/sbin/mm-watch.sh` (Bubbles) | none, HTTP probes only | n/a | n/a |
| `/usr/local/sbin/mm-offsite-watch.sh` (megamind-agi) | none, HTTP probes only | n/a | n/a |
**`mm-restore-drill.sh` HARDCODED A PLAINTEXT MIGRATOR PASSWORD until 29 July.** It was
therefore a consumer no inventory listed, invisible to the repository sweep, and the D5
rotation left it holding a dead value. The consequence was that the one instrument whose
entire purpose is to prove a backup can be restored could not run at all. It now reads
`MM_MIGRATOR_DATABASE_URL` from `api.env` and refuses to start if it is absent, so it
follows every future rotation without anyone remembering it exists.
**The rule that follows: a script is a consumer.** When adding an ops script that touches a
database, add a row here in the same change. And prefer reading an env file to embedding a
value, because a consumer that reads the store follows a rotation for free while a consumer
that embeds one has to be found.
### Application accounts, not database roles
| where | what |
|---|---|
| control plane `users` table | operator, tenant superusers, recruiters |
| staging `users` table | `patriciacox6@aol.com`, `desk.admin@fg-talent.test` |
| staging `users` table, Gate C fixtures | `gatec.recruiter@fg-talent.test`, `gatec.manager@fg-talent.test` |
Rotate these with `cmd/rotate-credential`, which goes through the real pepper and
PBKDF2 path rather than reimplementing the KDF.
### Other secrets in the env files
`MM_CREDENTIAL_PEPPER`, `MM_TOTP_KEK`, `MM_CSRF_KEY`, `MM_RECOVERY_LOOKUP_KEY`,
`MM_MEGAMIND_TOKEN`, and the OpenAI key in the M4 secrets file. The first two are
**versioned** and rotate without a flag day; the rest do not, which is recorded in
the risk register rather than pretended away.
## The rotation procedure
1. Read this file. Do not enumerate from memory.
2. Confirm the cluster for every consumer. A password is only valid in its own.
3. Take a fresh backup of the affected cluster.
4. **Update every consumer FIRST**, while the old credential still works.
5. `ALTER ROLE` **last**. The moment it commits, consumers already carrying the
new value simply start working, and there is no window where nothing connects.
6. Restart anything that reads its DSN at boot. Both API nodes do.
7. Prove every consumer **by role name and status code**, never by printing a DSN.
8. Prove an **authenticated login**, which is the only thing that exercises
control plane identity resolution end to end.
9. If a consumer was found that is not in this file, **add it in the same change**.
10. **Enumerate the ops scripts too**, not only the env files. Two of the five consumers
broken by the D5 rotation were scripts, and one of them held a plaintext password that
the repository secret sweep can never see because the file is not in the repository.