Skip to content

Commit e284739

Browse files
committed
feat: Add configurable password policy for PostgresUser
1 parent 615a675 commit e284739

11 files changed

Lines changed: 510 additions & 15 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,18 @@ Set environment variables in [`config/manager/operator.yaml`](config/manager/ope
7272
| `POSTGRES_INSTANCE` | Operator identity for multi-instance deployments. | (empty) |
7373
| `KEEP_SECRET_NAME` | Use user-provided secret names instead of auto-generated ones. | disabled |
7474

75+
### Password Policy Configuration
76+
77+
| Name | Description | Default |
78+
| --- | --- | --- |
79+
| `POSTGRES_DEFAULT_PASSWORD_LENGTH` | Length of the generated password. | `15` |
80+
| `POSTGRES_DEFAULT_PASSWORD_MIN_LOWER` | Minimum number of lowercase characters. | `0` |
81+
| `POSTGRES_DEFAULT_PASSWORD_MIN_UPPER` | Minimum number of uppercase characters. | `0` |
82+
| `POSTGRES_DEFAULT_PASSWORD_MIN_NUMERIC` | Minimum number of numeric characters. | `0` |
83+
| `POSTGRES_DEFAULT_PASSWORD_MIN_SPECIAL` | Minimum number of special characters. | `0` |
84+
| `POSTGRES_DEFAULT_PASSWORD_EXCLUDE_CHARS` | Characters to exclude from the generated password. | (empty) |
85+
| `POSTGRES_DEFAULT_PASSWORD_ENSURE_FIRST_LETTER` | Ensure the password starts with a letter. | `false` |
86+
7587
> **Note:**
7688
> If enabling `KEEP_SECRET_NAME`, ensure there are no secret name conflicts in your namespace to avoid reconcile loops.
7789

api/v1alpha1/postgresuser_types.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,38 @@ type PostgresUserSpec struct {
2323
// +optional
2424
AWS *PostgresUserAWSSpec `json:"aws,omitempty"`
2525
// +optional
26+
PasswordPolicy *PasswordPolicy `json:"passwordPolicy,omitempty"`
27+
// +optional
2628
Annotations map[string]string `json:"annotations,omitempty"`
2729
// +optional
2830
Labels map[string]string `json:"labels,omitempty"`
2931
}
3032

33+
// PasswordPolicy defines the complexity requirements for the generated password
34+
type PasswordPolicy struct {
35+
// +optional
36+
// Length of the password. Defaults to 15 if not set.
37+
Length int `json:"length,omitempty"`
38+
// +optional
39+
// Minimum number of lowercase characters
40+
MinLower int `json:"minLower,omitempty"`
41+
// +optional
42+
// Minimum number of uppercase characters
43+
MinUpper int `json:"minUpper,omitempty"`
44+
// +optional
45+
// Minimum number of numeric characters
46+
MinNumeric int `json:"minNumeric,omitempty"`
47+
// +optional
48+
// Minimum number of special characters
49+
MinSpecial int `json:"minSpecial,omitempty"`
50+
// +optional
51+
// Characters to explicitly exclude from generation
52+
ExcludeChars string `json:"excludeChars,omitempty"`
53+
// +optional
54+
// Ensure the first character is a letter (a-z, A-Z)
55+
EnsureFirstLetter bool `json:"ensureFirstLetter,omitempty"`
56+
}
57+
3158
// PostgresUserAWSSpec encapsulates AWS specific configuration toggles.
3259
type PostgresUserAWSSpec struct {
3360
// +optional

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

charts/ext-postgres-operator/Chart.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ description: |
88
99
type: application
1010

11-
version: 3.0.0
12-
appVersion: "2.4.0"
11+
version: 3.1.0
12+
appVersion: "2.5.0"

charts/ext-postgres-operator/templates/operator.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,22 @@ spec:
6262
valueFrom:
6363
fieldRef:
6464
fieldPath: metadata.name
65+
{{- if .Values.postgres.passwordPolicy }}
66+
- name: POSTGRES_DEFAULT_PASSWORD_LENGTH
67+
value: {{ .Values.postgres.passwordPolicy.length | default 15 | quote }}
68+
- name: POSTGRES_DEFAULT_PASSWORD_MIN_LOWER
69+
value: {{ .Values.postgres.passwordPolicy.minLower | default 0 | quote }}
70+
- name: POSTGRES_DEFAULT_PASSWORD_MIN_UPPER
71+
value: {{ .Values.postgres.passwordPolicy.minUpper | default 0 | quote }}
72+
- name: POSTGRES_DEFAULT_PASSWORD_MIN_NUMERIC
73+
value: {{ .Values.postgres.passwordPolicy.minNumeric | default 0 | quote }}
74+
- name: POSTGRES_DEFAULT_PASSWORD_MIN_SPECIAL
75+
value: {{ .Values.postgres.passwordPolicy.minSpecial | default 0 | quote }}
76+
- name: POSTGRES_DEFAULT_PASSWORD_EXCLUDE_CHARS
77+
value: {{ .Values.postgres.passwordPolicy.excludeChars | default "" | quote }}
78+
- name: POSTGRES_DEFAULT_PASSWORD_ENSURE_FIRST_LETTER
79+
value: {{ .Values.postgres.passwordPolicy.ensureFirstLetter | default "false" | quote }}
80+
{{- end }}
6581
{{- range $key, $value := .Values.env }}
6682
- name: {{ $key }}
6783
value: {{ $value | quote }}

charts/ext-postgres-operator/values.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ postgres:
9090
# default database to use
9191
default_database: "postgres"
9292

93+
# Default password policy for generated passwords
94+
passwordPolicy:
95+
length: 15
96+
minLower: 0
97+
minUpper: 0
98+
minNumeric: 0
99+
minSpecial: 0
100+
excludeChars: ""
101+
ensureFirstLetter: false
102+
93103
# Volumes to add to the pod.
94104
volumes: []
95105

config/crd/bases/db.movetokube.com_postgresusers.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,32 @@ spec:
6161
additionalProperties:
6262
type: string
6363
type: object
64+
passwordPolicy:
65+
description: PasswordPolicy defines the complexity requirements for
66+
PostgresUser generated passwords
67+
properties:
68+
ensureFirstLetter:
69+
description: Ensure the first character is a letter (a-z, A-Z)
70+
type: boolean
71+
excludeChars:
72+
description: Characters to explicitly exclude from generation
73+
type: string
74+
length:
75+
description: Length of the password. Defaults to 15 if not set.
76+
type: integer
77+
minLower:
78+
description: Minimum number of lowercase characters
79+
type: integer
80+
minNumeric:
81+
description: Minimum number of numeric characters
82+
type: integer
83+
minSpecial:
84+
description: Minimum number of special characters
85+
type: integer
86+
minUpper:
87+
description: Minimum number of uppercase characters
88+
type: integer
89+
type: object
6490
privileges:
6591
description: List of privileges to grant to this user
6692
type: string

internal/controller/postgresuser_controller.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type PostgresUserReconciler struct {
3131
pg postgres.PG
3232
pgHost string
3333
pgUriArgs string
34+
pgPassPolicy utils.PostgresPassPolicy
3435
instanceFilter string
3536
keepSecretName bool // use secret name as defined in PostgresUserSpec
3637
cloudProvider config.CloudProvider
@@ -44,6 +45,7 @@ func NewPostgresUserReconciler(mgr manager.Manager, cfg *config.Cfg, pg postgres
4445
pg: pg,
4546
pgHost: cfg.PostgresHost,
4647
pgUriArgs: cfg.PostgresUriArgs,
48+
pgPassPolicy: cfg.PostgresPassPolicy,
4749
instanceFilter: cfg.AnnotationFilter,
4850
keepSecretName: cfg.KeepSecretName,
4951
cloudProvider: cfg.CloudProvider,
@@ -118,7 +120,27 @@ func (r *PostgresUserReconciler) Reconcile(ctx context.Context, req ctrl.Request
118120
var (
119121
role, login string
120122
)
121-
password, err := utils.GetSecureRandomString(15)
123+
// Determine password policy
124+
passConfig := r.pgPassPolicy
125+
126+
// Override with instance specific policy if present
127+
if instance.Spec.PasswordPolicy != nil {
128+
pp := instance.Spec.PasswordPolicy
129+
if pp.Length > 0 {
130+
passConfig.Length = pp.Length
131+
}
132+
passConfig.MinLower = pp.MinLower
133+
passConfig.MinUpper = pp.MinUpper
134+
passConfig.MinNumeric = pp.MinNumeric
135+
passConfig.MinSpecial = pp.MinSpecial
136+
passConfig.ExcludeChars = pp.ExcludeChars
137+
if pp.EnsureFirstLetter {
138+
passConfig.EnsureFirstLetter = true
139+
}
140+
}
141+
142+
password, err := utils.GeneratePassword(passConfig)
143+
122144
if err != nil {
123145
return r.requeue(ctx, instance, err)
124146
}

pkg/config/config.go

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package config
22

33
import (
4+
"fmt"
45
"net/url"
56
"strconv"
67
"strings"
@@ -10,14 +11,15 @@ import (
1011
)
1112

1213
type Cfg struct {
13-
PostgresHost string
14-
PostgresUser string
15-
PostgresPass string
16-
PostgresUriArgs string
17-
PostgresDefaultDb string
18-
CloudProvider CloudProvider
19-
AnnotationFilter string
20-
KeepSecretName bool
14+
PostgresHost string
15+
PostgresUser string
16+
PostgresPass string
17+
PostgresUriArgs string
18+
PostgresPassPolicy utils.PostgresPassPolicy
19+
PostgresDefaultDb string
20+
CloudProvider CloudProvider
21+
AnnotationFilter string
22+
KeepSecretName bool
2123
}
2224

2325
var (
@@ -47,6 +49,12 @@ func Get() *Cfg {
4749
if value, err := strconv.ParseBool(utils.GetEnv("KEEP_SECRET_NAME")); err == nil {
4850
config.KeepSecretName = value
4951
}
52+
53+
pp, err := loadPassPolicy()
54+
if err != nil {
55+
panic(fmt.Errorf("failed to load password policy config: %w", err))
56+
}
57+
config.PostgresPassPolicy = pp
5058
})
5159
return config
5260
}
@@ -65,3 +73,57 @@ func ParseCloudProvider(s string) CloudProvider {
6573
return CloudProviderNone
6674
}
6775
}
76+
77+
// loadPassPolicy parses password policy configuration from environment variables.
78+
func loadPassPolicy() (utils.PostgresPassPolicy, error) {
79+
var pp utils.PostgresPassPolicy
80+
var err error
81+
82+
if pp.Length, err = parseIntEnv("POSTGRES_DEFAULT_PASSWORD_LENGTH"); err != nil {
83+
return pp, err
84+
}
85+
if pp.MinLower, err = parseIntEnv("POSTGRES_DEFAULT_PASSWORD_MIN_LOWER"); err != nil {
86+
return pp, err
87+
}
88+
if pp.MinUpper, err = parseIntEnv("POSTGRES_DEFAULT_PASSWORD_MIN_UPPER"); err != nil {
89+
return pp, err
90+
}
91+
if pp.MinNumeric, err = parseIntEnv("POSTGRES_DEFAULT_PASSWORD_MIN_NUMERIC"); err != nil {
92+
return pp, err
93+
}
94+
if pp.MinSpecial, err = parseIntEnv("POSTGRES_DEFAULT_PASSWORD_MIN_SPECIAL"); err != nil {
95+
return pp, err
96+
}
97+
98+
pp.ExcludeChars = utils.GetEnv("POSTGRES_DEFAULT_PASSWORD_EXCLUDE_CHARS")
99+
100+
if pp.EnsureFirstLetter, err = parseBoolEnv("POSTGRES_DEFAULT_PASSWORD_ENSURE_FIRST_LETTER"); err != nil {
101+
return pp, err
102+
}
103+
104+
return pp, nil
105+
}
106+
107+
func parseIntEnv(key string) (int, error) {
108+
val := utils.GetEnv(key)
109+
if val == "" {
110+
return 0, nil
111+
}
112+
i, err := strconv.Atoi(val)
113+
if err != nil {
114+
return 0, fmt.Errorf("invalid integer for %s: %v", key, err)
115+
}
116+
return i, nil
117+
}
118+
119+
func parseBoolEnv(key string) (bool, error) {
120+
val := utils.GetEnv(key)
121+
if val == "" {
122+
return false, nil
123+
}
124+
b, err := strconv.ParseBool(val)
125+
if err != nil {
126+
return false, fmt.Errorf("invalid boolean for %s: %v", key, err)
127+
}
128+
return b, nil
129+
}

0 commit comments

Comments
 (0)