Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ Set environment variables in [`config/manager/operator.yaml`](config/manager/ope
> **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 |
| --- | --- | --- |
| `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` |

## Installation

### Install Using Helm (Recommended)
Expand Down Expand Up @@ -182,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-<hash>` 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).
Expand Down
27 changes: 27 additions & 0 deletions api/v1alpha1/postgresuser_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions charts/ext-postgres-operator/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ description: |

type: application

version: 3.0.0
appVersion: "2.4.0"
version: 3.1.0
appVersion: "2.5.0"
16 changes: 16 additions & 0 deletions charts/ext-postgres-operator/templates/operator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
10 changes: 10 additions & 0 deletions charts/ext-postgres-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: []

Expand Down
26 changes: 26 additions & 0 deletions config/crd/bases/db.movetokube.com_postgresusers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 23 additions & 1 deletion internal/controller/postgresuser_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down
78 changes: 70 additions & 8 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"fmt"
"net/url"
"strconv"
"strings"
Expand All @@ -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 (
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Loading