Skip to content

feat: set and compare credentials without anyone reading them - #100

Merged
cnmaia merged 3 commits into
mainfrom
feat/rotation-tooling
Sep 26, 2026
Merged

cnmaia merged 3 commits into
mainfrom
feat/rotation-tooling

Conversation

@cnmaia

@cnmaia cnmaia commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

The rotation needs two things repeatedly: write a new value into an environment file, and confirm two files hold the same one. Doing that with sed and a pipe to sha1sum works until the password contains a character sed treats as syntax — & in the replacement, or the delimiter itself — which the current MinIO root password does.

The two scripts

set_env_value.py prompts with the echo off, twice, writes the value verbatim, and prints only an eight character fingerprint.

$ python3 scripts/set_env_value.py ~/environment/gatekeeper.prod.env DOI_PASSWORD
new DOI_PASSWORD:
again:
DOI_PASSWORD written to /home/datamap/environment/gatekeeper.prod.env
fingerprint: 4bd5eb57

env_fingerprint.py prints that fingerprint for variables already in a file. Matching fingerprints are how a shared secret is checked — and the only thing about a credential that is safe to put in a message.

It writes through a temporary file and os.replace, so an interrupt mid-write cannot leave a production environment file truncated, and leaves it at 0600.

The reason this PR is bigger than it looked

Exercising the setter with the real MinIO root password (it contains &, ', *) led me to check what the Makefile does with these files. It reads them with include — so they are make syntax before any container sees them. Measured, not assumed:

POSTGRES_PASSWORD=abc#def   →   make reads:  POSTGRES_PASSWORD = abc

Silently truncated. A deploy would apply a password nobody chose and report success. #, $ and backticks are now refused, with the reason in the error — and that matters now, because the human-chosen passwords in steps 1–3 of the rotation go through exactly that path.

Also here

  • The runbook switches its verification steps from grep | cut | sha1sum pipelines to these scripts.
  • A correction that missed the merge: fix: remove production credentials from a public repository #99 was merged before my last push landed, so main still says both sides "sign with" the upload token. The webapp's BFF signs (pages/api/auth/token.ts) and gatekeeper verifies. It changes nothing about the two files needing to match, but it does change the blast radius — the tokens last a day, so rotating invalidates up to 24 hours of issued ones, not only the uploads in flight.

Verification

272 unit tests pass, 22 of them new: 17 on the setter (including every character class above, and a value assigned twice, and a file with no trailing newline), 5 on the comparator.

Both scripts exercised end to end against a throwaway file with s.XZ4R9&Wj6'3*b@PNJC5m as the value — written verbatim, file left at 0600.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EQda9NZvkbStEeNU54Tqgh

cnmaia and others added 2 commits September 25, 2026 22:49
The rotation needs a new value written into an environment file, and needs to
confirm that two files hold the same one. Doing that with `sed` and a pipe to
sha1sum works until the password contains a character sed treats as syntax -
`&` in the replacement, or the delimiter itself - which the current MinIO root
password does.

set_env_value.py prompts with the echo off, writes the value verbatim through a
temporary file so an interrupted write cannot truncate the environment, leaves
the file at 0600, and prints only an eight character fingerprint.
env_fingerprint.py prints that fingerprint for variables already in a file.
Matching fingerprints are how a shared secret is checked, and the only thing
about a credential that is safe to put in a message.

It also refuses `#`, `$` and backticks. The Makefile reads these files with
`include`, so they are make syntax before any container sees them, and this is
measured rather than assumed:

    POSTGRES_PASSWORD=abc#def   ->   make reads:  POSTGRES_PASSWORD = abc

Silently. A deploy would apply a password nobody chose and report success,
which is the failure mode worth spending a guard on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQda9NZvkbStEeNU54Tqgh
I had written that both sign with it. The webapp signs a JWT in
pages/api/auth/token.ts and gatekeeper verifies - which changes nothing about
the two files needing the same value, but it does change the blast radius: the
tokens last a day, so rotating invalidates up to 24 hours of issued ones, not
only the uploads in flight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQda9NZvkbStEeNU54Tqgh
@cnmaia
cnmaia requested a review from andrenmaia as a code owner September 26, 2026 01:50
The host has two checkouts of this repository. `~/gatekeeper` is manual and
lags - it sat two commits behind while this was written - and the runner's
workspace is what the deploy actually uses.

The runbook said `cd ~/gatekeeper`. Compose read from there would quietly
reinstate the infrastructure of whatever commit that checkout happens to sit
on: the MinIO image pin, the healthchecks, the second instance. A rotation is
not the moment to discover that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQda9NZvkbStEeNU54Tqgh
@cnmaia
cnmaia merged commit a0af030 into main Sep 26, 2026
@cnmaia
cnmaia deleted the feat/rotation-tooling branch September 26, 2026 01:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant