Repository navigation
Stage an owner role's SCRAM-SHA-256 verifier, never its password - #156
Merged
Merged
Conversation
The cloudfall_postgresql role staged the plaintext owner password on the host and sent ALTER ROLE ... PASSWORD with it; psql substitutes the value into the statement text, so the server received the plaintext and could log it (log_statement ddl or all, a failing statement) or show it in pg_stat_activity. The controller now computes the SCRAM-SHA-256 verifier with the standard library (fresh 16-byte salt, 4096 iterations, SASLprep as pg_saslprep does) in PostgreSQL's stored format and stages it in the same 0600 file; PostgreSQL stores a verifier as it is. Every path that runs services.yml goes through staged_password_files, so all switch. Answers and receipts still name the reference, the key and the password's SHA-256 (D-9). Fixes #140 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
cloudfall_postgresqlstaged an owner role's plaintext password on the host and ranALTER ROLE ... PASSWORD :'cloudfall_password'with it. psql substitutes the variable into the statement text, so the server received the plaintext and could write it to its log (log_statement = 'ddl'/'all', or a failing statement) or show it inpg_stat_activity.The controller now stages the password's SCRAM-SHA-256 verifier instead (D-9, pending in #155):
cloudfall.secrets.scram_sha256_verifier(password, *, salt=None)computes PostgreSQL's stored format,SCRAM-SHA-256$4096:<salt>$<StoredKey>:<ServerKey>, with the standard library: a fresh 16-bytesecrets.token_bytessalt, PBKDF2-HMAC-SHA256 at 4096 iterations, and the password prepared with SASLprep the waypg_saslprepdoes it (ASCII passwords as they are; for others, non-ASCII spaces map to a space, map-to-nothing characters drop, NFKC; a prohibited, unassigned or bidi-invalid result falls back to the password as given, as PostgreSQL does), so a client still logs in with a non-ASCII passwordstaged_password_fileswrites that verifier to the same0600file in the same0700private temp dir, removed when the run ends. Every path that runsservices.yml(theconverge_servicesMCP tool,cloudfall migrate,operator approve/run,cloudfall-engine playbook run services.yml) goes through it, so all switchowner_password.ymlis unchanged in what it runs: psql reads the staged file and sendsALTER ROLE ... PASSWORDwith the verifier, which PostgreSQL stores as it is. The comment and two task names now say it stages the verifier.SET password_encryptionis left in place: it no longer matters, and it does no harmdocs/secrets-guide.mdnow say only the verifier reaches the hostPlan
scram_sha256_verifierin secrets.py andstaged_password_fileswriting it, then the task's comments and names, the spec text, and the changelog fragmentTest plan
sdk/tests/test_secrets.pytests: the verifier with RFC 7677's salt for passwordpencilauthenticates RFC 7677's published SCRAM-SHA-256 exchange (the proof recovers a ClientKey hashing to StoredKey, and ServerKey reproduces the RFC's ServerSignature), an independent known vector; a fresh salt per call, 16 bytes, recomputing from the verifier's salt and iterations verifies the password and not another, and the verifier never contains the password; SASLprep cases (no-break space, zero-width space, soft hyphen, NFKC, a prohibited character kept, ASCII control kept); an empty salt is refused. These failed on main (import error) before the fixsdk/tests/test_owned_postgresql_converge.py: the converge, operator approve and migrate paths now assert the staged file holds a verifier of the password, never the password (they asserted the plaintext before)engine/tests/test_postgresql_owner_role.py: the localhost replay stages a verifier; psql reads exactly it from the0600file, the file is removed, and neither the password nor the verifier is in Ansible output, psql's stdin or argvuv run ruff check .,uv run mypy,uv run pytest engine/tests/test_postgresql_owner_role.py engine/tests/test_services_passwords.py sdk/tests/(662 passed),ANSIBLE_CONFIG=engine/ansible/ansible.cfg ansible-lint engine/ansible(0 failures). The full suite is left to CIFixes #140
🤖 Generated with Claude Code