From 0a18aba91cd5a4039f2fcb098a6a3fe50a032972 Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:28:16 +0200 Subject: [PATCH] fix(database): skip CREATE USER when role exists to avoid password in Postgres logs (#12538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary When `zitadel init` runs on every Helm upgrade/re-deploy, `VerifyUser` always executed `CREATE USER ... WITH PASSWORD '...'`. If the role already existed, PostgreSQL returned `42710` and logged the full statement—including the plaintext password—even though ZITADEL swallowed that error. This mirrors the existing `VerifyDatabase` catalog check: query `pg_roles` first and skip creation when the role exists. Also stops mutating the package-level `createUserStmt` when appending `WITH PASSWORD`. Fixes #12178 ## Changes - `cmd/initialise/verify_user.go`: check `SELECT EXISTS(... FROM pg_roles ...)` before `CREATE USER`; build password clause on a local statement copy - `cmd/initialise/verify_user_test.go`: cover catalog skip, catalog-check failure, and create paths with the new query ## Test plan - [x] `go test ./cmd/initialise/...` (passed) - [ ] Re-run init against an existing role and confirm Postgres logs no longer contain `CREATE USER ... WITH PASSWORD`
Open in Web Open in Cursor 
Co-authored-by: Cursor Agent Co-authored-by: Silvan --- cmd/initialise/verify_user.go | 34 ++++++++++++++++++--- cmd/initialise/verify_user_test.go | 48 ++++++++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/cmd/initialise/verify_user.go b/cmd/initialise/verify_user.go index d821216f4..a574ee9f4 100644 --- a/cmd/initialise/verify_user.go +++ b/cmd/initialise/verify_user.go @@ -6,6 +6,7 @@ import ( _ "embed" "errors" "fmt" + "strings" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -58,11 +59,36 @@ func VerifyUser(username, password string) func(context.Context, *database.DB) e logging.Info(ctx, "config.database.postgres.user.username is same as config.database.postgres.admin.username, skipping create user", "username", username) return nil } - logging.Info(ctx, "verify user", "username", username) - if password != "" { - createUserStmt += " WITH PASSWORD '" + password + "'" + + // Check if the role already exists in the catalog before attempting CREATE USER. + // This avoids PostgreSQL logging the full CREATE USER statement (including the + // plaintext password) when the role already exists on re-deploy / re-init. + var exists bool + err = db.QueryRowContext(ctx, func(r *sql.Row) error { + return r.Scan(&exists) + }, "SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname = $1)", username) + if err != nil { + return fmt.Errorf("unable to check if user exists: %w", err) + } + if exists { + logging.Info(ctx, "user already exists, skipping creation", "username", username) + return nil } - return exec(ctx, db, fmt.Sprintf(createUserStmt, username), []string{roleAlreadyExistsCode}) + logging.Info(ctx, "verify user", "username", username) + // Format the username first so the password is never part of a fmt format string + // (passwords may contain '%' which would be interpreted as format verbs). + stmt := fmt.Sprintf(createUserStmt, username) + if password != "" { + stmt += " WITH PASSWORD " + quotePostgresLiteral(password) + } + + return exec(ctx, db, stmt, []string{roleAlreadyExistsCode}) } } + +// quotePostgresLiteral returns s as a single-quoted PostgreSQL string literal, +// escaping embedded single quotes by doubling them. +func quotePostgresLiteral(s string) string { + return "'" + strings.ReplaceAll(s, "'", "''") + "'" +} diff --git a/cmd/initialise/verify_user_test.go b/cmd/initialise/verify_user_test.go index 442543265..fdda6e189 100644 --- a/cmd/initialise/verify_user_test.go +++ b/cmd/initialise/verify_user_test.go @@ -32,6 +32,9 @@ func Test_verifyUser(t *testing.T) { expectQuery("SELECT current_user", nil, []string{"current_user"}, [][]driver.Value{ {"postgres"}, }), + expectQuery("SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname = $1)", nil, []string{"exists"}, [][]driver.Value{ + {false}, + }, "zitadel-user"), expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\"", sql.ErrTxDone), ), username: "zitadel-user", @@ -46,6 +49,9 @@ func Test_verifyUser(t *testing.T) { expectQuery("SELECT current_user", nil, []string{"current_user"}, [][]driver.Value{ {"postgres"}, }), + expectQuery("SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname = $1)", nil, []string{"exists"}, [][]driver.Value{ + {false}, + }, "zitadel-user"), expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\"", nil), ), username: "zitadel-user", @@ -60,6 +66,9 @@ func Test_verifyUser(t *testing.T) { expectQuery("SELECT current_user", nil, []string{"current_user"}, [][]driver.Value{ {"postgres"}, }), + expectQuery("SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname = $1)", nil, []string{"exists"}, [][]driver.Value{ + {false}, + }, "zitadel-user"), expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\" WITH PASSWORD 'password'", nil), ), username: "zitadel-user", @@ -68,19 +77,52 @@ func Test_verifyUser(t *testing.T) { targetErr: nil, }, { - name: "already exists", + name: "correct with password containing percent and quote", args: args{ db: prepareDB(t, expectQuery("SELECT current_user", nil, []string{"current_user"}, [][]driver.Value{ {"postgres"}, }), - expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\" WITH PASSWORD 'password'", nil), + expectQuery("SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname = $1)", nil, []string{"exists"}, [][]driver.Value{ + {false}, + }, "zitadel-user"), + expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\" WITH PASSWORD 'p%''ass'", nil), ), username: "zitadel-user", - password: "", + password: "p%'ass", }, targetErr: nil, }, + { + name: "already exists in catalog, skip creation", + args: args{ + db: prepareDB(t, + expectQuery("SELECT current_user", nil, []string{"current_user"}, [][]driver.Value{ + {"postgres"}, + }), + expectQuery("SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname = $1)", nil, []string{"exists"}, [][]driver.Value{ + {true}, + }, "zitadel-user"), + ), + username: "zitadel-user", + password: "password", + }, + targetErr: nil, + }, + { + name: "catalog check fails", + args: args{ + db: prepareDB(t, + expectQuery("SELECT current_user", nil, []string{"current_user"}, [][]driver.Value{ + {"postgres"}, + }), + expectQuery("SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname = $1)", sql.ErrConnDone, []string{"exists"}, [][]driver.Value{}, "zitadel-user"), + ), + username: "zitadel-user", + password: "password", + }, + targetErr: sql.ErrConnDone, + }, { name: "same user, skip create", args: args{