From c950aa2d9712b859d1875d07291d6b16d6611d6d Mon Sep 17 00:00:00 2001 From: Lukas Piwowarski Date: Mon, 20 Jul 2026 07:15:11 -0400 Subject: [PATCH 1/2] Log (dis)connections to PostgreSQL Configure PostgreSQL to log all successful client connections (log_connectsions = on) and all events related to disconnections of the client (log_disconnections = on). --- internal/controller/assets/postgres.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/controller/assets/postgres.conf b/internal/controller/assets/postgres.conf index 2fb04d2..35c9c3f 100644 --- a/internal/controller/assets/postgres.conf +++ b/internal/controller/assets/postgres.conf @@ -7,3 +7,5 @@ ssl_key_file = '/etc/certs/tls.key' # (sslcert/sslkey) to PostgreSQL, so ssl_ca_file has no effect (even when pg_hba.conf # is correctly configured for mTLS) # ssl_ca_file = '' +log_connections = on +log_disconnections = on From 891a67d85a61a7c78a391ccc90b020694a5c4303 Mon Sep 17 00:00:00 2001 From: Lukas Piwowarski Date: Mon, 20 Jul 2026 10:20:26 -0400 Subject: [PATCH 2/2] Strengthen generateRandomString Previously, generateRandomString used crypto/rand bytes encoded with hex.EncodeToString, so every character in the output was limited to the 16 hex digits. This gave only 4 bits of entropy per character, making the generated passwords significantly weaker than their length would suggest. Replace hex encoding with an alphanumeric charset (a-z, A-Z, 0-9), which provides 62 possible values per character, enforce a minimum length of 16 characters, and guarantee that every generated password contains at least one lowercase letter, one uppercase letter, and one digit. This makes the password more secure against brute-force attacks. Update tests to cover the minimum-length error path and the new character-class requirements. --- internal/controller/common.go | 48 ++++++++++++++++++---- internal/controller/common_test.go | 66 ++++++++++++++---------------- 2 files changed, 70 insertions(+), 44 deletions(-) diff --git a/internal/controller/common.go b/internal/controller/common.go index cabde38..114cc39 100644 --- a/internal/controller/common.go +++ b/internal/controller/common.go @@ -20,10 +20,10 @@ import ( "context" "crypto/rand" _ "embed" - "encoding/hex" "encoding/json" "errors" "fmt" + "math/big" "strings" common_helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" @@ -187,16 +187,46 @@ func getDeployment(ctx context.Context, h *common_helper.Helper, name string, na return deployment, nil } -// generateRandomString generates a random hex string of the given length. +// generateRandomString generates a random alphanumeric string of the given length containing at +// least one lowercase letter, one uppercase letter, and one digit. Minimum required secretLength +// is 16. func generateRandomString(secretLength int) (string, error) { - // Ceiling division: hex.EncodeToString doubles the byte count, so we need ceil(secretLength/2) bytes. - randDataLen := (secretLength + 1) / 2 + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + const charsetLen = len(charset) + const minLength = 16 + const maxIterations = 1000 - b := make([]byte, randDataLen) - if _, err := rand.Read(b); err != nil { - return "", fmt.Errorf("failed to generate secret: %w", err) + if secretLength < minLength { + return "", fmt.Errorf("secret length must be at least %d", minLength) } - randString := hex.EncodeToString(b) - return randString[:secretLength], nil + for range maxIterations { + + randPassword := make([]byte, secretLength) + var hasLowerCase, hasUpperCase, hasNumber bool + + for passwdCharIdx := range secretLength { + idx, err := rand.Int(rand.Reader, big.NewInt(int64(charsetLen))) + if err != nil { + return "", fmt.Errorf("failed to generate secret: %w", err) + } + + passwdChar := charset[idx.Int64()] + if passwdChar >= 'a' && passwdChar <= 'z' { + hasLowerCase = true + } else if passwdChar >= 'A' && passwdChar <= 'Z' { + hasUpperCase = true + } else if passwdChar >= '0' && passwdChar <= '9' { + hasNumber = true + } + + randPassword[passwdCharIdx] = passwdChar + } + + if hasLowerCase && hasUpperCase && hasNumber { + return string(randPassword), nil + } + } + + return "", fmt.Errorf("failed to generate secret: exceeded iterations - %d", maxIterations) } diff --git a/internal/controller/common_test.go b/internal/controller/common_test.go index 59860e7..c39eace 100644 --- a/internal/controller/common_test.go +++ b/internal/controller/common_test.go @@ -21,42 +21,20 @@ import ( ) func TestGenerateRandomStringLength(t *testing.T) { + t.Run("Below minimum length returns error", func(t *testing.T) { + _, err := generateRandomString(15) + if err == nil { + t.Error("generateRandomString(15) expected error, got nil") + } + }) + tests := []struct { name string length int }{ - { - name: "Zero length", - length: 0, - }, - { - name: "Odd length 1", - length: 1, - }, - { - name: "Even length 2", - length: 2, - }, - { - name: "Odd length 7", - length: 7, - }, - { - name: "Even length 8", - length: 8, - }, - { - name: "Odd length 15", - length: 15, - }, - { - name: "Even length 16", - length: 16, - }, - { - name: "Even length 32", - length: 32, - }, + {name: "Min length 16", length: 16}, + {name: "Even length 32", length: 32}, + {name: "Odd length 33", length: 33}, } for _, tt := range tests { @@ -72,16 +50,34 @@ func TestGenerateRandomStringLength(t *testing.T) { } } -func TestGenerateRandomStringHexCharacters(t *testing.T) { +func TestGenerateRandomStringCharacters(t *testing.T) { result, err := generateRandomString(32) if err != nil { t.Fatalf("generateRandomString(32) unexpected error: %v", err) } + + var hasLower, hasUpper, hasDigit bool for i, c := range result { - if (c < '0' || c > '9') && (c < 'a' || c > 'f') { - t.Errorf("generateRandomString(32) character at index %d is %q, not a lowercase hex character", i, c) + switch { + case c >= 'a' && c <= 'z': + hasLower = true + case c >= 'A' && c <= 'Z': + hasUpper = true + case c >= '0' && c <= '9': + hasDigit = true + default: + t.Errorf("generateRandomString(32) character at index %d is %q, not alphanumeric", i, c) } } + if !hasLower { + t.Error("generateRandomString(32) result has no lowercase letter") + } + if !hasUpper { + t.Error("generateRandomString(32) result has no uppercase letter") + } + if !hasDigit { + t.Error("generateRandomString(32) result has no digit") + } } func TestGenerateRandomStringUniqueness(t *testing.T) {