Skip to content

Commit b284ed4

Browse files
committed
Use SCRAM-SHA-256 to authenticate PgBouncer
We originally used MD5 to authenticate with PostgreSQL to avoid storing a plaintext password. Requiring SCRAM during the lifetime of the Pod is better. Issue: [ch11210]
1 parent 644b0dc commit b284ed4

File tree

6 files changed

+50
-28
lines changed

6 files changed

+50
-28
lines changed

internal/pgbouncer/config.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ const (
3838
iniFileProjectionPath = "~postgres-operator.ini"
3939

4040
authFileSecretKey = "pgbouncer-users.txt" // #nosec G101 this is a name, not a credential
41-
credentialSecretKey = "pgbouncer-verifier" // #nosec G101 this is a name, not a credential
41+
passwordSecretKey = "pgbouncer-password" // #nosec G101 this is a name, not a credential
42+
verifierSecretKey = "pgbouncer-verifier" // #nosec G101 this is a name, not a credential
4243
emptyConfigMapKey = "pgbouncer-empty"
4344
iniFileConfigMapKey = "pgbouncer.ini"
4445
)
@@ -71,15 +72,15 @@ func (vs iniValueSet) String() string {
7172
}
7273

7374
// authFileContents returns a PgBouncer user database.
74-
func authFileContents(password []byte) []byte {
75+
func authFileContents(password string) []byte {
7576
// > There should be at least 2 fields, surrounded by double quotes.
7677
// > Double quotes in a field value can be escaped by writing two double quotes.
7778
// - https://www.pgbouncer.org/config.html#authentication-file-format
7879
quote := func(s string) string {
7980
return `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
8081
}
8182

82-
user1 := quote(postgresqlUser) + " " + quote(string(password)) + "\n"
83+
user1 := quote(postgresqlUser) + " " + quote(password) + "\n"
8384

8485
return []byte(user1)
8586
}

internal/pgbouncer/config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func TestAuthFileContents(t *testing.T) {
4444
t.Parallel()
4545

4646
password := `very"random`
47-
data := authFileContents([]byte(password))
47+
data := authFileContents(password)
4848
assert.Equal(t, string(data), `"_crunchypgbouncer" "very""random"`+"\n")
4949
}
5050

internal/pgbouncer/postgres.go

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ REVOKE ALL PRIVILEGES
207207
map[string]string{
208208
"username": postgresqlUser,
209209
"namespace": postgresqlSchema,
210-
"verifier": string(clusterSecret.Data[credentialSecretKey]),
210+
"verifier": string(clusterSecret.Data[verifierSecretKey]),
211211

212212
"ON_ERROR_STOP": "on", // Abort when any one statement fails.
213213
"QUIET": "on", // Do not print successful statements to stdout.
@@ -218,21 +218,29 @@ REVOKE ALL PRIVILEGES
218218
return err
219219
}
220220

221-
func generateVerifier() ([]byte, error) {
222-
v, err := util.GeneratePassword(32)
221+
func generatePassword() (plaintext, verifier string, err error) {
222+
// PgBouncer can login to PostgreSQL using either MD5 or SCRAM-SHA-256.
223+
// When using MD5, the (hashed) verifier can be stored in PgBouncer's
224+
// authentication file. When using SCRAM, the plaintext password must be
225+
// stored.
226+
// - https://www.pgbouncer.org/config.html#authentication-file-format
227+
// - https://github.com/pgbouncer/pgbouncer/issues/508#issuecomment-713339834
228+
229+
plaintext, err = util.GeneratePassword(32)
223230
if err == nil {
224-
// NOTE(cbandy): It is not possible to use a SCRAM password for the
225-
// "auth_user" account.
226-
// - https://github.com/pgbouncer/pgbouncer/issues/508#issuecomment-713339834
227-
v, err = password.NewMD5Password(postgresqlUser, v).Build()
231+
verifier, err = password.NewSCRAMPassword(plaintext).Build()
228232
}
229-
return []byte(v), err
233+
return
230234
}
231235

232-
func postgresqlHBA() postgres.HostBasedAuthentication {
233-
// PgBouncer connects over TLS using an MD5 password.
234-
// NOTE(cbandy): It is not possible to use a SCRAM password for the
235-
// "auth_user" account.
236-
// - https://github.com/pgbouncer/pgbouncer/issues/508#issuecomment-713339834
237-
return *postgres.NewHBA().User(postgresqlUser).TLS().Method("md5")
236+
func postgresqlHBAs() []postgres.HostBasedAuthentication {
237+
// PgBouncer must connect over TLS using a SCRAM password. Other network
238+
// connections are forbidden.
239+
// - https://www.postgresql.org/docs/current/auth-pg-hba-conf.html
240+
// - https://www.postgresql.org/docs/current/auth-password.html
241+
242+
return []postgres.HostBasedAuthentication{
243+
*postgres.NewHBA().User(postgresqlUser).TLS().Method("scram-sha-256"),
244+
*postgres.NewHBA().User(postgresqlUser).TCP().Method("reject"),
245+
}
238246
}

internal/pgbouncer/postgres_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,9 @@ COMMIT;`))
193193
assert.Equal(t, expected, EnableInPostgreSQL(ctx, exec, secret))
194194
}
195195

196-
func TestPostgreSQLHBA(t *testing.T) {
197-
assert.Equal(t, postgresqlHBA().String(), `hostssl all "_crunchypgbouncer" all md5`)
196+
func TestPostgreSQLHBAs(t *testing.T) {
197+
rules := postgresqlHBAs()
198+
assert.Equal(t, len(rules), 2)
199+
assert.Equal(t, rules[0].String(), `hostssl all "_crunchypgbouncer" all scram-sha-256`)
200+
assert.Equal(t, rules[1].String(), `host all "_crunchypgbouncer" all reject`)
198201
}

internal/pgbouncer/reconcile.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,23 @@ func Secret(ctx context.Context,
6262
var err error
6363
initialize.ByteMap(&outSecret.Data)
6464

65-
verifier := inSecret.Data[credentialSecretKey]
66-
67-
if err == nil && len(verifier) == 0 {
68-
verifier, err = generateVerifier()
65+
// Use the existing password and verifier. Generate both when either is missing.
66+
// NOTE(cbandy): We don't have a function to compare a plaintext password
67+
// to a SCRAM verifier.
68+
password := string(inSecret.Data[passwordSecretKey])
69+
verifier := string(inSecret.Data[verifierSecretKey])
70+
71+
if err == nil && (len(password) == 0 || len(verifier) == 0) {
72+
password, verifier, err = generatePassword()
73+
err = errors.WithStack(err)
6974
}
75+
7076
if err == nil {
71-
outSecret.Data[authFileSecretKey] = authFileContents(verifier)
72-
outSecret.Data[credentialSecretKey] = verifier
77+
// Store the SCRAM verifier alongside the plaintext password so that
78+
// later reconciles don't generate it repeatedly.
79+
outSecret.Data[authFileSecretKey] = authFileContents(password)
80+
outSecret.Data[passwordSecretKey] = []byte(password)
81+
outSecret.Data[verifierSecretKey] = []byte(verifier)
7382
}
7483

7584
if inCluster.Spec.Proxy.PGBouncer.CustomTLSSecret == nil {
@@ -216,5 +225,5 @@ func PostgreSQL(
216225
return
217226
}
218227

219-
outHBAs.Mandatory = append(outHBAs.Mandatory, postgresqlHBA())
228+
outHBAs.Mandatory = append(outHBAs.Mandatory, postgresqlHBAs()...)
220229
}

internal/pgbouncer/reconcile_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ func TestSecret(t *testing.T) {
8787
assert.DeepEqual(t, constant, existing)
8888

8989
// A password should be generated.
90+
assert.Assert(t, len(intent.Data["pgbouncer-password"]) != 0)
9091
assert.Assert(t, len(intent.Data["pgbouncer-verifier"]) != 0)
9192

9293
// The output of authFileContents should go into intent.
@@ -350,7 +351,7 @@ func TestPostgreSQL(t *testing.T) {
350351

351352
assert.DeepEqual(t, hbas,
352353
&postgres.HBAs{
353-
Mandatory: []postgres.HostBasedAuthentication{postgresqlHBA()},
354+
Mandatory: postgresqlHBAs(),
354355
},
355356
// postgres.HostBasedAuthentication has unexported fields. Call String() to compare.
356357
cmp.Transformer("", postgres.HostBasedAuthentication.String))

0 commit comments

Comments
 (0)