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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ AboutBits PostgreSQL Operator is a Kubernetes operator that helps you manage Pos

> **Note:** Kubernetes 1.29+ is required due to the use of CRD CEL validations (GA in 1.29, Beta in 1.25).

The admin role used by the operator does not need to be a superuser.
Managed services such as AWS RDS, Amazon Aurora, Google Cloud SQL, and Azure Database for PostgreSQL are supported.
See [Admin privileges](docs/cluster-connection.md#admin-privileges).

## Architecture

```
Expand Down
16 changes: 15 additions & 1 deletion docs/cluster-connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ Use this option when the credentials should be mounted as a file inside the oper
|--------|----------|-----------------------------------------------------------------------------------------|----------|
| `path` | `string` | The absolute path inside the operator Pod to the file containing the admin credentials. | Yes |


#### File format

The file must contain JSON with the following fields:
Expand Down Expand Up @@ -66,6 +65,21 @@ The value of `adminSecretFileRef.path` is the `mountPath` plus the name of the f

See [Using a file reference](#using-a-file-reference-adminsecretfileref) in the examples for a complete setup with each volume source.

## Admin privileges

The admin role does not need to be a superuser. This makes the operator usable with managed services such as AWS RDS, Amazon Aurora, Google Cloud SQL, or Azure Database for PostgreSQL, where no superuser is available.

| Custom Resource | Required privilege of the admin role |
|---------------------------------------|-----------------------------------------------------------------|
| `ClusterConnection` | `LOGIN` |
| `Role` | `CREATEROLE` |
| `Database` | `CREATEDB` |
| `Schema`, `Grant`, `DefaultPrivilege` | Ownership of, or the matching privileges on, the target objects |

The master user of the managed services above has `LOGIN`, `CREATEDB`, and `CREATEROLE`. See [Role](role.md#non-superuser-admins) for the limits that apply to a non-superuser admin.

The operator reads role state from the public view `pg_roles`. It does not read `pg_authid` or `pg_shadow`, which these services deny.

## Examples

### Using a Kubernetes Secret (`adminSecretRef`)
Expand Down
64 changes: 57 additions & 7 deletions docs/role.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ The `Role` Custom Resource Definition (CRD) manages PostgreSQL roles (users).

## Spec

| Field | Type | Description | Required | Mutable |
|---------------------|---------------|-------------------------------------------------------------------------------------|----------|---------|
| `clusterRef` | `ResourceRef` | Reference to the `ClusterConnection` to use. | Yes | Yes |
| `name` | `string` | The name of the role to create in the database. | Yes | No |
| `comment` | `string` | A comment to add to the role. | No | Yes |
| `passwordSecretRef` | `ResourceRef` | Reference to a secret containing the password for the role to make it a LOGIN role. | No | Yes |
| `flags` | `RoleFlags` | Flags and attributes for the role. | No | Yes |
| Field | Type | Description | Required | Mutable |
|----------------------|---------------|-------------------------------------------------------------------------------------|----------|---------|
| `clusterRef` | `ResourceRef` | Reference to the `ClusterConnection` to use. | Yes | Yes |
| `name` | `string` | The name of the role to create in the database. | Yes | No |
| `comment` | `string` | A comment to add to the role. | No | Yes |
| `passwordSecretRef` | `ResourceRef` | Reference to a secret containing the password for the role to make it a LOGIN role. | No | Yes |
| `passwordEncryption` | `string` | How the password is sent to PostgreSQL: `scram-sha-256` (default) or `server`. | No | Yes |
| `flags` | `RoleFlags` | Flags and attributes for the role. | No | Yes |

### ResourceRef (`clusterRef` and `passwordSecretRef`)

Expand Down Expand Up @@ -45,6 +46,55 @@ The operator uses the presence of the `passwordSecretRef` field to determine if
- **Login Role (User)**: If `passwordSecretRef` is specified, the role is created with the `LOGIN` attribute. It uses the password from the referenced secret.
- **No-Login Role (Group)**: If `passwordSecretRef` is omitted, the role is created with the `NOLOGIN` attribute. This is useful for creating roles that serve as groups for permissions.

### Password handling

The operator does not read the password hash from `pg_authid`.
That catalog is readable by superusers only, and managed PostgreSQL services of cloud providers, such as AWS RDS, Google Cloud SQL, or Azure Database for PostgreSQL, revoke it from every role, including the master user.

Instead, the operator stores a keyed fingerprint of the password it applied last in `status.passwordFingerprint`.
On each reconcile it compares the referenced Secret against that fingerprint. When they differ, the operator runs `ALTER ROLE ... PASSWORD`.

The fingerprint is an `HMAC-SHA256`. Its key is random and private to the operator.
The operator generates the key once and stores it in a Secret named `postgresql-operator-password-fingerprint-key` in its own namespace. A reader of the `Role` status learns nothing about the password without that key.
The Secret name is set by the configuration property `postgresql-operator.password-fingerprint.secret-name`, for example through the environment variable `POSTGRESQL_OPERATOR_PASSWORD_FINGERPRINT_SECRET_NAME`.
The Helm chart grants `create` on Secrets through a `Role` and `RoleBinding` in the operator namespace only. The `ClusterRole` of the operator keeps read access to Secrets.

**Consequences:**

- The Secret is the source of truth. A password change made directly in PostgreSQL is not detected.
- If the key Secret is lost, the operator generates a new key and re-applies every `Role` password once.
- After the upgrade to the version that introduced the fingerprint, every existing `Role` gets one password update, because its status has no fingerprint yet.

**Threat model:**

The fingerprint is a keyed hash. It is not a password hash with a work factor, such as `bcrypt` or `PBKDF2`.

- Without the key, the fingerprint reveals nothing about the password. A reader of the `Role` status alone cannot attack it. This includes a backup of etcd and a user with `get` on `Role` resources.
- With the key, an attacker can test password guesses offline at `HMAC-SHA256` speed. Treat the key Secret as a credential. Keep the number of principals with `get` on Secrets in the operator namespace small.
- An attacker who reads the key Secret can usually also read the password Secrets that the `Role` resources reference. In that case the fingerprint adds no exposure that the attacker does not already have.
- The operator never writes the password, its `SCRAM-SHA-256` verifier, or the key into the `Role` status.
- To retire a key, delete the key Secret. The operator generates a new key and re-applies every `Role` password once. Every old fingerprint then becomes meaningless.

#### `passwordEncryption`

| Value | Behavior |
|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `scram-sha-256` | **Default**. The operator computes the `SCRAM-SHA-256` verifier itself and sends only the verifier. The cleartext password never reaches the server, its statement log, or extensions such as `pgaudit`. |
| `server` | The operator sends the cleartext password. The server hashes it according to its `password_encryption` setting. Use this for clients that only support MD5 authentication. |

If the Secret already contains an `MD5` or `SCRAM-SHA-256` verifier, the operator forwards it unchanged in both modes.

**Note:**
A pre-hashed password bypasses server-side password policies. The `credcheck` extension rejects it unless `credcheck.encrypted_password_allowed` is on. For example a Cloud SQL password policy does not apply to hashed passwords. Set `passwordEncryption: server` when such a policy must apply.

### Non-superuser admins

The admin role of the `ClusterConnection` does not need to be a superuser. `CREATEROLE` is sufficient for `Role` resources.
See [ClusterConnection](cluster-connection.md#admin-privileges) for the full list of privileges. The following limits apply when the admin is not a superuser:

- The flags `superuser`, `replication`, and `bypassrls` cannot be set. PostgreSQL rejects them, and the `Role` status shows the error.
- On PostgreSQL 16 and later, the admin can only alter roles on which it holds `ADMIN OPTION`. Roles created by the operator qualify. Roles created by another user do not, unless that user grants the admin `ADMIN OPTION`.

### Example

```yaml
Expand Down
1 change: 1 addition & 0 deletions generated/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jooq {
| pg_default_acl
| pg_get_userbyid
| pg_namespace
| pg_roles
| shobj_description
""".trimIndent()
excludes = """
Expand Down

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

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

Loading
Loading