DatabaseUser¶
A DatabaseUser creates and manages database users with automatic credential management.
Overview¶
apiVersion: dbops.dbprovision.io/v1alpha1
kind: DatabaseUser
metadata:
name: myapp-user
spec:
instanceRef:
name: postgres-primary
username: myapp_user
passwordSecret:
generate: true
secretName: myapp-user-credentials
Database Ownership
When creating a Database with owner: <username>, create the DatabaseUser first so the role exists. The user's auto-generated Secret provides credentials for applications to access that database. See Database owner field for details.
Spec Fields¶
instanceRef (required)¶
Reference to the parent DatabaseInstance.
username (required)¶
The username in the database. Immutable after creation.
passwordSecret (optional)¶
Configuration for password management.
| Field | Type | Description |
|---|---|---|
generate |
bool | Auto-generate a secure password |
length |
int | Password length (default: 24) |
includeSpecialChars |
bool | Include special characters (default: true) |
excludeChars |
string | Characters to exclude |
secretName |
string | Name for the generated Secret |
secretTemplate |
object | Custom Secret template |
secretTemplate¶
Customize the generated Secret:
| Field | Type | Description |
|---|---|---|
labels |
map | Additional labels |
annotations |
map | Additional annotations |
type |
string | Secret type (default: Opaque) |
data |
map | Templated data keys — replaces default keys when provided |
Full Replacement Behavior
When secretTemplate.data is provided, it fully replaces the default secret keys (username, password, host, port). No merging occurs. If you need those values, include them explicitly in your template.
Template variables:
| Variable | Type | Description |
|---|---|---|
.Username |
string | The database username |
.Password |
string | The password (generated or existing) |
.Host |
string | Database host from the parent instance |
.Port |
int32 | Database port from the parent instance |
.Database |
string | Database name from the parent instance |
.SSLMode |
string | TLS mode from the parent instance (e.g., verify-full) |
.Namespace |
string | Namespace of the DatabaseUser resource |
.Name |
string | Name of the DatabaseUser resource |
.CA |
string | PEM-encoded CA certificate from instance TLS secret |
.TLSCert |
string | PEM-encoded client certificate from instance TLS secret |
.TLSKey |
string | PEM-encoded client key from instance TLS secret |
TLS Variables
.CA, .TLSCert, and .TLSKey are populated from the parent DatabaseInstance's tls.secretRef. If TLS is not enabled or the secret is missing, these fields are empty strings.
Template functions:
| Function | Description | Example |
|---|---|---|
urlEncode |
URL query-escape | {{ urlEncode .Password }} → p%40ss%21 |
urlPathEncode |
URL path-escape | {{ urlPathEncode .Database }} → my%2Fdb |
base64Encode |
Base64 encode | {{ base64Encode .CA }} |
base64Decode |
Base64 decode | {{ base64Decode .EncodedValue }} |
upper / lower |
Case conversion | {{ upper .Username }} |
title |
Capitalize first letter | {{ title .Name }} |
trim |
Trim whitespace | {{ trim .Value }} |
trimPrefix / trimSuffix |
Trim prefix/suffix | {{ trimPrefix .Host "db-" }} |
replace |
Replace all occurrences | {{ replace .Host "." "-" }} |
quote / squote |
Wrap in double/single quotes | {{ quote .Password }} |
default |
Fallback for empty values | {{ default "5432" .Port }} |
toJson |
Marshal to JSON | {{ toJson .Data }} |
join |
Join string slice | {{ join .Items "," }} |
existingPasswordSecret (optional)¶
Use an existing password instead of generating one.
| Field | Type | Description |
|---|---|---|
name |
string | Secret name |
key |
string | Key containing the password |
postgres (optional)¶
PostgreSQL-specific user configuration.
| Field | Type | Description |
|---|---|---|
connectionLimit |
int | Max concurrent connections |
inherit |
bool | Inherit role privileges (default: true) |
login |
bool | Can login (default: true) |
createDB |
bool | Can create databases |
createRole |
bool | Can create roles |
superuser |
bool | Is superuser |
replication |
bool | Can replicate |
bypassRLS |
bool | Bypass row-level security |
inRoles |
array | Roles to inherit from |
configParameters |
map | Session parameters |
mysql (optional)¶
MySQL-specific user configuration.
| Field | Type | Description |
|---|---|---|
maxQueriesPerHour |
int | Query limit (0 = unlimited) |
maxUpdatesPerHour |
int | Update limit |
maxConnectionsPerHour |
int | Connection limit per hour |
maxUserConnections |
int | Max concurrent connections |
authPlugin |
string | Authentication plugin |
requireSSL |
bool | Require SSL connections |
allowedHosts |
array | Allowed connection hosts |
accountLocked |
bool | Lock the account |
Status¶
| Field | Description |
|---|---|
phase |
Current phase |
conditions |
Detailed conditions |
credentialsSecretRef |
Reference to the credentials Secret |
Examples¶
Basic User with Generated Password¶
apiVersion: dbops.dbprovision.io/v1alpha1
kind: DatabaseUser
metadata:
name: myapp-user
spec:
instanceRef:
name: postgres-primary
username: myapp_user
passwordSecret:
generate: true
secretName: myapp-user-credentials
User with Custom Secret Template¶
apiVersion: dbops.dbprovision.io/v1alpha1
kind: DatabaseUser
metadata:
name: myapp-user
spec:
instanceRef:
name: postgres-primary
username: myapp_user
passwordSecret:
generate: true
length: 32
secretName: myapp-user-credentials
secretTemplate:
labels:
app: myapp
data:
DATABASE_URL: "postgresql://{{ urlEncode .Username }}:{{ urlEncode .Password }}@{{ .Host }}:{{ .Port }}/myapp?sslmode={{ default \"prefer\" .SSLMode }}"
JDBC_URL: "jdbc:postgresql://{{ .Host }}:{{ .Port }}/myapp?user={{ .Username }}&password={{ .Password }}"
MySQL DSN for Go Applications¶
secretTemplate:
data:
DSN: "{{ urlEncode .Username }}:{{ urlEncode .Password }}@tcp({{ .Host }}:{{ .Port }})/{{ .Database }}?tls=required&parseTime=true"
.env File Format¶
secretTemplate:
data:
.env: |
DB_HOST={{ .Host }}
DB_PORT={{ .Port }}
DB_USER={{ .Username }}
DB_PASS={{ .Password }}
DB_NAME={{ .Database }}
DB_SSLMODE={{ default "disable" .SSLMode }}
mTLS-Enabled Application¶
Distribute TLS certificates alongside connection credentials:
apiVersion: dbops.dbprovision.io/v1alpha1
kind: DatabaseUser
metadata:
name: secure-app-user
spec:
instanceRef:
name: postgres-mtls # Instance with TLS enabled
username: secure_app
passwordSecret:
generate: true
secretName: secure-app-credentials
secretTemplate:
data:
DATABASE_URL: "postgresql://{{ urlEncode .Username }}:{{ urlEncode .Password }}@{{ .Host }}:{{ .Port }}/{{ .Database }}?sslmode=verify-full&sslrootcert=/certs/ca.crt&sslcert=/certs/tls.crt&sslkey=/certs/tls.key"
ca.crt: "{{ .CA }}"
tls.crt: "{{ .TLSCert }}"
tls.key: "{{ .TLSKey }}"
User with Existing Password¶
apiVersion: dbops.dbprovision.io/v1alpha1
kind: DatabaseUser
metadata:
name: myapp-user
spec:
instanceRef:
name: postgres-primary
username: myapp_user
existingPasswordSecret:
name: my-existing-password
key: password
PostgreSQL User with Role Membership¶
apiVersion: dbops.dbprovision.io/v1alpha1
kind: DatabaseUser
metadata:
name: myapp-user
spec:
instanceRef:
name: postgres-primary
username: myapp_user
passwordSecret:
generate: true
secretName: myapp-user-credentials
postgres:
connectionLimit: 20
inRoles:
- readonly_role
- analytics_role
configParameters:
search_path: "app,public"
statement_timeout: "30000"
MySQL User with Host Restrictions¶
apiVersion: dbops.dbprovision.io/v1alpha1
kind: DatabaseUser
metadata:
name: myapp-mysql-user
spec:
instanceRef:
name: mysql-primary
username: myapp_user
passwordSecret:
generate: true
secretName: myapp-mysql-credentials
mysql:
maxUserConnections: 50
authPlugin: caching_sha2_password
allowedHosts:
- "10.0.0.%"
- "192.168.1.%"
Generated Secret Structure¶
When passwordSecret.generate: true, the operator creates a Secret.
Default keys (no secretTemplate.data):
apiVersion: v1
kind: Secret
metadata:
name: myapp-user-credentials
ownerReferences:
- apiVersion: dbops.dbprovision.io/v1alpha1
kind: DatabaseUser
name: myapp-user
type: Opaque
data:
username: <base64>
password: <base64>
host: <base64>
port: <base64>
Custom keys (with secretTemplate.data):
apiVersion: v1
kind: Secret
metadata:
name: myapp-user-credentials
type: Opaque
data:
# Only the keys you defined in secretTemplate.data
DATABASE_URL: <base64-encoded rendered template>
ca.crt: <base64-encoded PEM certificate>
Deletion¶
Deletion Protection¶
DatabaseUser uses annotations (not spec fields) for deletion protection:
To remove protection:
Deletion Policy¶
DatabaseUser uses an annotation for deletion policy:
- Retain (default): CR is deleted, but the database user is kept
- Delete: Database user is dropped, then CR is deleted
Deletion Flow¶
- Deletion protection check: Blocked if annotation
deletion-protection: "true"is present, unlessforce-deleteannotation is set. - Child dependency check: If DatabaseGrant children reference this user, deletion is blocked (Phase=Failed, condition=DependenciesExist) unless force-delete is set.
- Cascade confirmation: When force-delete is set and grants exist, the operator enters
PhasePendingDeletionand requires theconfirm-force-deleteannotation with the hash fromstatus.deletionConfirmation.hash. See Force Delete with Children. - Deletion policy: The annotation
dbops.dbprovision.io/deletion-policycontrols whether the external user is dropped. - Force-delete and external failures: If the user drop fails and force-delete is set, the operator continues with finalizer removal.
Password Rotation¶
Automatic Rotation¶
The operator supports scheduled password rotation via spec.passwordRotation. When enabled, the controller evaluates the cron schedule during each reconciliation and performs rotation when due.
apiVersion: dbops.dbprovision.io/v1alpha1
kind: DatabaseUser
metadata:
name: myapp-user
spec:
username: myapp
instanceRef:
name: prod-pg
passwordRotation:
enabled: true
schedule: "0 0 1 * *" # Monthly at midnight
strategy: role-inheritance
serviceRole:
name: svc_myapp # Optional, defaults to svc_<username>
autoCreate: true
userNaming: "{{.Username}}_{{.Date}}"
oldUserPolicy:
action: delete # delete | disable | retain
gracePeriodDays: 7
ownershipCheck: true
Role-Inheritance Strategy (PostgreSQL / CockroachDB)¶
The role-inheritance strategy creates a NOLOGIN service role that holds all privileges, and LOGIN users that inherit from it:
- Service role created —
svc_myapp(NOLOGIN, INHERIT). Holds privilege grants. - New login user created — e.g.,
myapp_20260315with membership insvc_myapp. - Password generated — stored in the credentials Secret (same Secret key layout).
- Old user deprecated — added to
status.rotation.pendingDeletionwith a grace period. - Old user cleaned up — after
gracePeriodDays, the action (delete/disable/retain) is applied.
Existing connections using the old user continue working during the grace period. New connections use the updated Secret.
Configuration Reference¶
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Enable automatic rotation |
schedule |
string | — | Cron expression (5-field: min hour dom month dow) |
strategy |
string | role-inheritance |
Rotation strategy (role-inheritance or dual-password) |
serviceRole.name |
string | svc_<username> |
Service role name for PostgreSQL |
serviceRole.autoCreate |
bool | true |
Create service role if it doesn't exist |
userNaming |
string | {{.Username}}_{{.Date}} |
Template for rotated usernames. Variables: {{.Username}}, {{.Date}}, {{.Timestamp}} |
oldUserPolicy.action |
string | delete |
What to do with old users: delete, disable, or retain |
oldUserPolicy.gracePeriodDays |
int | 7 |
Days before applying the action |
oldUserPolicy.ownershipCheck |
bool | true |
Block deletion if user owns database objects |
Status Tracking¶
Rotation state is tracked in status.rotation:
| Field | Description |
|---|---|
lastRotatedAt |
Timestamp of last rotation |
nextRotationAt |
Scheduled next rotation |
activeUser |
Currently active database username |
serviceRole |
Service role name |
pendingDeletion |
List of users awaiting cleanup, with status (pending/blocked/deleted) |
Old User Cleanup¶
After the grace period expires, the configured action is applied:
- delete: Drops the user from the database. Blocked if
ownershipCheckis enabled and the user owns objects — status showsblockedwith the REASSIGN command. - disable: Sets NOLOGIN on the user, preventing new connections but preserving the role for audit.
- retain: No action taken; the user remains as-is.
Manual Rotation (Fallback)¶
The operator also automatically recovers when a DatabaseUser's credentials Secret is deleted:
- The credentials Secret is deleted (manually or by an external process).
- The
Owns(&corev1.Secret{})Watch detects the deletion and triggers an immediate reconciliation. - The controller generates a new password, calls
SetPassword()to sync it to the database, and recreates the Secret viaensureCredentialsSecret(). - A
SecretRegeneratedevent is emitted on the DatabaseUser resource.
The operator will regenerate the password and recreate the Secret within seconds.
Troubleshooting Rotation¶
Rotation not triggering:
- Verify
spec.passwordRotation.enabled: true - Check the cron expression is valid (5-field format)
- Look for
InvalidRotationScheduleevents on the resource
Old user stuck in pending:
- Check
status.rotation.pendingDeletion[].status— ifblocked, the user owns objects - Run the REASSIGN command from
status.rotation.pendingDeletion[].resolution - Or set
ownershipCheck: falseto skip the check
Troubleshooting¶
User stuck in Pending¶
- Verify the DatabaseInstance is Ready
- Check the credentials Secret exists (if using existing)
Password not generated¶
- Check if
passwordSecret.generate: trueis set - Verify
secretNameis specified - Check for errors in operator logs
Cannot connect with generated password¶
- Verify the Secret contains the correct password
- Check the user was created in the database
- For PostgreSQL, verify pg_hba.conf allows the connection