Skip to content

The owner password reaches PostgreSQL in plaintext inside ALTER ROLE #140

Closed

Description

@shipmill-romamo

Found in the review of #139 (S-006, owned PostgreSQL databases).

The problem

engine/ansible/roles/cloudfall_postgresql/tasks/owner_password.yml sets the owner role's password with psql's ALTER ROLE ... PASSWORD :'var'. psql substitutes the value into the statement text before it sends it, so the PostgreSQL server receives the plaintext password inside the SQL. With password_encryption = scram-sha-256 the stored value is a SCRAM verifier, but while the statement runs the text is visible in pg_stat_activity, and it is written to the PostgreSQL log when log_statement is ddl or all, or when the statement fails (log_min_error_statement).

The Ansible side is clean: the value is in no argv, inventory, Ansible output, answer or receipt, and the host file is removed in an always block. The leak is only on the PostgreSQL server's side.

Reproduce

On a host converged with an owned database, set log_statement = 'ddl', run cloudfall converge services again, and read the PostgreSQL log: the ALTER ROLE billing PASSWORD '...' line holds the password.

Not reproduced live yet (no PostgreSQL in the review environment); this is documented PostgreSQL behaviour.

Proposed fix

Compute the SCRAM-SHA-256 verifier (SCRAM-SHA-256$4096:<salt>$<StoredKey>:<ServerKey>) on the controller, stage the verifier instead of the password, and send ALTER ROLE ... PASSWORD '<verifier>'. PostgreSQL stores a pre-hashed verifier as is, so neither the log nor pg_stat_activity ever holds the password. Keep the answer's reference, key and SHA-256 as they are. A regression test can check the staged file holds a verifier that verifies the password, never the password.

Touches engine/ansible/roles/cloudfall_postgresql/tasks/owner_password.yml and the controller staging in sdk/src/cloudfall/secrets.py / engine/src/cloudfall_engine/cli.py.

Related: #81, #122, spec docs/specs/006-standalone-postgresql.md (S-006-6).

Activity

  1. shipmill-romamo commented on Oct 27, 2026

    @shipmill-romamo
    ContributorAuthor

    Triage: implement, for the next release candidate. Waits on your decision below.

    Confirmed on main (389f0af): engine/ansible/roles/cloudfall_postgresql/tasks/owner_password.yml sends ALTER ROLE ... PASSWORD :'var', and psql puts the value into the statement text, so the server sees the plaintext password. Today it never reaches Ansible output, the inventory, an answer or a receipt (S-006-6 holds), but PostgreSQL itself can log it (log_statement = 'ddl'/'all', or a failing statement) and shows it in pg_stat_activity while it runs.

    The plan: compute the SCRAM-SHA-256 verifier on the controller (random 16-byte salt, 4096 iterations, the format PostgreSQL stores), stage the verifier instead of the password in the same 0600 file, and send ALTER ROLE ... PASSWORD '<verifier>'. PostgreSQL stores a pre-hashed verifier as it is, so the password never reaches the server. Answers keep naming the reference, key and the value's SHA-256. A test checks that the staged file holds a verifier that verifies the password and never the password. One side effect: the stored verifier gets a new salt on every converge, so ALTER ROLE always reports a change. Re-applying the same password already did that.

  2. shipmill-romamo commented on Oct 27, 2026

    @shipmill-romamo
    ContributorAuthor

    @romamo Decision needed: should owner role passwords reach PostgreSQL only as a SCRAM verifier computed on the controller, so the server never sees the plaintext?

    1. Stage a SCRAM verifier (recommended): the password never appears in the PostgreSQL log or pg_stat_activity. It costs a small SCRAM helper on the controller (standard library hashlib.pbkdf2_hmac and hmac, no new dependency).
    2. Keep ALTER ROLE ... PASSWORD with the plaintext and document turning off log_statement for DDL: no code change, but any host with statement logging on keeps the password in its logs.
    3. Won't fix: close this issue as accepted risk.

    Reply here with a number or your own answer; shipmill takes this up on the tick after your reply.

  3. romamo commented on Oct 30, 2026

    @romamo
    Collaborator

    Decision by @romamo, relayed by Claude Code

    1. Stage a SCRAM verifier computed on the controller, so the server never sees the plaintext password.
  4. shipmill-romamo commented on Oct 30, 2026

    @shipmill-romamo
    ContributorAuthor

    Triage: implement, on @romamo's answer (option 1). Security fix, no goal needed.

    The plan stands as in the triage comment above: compute the SCRAM-SHA-256 verifier on the controller (random 16-byte salt, 4096 iterations, PostgreSQL's stored format), stage the verifier instead of the password in the same 0600 file, and send ALTER ROLE ... PASSWORD '<verifier>'. Answers keep naming the reference, key and the value's SHA-256. A regression test checks the staged file holds a verifier that verifies the password and never the password. The rule is recorded as D-9 in docs/decisions.md. An implementer is on it; the pull request will be linked here.

  5. shipmill-romamo commented on Oct 30, 2026

    @shipmill-romamo
    ContributorAuthor

    Pull request: #156. The controller stages the password's SCRAM-SHA-256 verifier (cloudfall.secrets.scram_sha256_verifier) in the same 0600 file, and every path that runs services.yml goes through it. The decision record is #155 (D-9).

    One addition to the plan: the password is prepared with SASLprep the way PostgreSQL's pg_saslprep does, so a non-ASCII password still logs in; ASCII passwords are hashed as given. The verifier is checked against RFC 7677's published exchange, not yet against a live PostgreSQL server.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions