temporalio / temporalio/temporal

PostgreSQL buildDSN does not URL-encode cfg.User — usernames containing ":" (e.g. Vault dynamic credentials) break authentication

Open Beginner friendly
#12,192 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
23.2k
Forks
1.9k
Avg merge
2d 8h
Merged PRs (30d)
228

Description

Expected Behavior

The Temporal server should connect to PostgreSQL regardless of URI-reserved characters in the
username. In particular, a username containing : (colon) — the shape produced by HashiCorp
Vault's database secrets engine for dynamic/per-lease credentials, e.g. entity:serv-xxxx
must authenticate correctly.

Actual Behavior

Authentication fails when the username contains :. The colon is interpreted as the
user:password separator inside the connection URL userinfo component, so the username is
truncated at the colon and PostgreSQL rejects the login (password authentication failed /
no such user).

The credentials themselves are valid — they authenticate correctly when the username does not
contain URI-reserved characters, or when the username is percent-encoded before building the DSN.

Root cause (in the source)

common/persistence/sql/sqlplugin/postgresql/session/session.go, function buildDSN:

const dsnFmt = "postgres://%v:%v@%v/%v?%v"

// ...
return fmt.Sprintf(
    dsnFmt,
    cfg.User,                    // <-- username interpolated RAW (not encoded)
    url.QueryEscape(password),   // <-- password IS escaped
    resolvedAddr,
    cfg.DatabaseName,
    tlsAttrs.Encode(),
), nil

cfg.User is interpolated raw into the postgres://<user>:<password>@... URL, while the
password is passed through url.QueryEscape. So any URI-reserved character in the username
(:, /, @, ?, #) corrupts the DSN. For entity:serv-xxxx the effective parsed username
becomes entity and serv-xxxx is misread as the start of the password.

Two observations:

  1. The username is not encoded at all — this is the primary bug.
  2. url.QueryEscape is also not strictly the correct encoder for the userinfo component
    (it is query-string encoding, e.g. it turns a space into +, and does not match RFC 3986
    userinfo rules). Building the userinfo via net/url (e.g. url.User(cfg.User) /
    url.UserPassword(cfg.User, password) and letting url.URL.String() render it) would encode
    both fields correctly. This is likely the same class of issue behind the password-side report
    #5729.
Suggested fix

Compose the DSN with net/url so both username and password are correctly percent-encoded for
the userinfo component, instead of interpolating raw values into dsnFmt. For example, build a
url.URL{ Scheme: "postgres", User: url.UserPassword(cfg.User, password), Host: resolvedAddr, Path: cfg.DatabaseName, RawQuery: tlsAttrs.Encode() } and use its String(). At minimum,
percent-encode cfg.User the same way the password is handled.

Steps to Reproduce the Problem
  1. Create a PostgreSQL login role whose name contains a colon, e.g. entity:serv-abcd, with a
    known password and access to the Temporal database.
  2. Configure the sql datastore with user: "entity:serv-abcd", the matching password, and
    the postgres plugin (pgx).
  3. Start the server → password authentication failed for user "entity" (username truncated at
    the colon).
  4. Rename the role to remove the colon → identical setup works. This isolates the failure to DSN
    composition, not the credentials.

This is the exact scenario when delivering DB credentials via Vault dynamic secrets (per-lease
usernames of the form entity:serv-...), synced into the pod by the Vault Secrets Operator.

Specifications
  • Version: v1.31.2 (also present on main — the buildDSN code above is current).
  • Plugin/driver: postgres (pgx).
  • Platform: Kubernetes; credentials delivered as a native Secret (username + password), rotated
    per Vault lease.
Related
  • #5729 — same DSN-encoding class of problem, but reported for the password. The current code
    applies url.QueryEscape to the password only; the username (cfg.User) is still
    interpolated raw, which is what this issue is about.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in common/persistence/sql/sqlplugin/postgresql/session/session.go at buildDSN, and inspect how the DSN userinfo is assembled. Use net/url to encode both username and password as userinfo rather than interpolating cfg.User raw. Done means a username such as entity:serv-abcd produces a valid PostgreSQL DSN and authenticates without truncation.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, postgres
Domain
database
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
86/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.