From c88660119ca7e26995f0cbe6b0efdaa1e3db8668 Mon Sep 17 00:00:00 2001 From: Victor Prechtel Date: Wed, 21 Jan 2026 02:32:00 -0600 Subject: [PATCH 1/2] feat: Add configurable password policy for PostgresUser --- README.md | 12 ++ api/v1alpha1/postgresuser_types.go | 27 +++ api/v1alpha1/zz_generated.deepcopy.go | 20 ++ charts/ext-postgres-operator/Chart.yaml | 4 +- .../templates/operator.yaml | 16 ++ charts/ext-postgres-operator/values.yaml | 10 + .../db.movetokube.com_postgresusers.yaml | 26 +++ .../controller/postgresuser_controller.go | 24 ++- pkg/config/config.go | 78 +++++++- pkg/utils/random.go | 175 +++++++++++++++++- pkg/utils/random_test.go | 133 +++++++++++++ 11 files changed, 510 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 8f5d8bd39..7e5097d09 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,18 @@ Set environment variables in [`config/manager/operator.yaml`](config/manager/ope | `POSTGRES_INSTANCE` | Operator identity for multi-instance deployments. | (empty) | | `KEEP_SECRET_NAME` | Use user-provided secret names instead of auto-generated ones. | disabled | +### Password Policy Configuration + +| Name | Description | Default | +| --- | --- | --- | +| `POSTGRES_DEFAULT_PASSWORD_LENGTH` | Length of the generated password. | `15` | +| `POSTGRES_DEFAULT_PASSWORD_MIN_LOWER` | Minimum number of lowercase characters. | `0` | +| `POSTGRES_DEFAULT_PASSWORD_MIN_UPPER` | Minimum number of uppercase characters. | `0` | +| `POSTGRES_DEFAULT_PASSWORD_MIN_NUMERIC` | Minimum number of numeric characters. | `0` | +| `POSTGRES_DEFAULT_PASSWORD_MIN_SPECIAL` | Minimum number of special characters. | `0` | +| `POSTGRES_DEFAULT_PASSWORD_EXCLUDE_CHARS` | Characters to exclude from the generated password. | (empty) | +| `POSTGRES_DEFAULT_PASSWORD_ENSURE_FIRST_LETTER` | Ensure the password starts with a letter. | `false` | + > **Note:** > If enabling `KEEP_SECRET_NAME`, ensure there are no secret name conflicts in your namespace to avoid reconcile loops. diff --git a/api/v1alpha1/postgresuser_types.go b/api/v1alpha1/postgresuser_types.go index a2d49d1a6..5ce7218cb 100644 --- a/api/v1alpha1/postgresuser_types.go +++ b/api/v1alpha1/postgresuser_types.go @@ -23,11 +23,38 @@ type PostgresUserSpec struct { // +optional AWS *PostgresUserAWSSpec `json:"aws,omitempty"` // +optional + PasswordPolicy *PasswordPolicy `json:"passwordPolicy,omitempty"` + // +optional Annotations map[string]string `json:"annotations,omitempty"` // +optional Labels map[string]string `json:"labels,omitempty"` } +// PasswordPolicy defines the complexity requirements for the generated password +type PasswordPolicy struct { + // +optional + // Length of the password. Defaults to 15 if not set. + Length int `json:"length,omitempty"` + // +optional + // Minimum number of lowercase characters + MinLower int `json:"minLower,omitempty"` + // +optional + // Minimum number of uppercase characters + MinUpper int `json:"minUpper,omitempty"` + // +optional + // Minimum number of numeric characters + MinNumeric int `json:"minNumeric,omitempty"` + // +optional + // Minimum number of special characters + MinSpecial int `json:"minSpecial,omitempty"` + // +optional + // Characters to explicitly exclude from generation + ExcludeChars string `json:"excludeChars,omitempty"` + // +optional + // Ensure the first character is a letter (a-z, A-Z) + EnsureFirstLetter bool `json:"ensureFirstLetter,omitempty"` +} + // PostgresUserAWSSpec encapsulates AWS specific configuration toggles. type PostgresUserAWSSpec struct { // +optional diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index c21128086..9133ededd 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -8,6 +8,21 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PasswordPolicy) DeepCopyInto(out *PasswordPolicy) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PasswordPolicy. +func (in *PasswordPolicy) DeepCopy() *PasswordPolicy { + if in == nil { + return nil + } + out := new(PasswordPolicy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Postgres) DeepCopyInto(out *Postgres) { *out = *in @@ -222,6 +237,11 @@ func (in *PostgresUserSpec) DeepCopyInto(out *PostgresUserSpec) { *out = new(PostgresUserAWSSpec) **out = **in } + if in.PasswordPolicy != nil { + in, out := &in.PasswordPolicy, &out.PasswordPolicy + *out = new(PasswordPolicy) + **out = **in + } if in.Annotations != nil { in, out := &in.Annotations, &out.Annotations *out = make(map[string]string, len(*in)) diff --git a/charts/ext-postgres-operator/Chart.yaml b/charts/ext-postgres-operator/Chart.yaml index 1407a4fd0..8ef057118 100644 --- a/charts/ext-postgres-operator/Chart.yaml +++ b/charts/ext-postgres-operator/Chart.yaml @@ -8,5 +8,5 @@ description: | type: application -version: 3.0.0 -appVersion: "2.4.0" +version: 3.1.0 +appVersion: "2.5.0" diff --git a/charts/ext-postgres-operator/templates/operator.yaml b/charts/ext-postgres-operator/templates/operator.yaml index 38d075622..5701b7d94 100644 --- a/charts/ext-postgres-operator/templates/operator.yaml +++ b/charts/ext-postgres-operator/templates/operator.yaml @@ -62,6 +62,22 @@ spec: valueFrom: fieldRef: fieldPath: metadata.name + {{- if .Values.postgres.passwordPolicy }} + - name: POSTGRES_DEFAULT_PASSWORD_LENGTH + value: {{ .Values.postgres.passwordPolicy.length | default 15 | quote }} + - name: POSTGRES_DEFAULT_PASSWORD_MIN_LOWER + value: {{ .Values.postgres.passwordPolicy.minLower | default 0 | quote }} + - name: POSTGRES_DEFAULT_PASSWORD_MIN_UPPER + value: {{ .Values.postgres.passwordPolicy.minUpper | default 0 | quote }} + - name: POSTGRES_DEFAULT_PASSWORD_MIN_NUMERIC + value: {{ .Values.postgres.passwordPolicy.minNumeric | default 0 | quote }} + - name: POSTGRES_DEFAULT_PASSWORD_MIN_SPECIAL + value: {{ .Values.postgres.passwordPolicy.minSpecial | default 0 | quote }} + - name: POSTGRES_DEFAULT_PASSWORD_EXCLUDE_CHARS + value: {{ .Values.postgres.passwordPolicy.excludeChars | default "" | quote }} + - name: POSTGRES_DEFAULT_PASSWORD_ENSURE_FIRST_LETTER + value: {{ .Values.postgres.passwordPolicy.ensureFirstLetter | default "false" | quote }} + {{- end }} {{- range $key, $value := .Values.env }} - name: {{ $key }} value: {{ $value | quote }} diff --git a/charts/ext-postgres-operator/values.yaml b/charts/ext-postgres-operator/values.yaml index 2d79cc112..b0541c7d5 100644 --- a/charts/ext-postgres-operator/values.yaml +++ b/charts/ext-postgres-operator/values.yaml @@ -90,6 +90,16 @@ postgres: # default database to use default_database: "postgres" + # Default password policy for generated passwords + passwordPolicy: + length: 15 + minLower: 0 + minUpper: 0 + minNumeric: 0 + minSpecial: 0 + excludeChars: "" + ensureFirstLetter: false + # Volumes to add to the pod. volumes: [] diff --git a/config/crd/bases/db.movetokube.com_postgresusers.yaml b/config/crd/bases/db.movetokube.com_postgresusers.yaml index 14b8d6e04..a997acd8d 100644 --- a/config/crd/bases/db.movetokube.com_postgresusers.yaml +++ b/config/crd/bases/db.movetokube.com_postgresusers.yaml @@ -61,6 +61,32 @@ spec: additionalProperties: type: string type: object + passwordPolicy: + description: PasswordPolicy defines the complexity requirements for + PostgresUser generated passwords + properties: + ensureFirstLetter: + description: Ensure the first character is a letter (a-z, A-Z) + type: boolean + excludeChars: + description: Characters to explicitly exclude from generation + type: string + length: + description: Length of the password. Defaults to 15 if not set. + type: integer + minLower: + description: Minimum number of lowercase characters + type: integer + minNumeric: + description: Minimum number of numeric characters + type: integer + minSpecial: + description: Minimum number of special characters + type: integer + minUpper: + description: Minimum number of uppercase characters + type: integer + type: object privileges: description: List of privileges to grant to this user type: string diff --git a/internal/controller/postgresuser_controller.go b/internal/controller/postgresuser_controller.go index aeb0dc8bc..44fa52cc6 100644 --- a/internal/controller/postgresuser_controller.go +++ b/internal/controller/postgresuser_controller.go @@ -31,6 +31,7 @@ type PostgresUserReconciler struct { pg postgres.PG pgHost string pgUriArgs string + pgPassPolicy utils.PostgresPassPolicy instanceFilter string keepSecretName bool // use secret name as defined in PostgresUserSpec cloudProvider config.CloudProvider @@ -44,6 +45,7 @@ func NewPostgresUserReconciler(mgr manager.Manager, cfg *config.Cfg, pg postgres pg: pg, pgHost: cfg.PostgresHost, pgUriArgs: cfg.PostgresUriArgs, + pgPassPolicy: cfg.PostgresPassPolicy, instanceFilter: cfg.AnnotationFilter, keepSecretName: cfg.KeepSecretName, cloudProvider: cfg.CloudProvider, @@ -118,7 +120,27 @@ func (r *PostgresUserReconciler) Reconcile(ctx context.Context, req ctrl.Request var ( role, login string ) - password, err := utils.GetSecureRandomString(15) + // Determine password policy + passConfig := r.pgPassPolicy + + // Override with instance specific policy if present + if instance.Spec.PasswordPolicy != nil { + pp := instance.Spec.PasswordPolicy + if pp.Length > 0 { + passConfig.Length = pp.Length + } + passConfig.MinLower = pp.MinLower + passConfig.MinUpper = pp.MinUpper + passConfig.MinNumeric = pp.MinNumeric + passConfig.MinSpecial = pp.MinSpecial + passConfig.ExcludeChars = pp.ExcludeChars + if pp.EnsureFirstLetter { + passConfig.EnsureFirstLetter = true + } + } + + password, err := utils.GeneratePassword(passConfig) + if err != nil { return r.requeue(ctx, instance, err) } diff --git a/pkg/config/config.go b/pkg/config/config.go index 5b63497c2..1f7f13ff1 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "net/url" "strconv" "strings" @@ -10,14 +11,15 @@ import ( ) type Cfg struct { - PostgresHost string - PostgresUser string - PostgresPass string - PostgresUriArgs string - PostgresDefaultDb string - CloudProvider CloudProvider - AnnotationFilter string - KeepSecretName bool + PostgresHost string + PostgresUser string + PostgresPass string + PostgresUriArgs string + PostgresPassPolicy utils.PostgresPassPolicy + PostgresDefaultDb string + CloudProvider CloudProvider + AnnotationFilter string + KeepSecretName bool } var ( @@ -47,6 +49,12 @@ func Get() *Cfg { if value, err := strconv.ParseBool(utils.GetEnv("KEEP_SECRET_NAME")); err == nil { config.KeepSecretName = value } + + pp, err := loadPassPolicy() + if err != nil { + panic(fmt.Errorf("failed to load password policy config: %w", err)) + } + config.PostgresPassPolicy = pp }) return config } @@ -65,3 +73,57 @@ func ParseCloudProvider(s string) CloudProvider { return CloudProviderNone } } + +// loadPassPolicy parses password policy configuration from environment variables. +func loadPassPolicy() (utils.PostgresPassPolicy, error) { + var pp utils.PostgresPassPolicy + var err error + + if pp.Length, err = parseIntEnv("POSTGRES_DEFAULT_PASSWORD_LENGTH"); err != nil { + return pp, err + } + if pp.MinLower, err = parseIntEnv("POSTGRES_DEFAULT_PASSWORD_MIN_LOWER"); err != nil { + return pp, err + } + if pp.MinUpper, err = parseIntEnv("POSTGRES_DEFAULT_PASSWORD_MIN_UPPER"); err != nil { + return pp, err + } + if pp.MinNumeric, err = parseIntEnv("POSTGRES_DEFAULT_PASSWORD_MIN_NUMERIC"); err != nil { + return pp, err + } + if pp.MinSpecial, err = parseIntEnv("POSTGRES_DEFAULT_PASSWORD_MIN_SPECIAL"); err != nil { + return pp, err + } + + pp.ExcludeChars = utils.GetEnv("POSTGRES_DEFAULT_PASSWORD_EXCLUDE_CHARS") + + if pp.EnsureFirstLetter, err = parseBoolEnv("POSTGRES_DEFAULT_PASSWORD_ENSURE_FIRST_LETTER"); err != nil { + return pp, err + } + + return pp, nil +} + +func parseIntEnv(key string) (int, error) { + val := utils.GetEnv(key) + if val == "" { + return 0, nil + } + i, err := strconv.Atoi(val) + if err != nil { + return 0, fmt.Errorf("invalid integer for %s: %v", key, err) + } + return i, nil +} + +func parseBoolEnv(key string) (bool, error) { + val := utils.GetEnv(key) + if val == "" { + return false, nil + } + b, err := strconv.ParseBool(val) + if err != nil { + return false, fmt.Errorf("invalid boolean for %s: %v", key, err) + } + return b, nil +} diff --git a/pkg/utils/random.go b/pkg/utils/random.go index 9efc466d3..daf7dc83f 100644 --- a/pkg/utils/random.go +++ b/pkg/utils/random.go @@ -1,10 +1,21 @@ package utils -import cryptorand "crypto/rand" -import "math/rand" -import "math/big" +import ( + cryptorand "crypto/rand" + "errors" + "fmt" + "math/big" + "math/rand" + "strings" +) -var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") +var ( + letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") + lowerRunes = []rune("abcdefghijklmnopqrstuvwxyz") + upperRunes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ") + numericRunes = []rune("0123456789") + specialRunes = []rune("!@#$%^&*") +) func GetRandomString(length int) string { b := make([]rune, length) @@ -27,3 +38,159 @@ func GetSecureRandomString(length int) (string, error) { return string(b), nil } + +type PostgresPassPolicy struct { + Length int + MinLower int + MinUpper int + MinNumeric int + MinSpecial int + ExcludeChars string + EnsureFirstLetter bool +} + +// GeneratePassword creates a random password based on the provided configuration. +// It ensures that the generated password meets the length and complexity requirements +// defined in PostgresPassPolicy. +func GeneratePassword(config PostgresPassPolicy) (string, error) { + if config.Length == 0 { + config.Length = 15 // Default length + } + + if err := validatePasswordConfig(config); err != nil { + return "", err + } + + var password []rune + + chars, err := collectRequiredChars(config) + if err != nil { + return "", err + } + password = append(password, chars...) + + chars, err = fillRemainingChars(config, len(password)) + if err != nil { + return "", err + } + password = append(password, chars...) + + shuffleRunes(password) + + if config.EnsureFirstLetter { + if err := ensureFirstCharIsLetter(password); err != nil { + return "", err + } + } + + return string(password), nil +} + +func validatePasswordConfig(config PostgresPassPolicy) error { + requiredLength := config.MinLower + config.MinUpper + config.MinNumeric + config.MinSpecial + if config.Length < requiredLength { + return fmt.Errorf("password length %d is less than required minimum characters %d", config.Length, requiredLength) + } + return nil +} + +func collectRequiredChars(config PostgresPassPolicy) ([]rune, error) { + var password []rune + + categories := []struct { + source []rune + count int + }{ + {lowerRunes, config.MinLower}, + {upperRunes, config.MinUpper}, + {numericRunes, config.MinNumeric}, + {specialRunes, config.MinSpecial}, + } + + for _, cat := range categories { + if cat.count > 0 { + chars, err := pickRandomChars(cat.source, cat.count, config.ExcludeChars) + if err != nil { + return nil, err + } + password = append(password, chars...) + } + } + return password, nil +} + +func fillRemainingChars(config PostgresPassPolicy, currentLength int) ([]rune, error) { + remaining := config.Length - currentLength + if remaining <= 0 { + return nil, nil + } + + // Default pool is Alphanumeric (Legacy behavior) + pool := append([]rune(nil), letterRunes...) + + // If Special characters are required (Min > 0), add them to the pool for the remaining characters too + if config.MinSpecial > 0 { + pool = append(pool, specialRunes...) + } + + return pickRandomChars(pool, remaining, config.ExcludeChars) +} + +func pickRandomChars(source []rune, count int, exclude string) ([]rune, error) { + filtered := filterRunes(source, exclude) + if len(filtered) == 0 && count > 0 { + return nil, errors.New("no characters available for required category after exclusion") + } + + res := make([]rune, count) + for i := 0; i < count; i++ { + num, err := cryptorand.Int(cryptorand.Reader, big.NewInt(int64(len(filtered)))) + if err != nil { + return nil, err + } + res[i] = filtered[num.Int64()] + } + return res, nil +} + +func shuffleRunes(runes []rune) { + rand.Shuffle(len(runes), func(i, j int) { + runes[i], runes[j] = runes[j], runes[i] + }) +} + +func ensureFirstCharIsLetter(password []rune) error { + if len(password) == 0 { + return nil + } + + isLetter := func(r rune) bool { + return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') + } + + if isLetter(password[0]) { + return nil + } + + for i := 1; i < len(password); i++ { + if isLetter(password[i]) { + password[0], password[i] = password[i], password[0] + return nil + } + } + + return errors.New("cannot ensure first letter: no letters in generated password") +} + +func filterRunes(runes []rune, exclude string) []rune { + if exclude == "" { + return runes + } + var res []rune + for _, r := range runes { + if !strings.ContainsRune(exclude, r) { + res = append(res, r) + } + } + return res +} diff --git a/pkg/utils/random_test.go b/pkg/utils/random_test.go index 418f454b7..0342b1b5d 100644 --- a/pkg/utils/random_test.go +++ b/pkg/utils/random_test.go @@ -59,5 +59,138 @@ var _ = Describe("Random String Utils", func() { Expect(result2).To(HaveLen(15)) }) }) + + When("using GeneratePassword with default configuration", func() { + It("should mimic legacy behavior (15 chars, alphanumeric)", func() { + // verify default behavior (15 chars, alphanumeric) when config is empty + config := PostgresPassPolicy{} + result, err := GeneratePassword(config) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(15)) + Expect(result).To(MatchRegexp("^[a-zA-Z0-9]+$")) + }) + }) + + When("using GeneratePassword with specific length", func() { + It("should return a string of the specified length", func() { + config := PostgresPassPolicy{Length: 20} + result, err := GeneratePassword(config) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(20)) + }) + + It("should remain alphanumeric if no complexity requirements are set", func() { + // verify that the default pool remains alphanumeric when MinSpecial is 0 + config := PostgresPassPolicy{Length: 50} + result, err := GeneratePassword(config) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(50)) + // Should strictly be alphanumeric + Expect(result).To(MatchRegexp("^[a-zA-Z0-9]+$")) + // Should NOT contain special characters + Expect(result).NotTo(MatchRegexp(`[!@#$%^&*]`)) + }) + + It("should validate length vs requirements", func() { + // verify error when length is less than sum of minimum requirements (20 > 10) + config := PostgresPassPolicy{ + Length: 10, + MinLower: 5, + MinUpper: 5, + MinNumeric: 5, + MinSpecial: 5, + } + _, err := GeneratePassword(config) + Expect(err).To(HaveOccurred()) + }) + }) + + When("using GeneratePassword with complexity requirements", func() { + It("should satisfy minimum character counts", func() { + config := PostgresPassPolicy{ + Length: 50, + MinLower: 5, + MinUpper: 5, + MinNumeric: 5, + MinSpecial: 5, + } + // Generate multiple times to be sure + for i := 0; i < 10; i++ { + result, err := GeneratePassword(config) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(MatchRegexp(`[a-z].*[a-z].*[a-z].*[a-z].*[a-z]`)) + Expect(result).To(MatchRegexp(`[A-Z].*[A-Z].*[A-Z].*[A-Z].*[A-Z]`)) + Expect(result).To(MatchRegexp(`[0-9].*[0-9].*[0-9].*[0-9].*[0-9]`)) + // verify special character count manually as they require escaping in regex + specialCount := 0 + for _, c := range result { + for _, s := range "!@#$%^&*" { + if c == s { + specialCount++ + break + } + } + } + Expect(specialCount).To(BeNumerically(">=", 5)) + } + }) + }) + + When("using GeneratePassword with exclusion", func() { + It("should not contain excluded characters", func() { + config := PostgresPassPolicy{ + Length: 100, + ExcludeChars: "abc123!@#", + } + result, err := GeneratePassword(config) + Expect(err).NotTo(HaveOccurred()) + for _, c := range result { + Expect(string(c)).NotTo(BeElementOf("a", "b", "c", "1", "2", "3", "!", "@", "#")) + } + }) + + It("should error if exclusion leaves no characters", func() { + // verify error when all characters are excluded + config := PostgresPassPolicy{ + Length: 10, + ExcludeChars: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*", + } + _, err := GeneratePassword(config) + Expect(err).To(HaveOccurred()) + }) + }) + + When("using GeneratePassword with EnsureFirstLetter", func() { + It("should make sure the password starts with a letter", func() { + // set high numeric/special counts to increase probability of non-letter first char, + // but ensure at least one letter exists for swapping + config := PostgresPassPolicy{ + Length: 10, + MinNumeric: 5, + MinSpecial: 2, + MinLower: 1, + EnsureFirstLetter: true, + } + + for i := 0; i < 50; i++ { + result, err := GeneratePassword(config) + Expect(err).NotTo(HaveOccurred()) + firstChar := rune(result[0]) + isLetter := (firstChar >= 'a' && firstChar <= 'z') || (firstChar >= 'A' && firstChar <= 'Z') + Expect(isLetter).To(BeTrue(), "Expected first char %c to be a letter", firstChar) + } + }) + + It("should error if no letters are available to start with", func() { + // verify error when numeric requirements consume entire length, leaving no room for a letter + config := PostgresPassPolicy{ + Length: 10, + MinNumeric: 10, + EnsureFirstLetter: true, + } + _, err := GeneratePassword(config) + Expect(err).To(HaveOccurred()) + }) + }) }) }) From e4e050f16e35a2d375e39f5a427f4f3dd1fdecf2 Mon Sep 17 00:00:00 2001 From: Victor Prechtel Date: Wed, 21 Jan 2026 03:27:04 -0600 Subject: [PATCH 2/2] Update readme with additional context around custom password policies for PostgresUser --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7e5097d09..739125149 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,9 @@ Set environment variables in [`config/manager/operator.yaml`](config/manager/ope | `POSTGRES_INSTANCE` | Operator identity for multi-instance deployments. | (empty) | | `KEEP_SECRET_NAME` | Use user-provided secret names instead of auto-generated ones. | disabled | +> **Note:** +> If enabling `KEEP_SECRET_NAME`, ensure there are no secret name conflicts in your namespace to avoid reconcile loops. + ### Password Policy Configuration | Name | Description | Default | @@ -84,9 +87,6 @@ Set environment variables in [`config/manager/operator.yaml`](config/manager/ope | `POSTGRES_DEFAULT_PASSWORD_EXCLUDE_CHARS` | Characters to exclude from the generated password. | (empty) | | `POSTGRES_DEFAULT_PASSWORD_ENSURE_FIRST_LETTER` | Ensure the password starts with a letter. | `false` | -> **Note:** -> If enabling `KEEP_SECRET_NAME`, ensure there are no secret name conflicts in your namespace to avoid reconcile loops. - ## Installation ### Install Using Helm (Recommended) @@ -194,6 +194,14 @@ spec: foo: "bar" # Labels to be propagated to the secrets metadata section (optional) secretTemplate: # Output secrets can be customized using standard Go templates PQ_URL: "host={{.Host}} user={{.Role}} password={{.Password}} dbname={{.Database}}" + passwordPolicy: # Specific password policy for this user (optional) + length: 20 + minLower: 1 + minUpper: 1 + minNumeric: 1 + minSpecial: 1 + excludeChars: "@" + ensureFirstLetter: true ``` This creates a user role `username-` and grants role `test-db-group`, `test-db-writer` or `test-db-reader` depending on `privileges` property. Its credentials are put in secret `my-secret-my-db-user` (unless `KEEP_SECRET_NAME` is enabled).