From 90d378798615471e2af55e5ac17c63ccaae33cfa Mon Sep 17 00:00:00 2001 From: matthias Date: Mon, 8 Dec 2025 14:14:58 +0100 Subject: [PATCH 1/9] optimize handling with pgbackrest containers for emptydir --- pkg/cluster/k8sres.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 8b516893..f778b879 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -882,13 +882,13 @@ func (c *Cluster) generatePodTemplate( addEmptyDirVolume(&podSpec, "exporter-tmp", "postgres-exporter", "/tmp") } - if c.OpConfig.ReadOnlyRootFilesystem != nil && *c.OpConfig.ReadOnlyRootFilesystem && !isRepoHost { + if c.OpConfig.ReadOnlyRootFilesystem != nil && *c.OpConfig.ReadOnlyRootFilesystem && !strings.Contains(spiloContainer.Name, "pgbackrest") { addRunVolume(&podSpec, "postgres-run", "postgres", "/run") addEmptyDirVolume(&podSpec, "postgres-tmp", "postgres", "/tmp") } - if c.OpConfig.ReadOnlyRootFilesystem != nil && *c.OpConfig.ReadOnlyRootFilesystem && isRepoHost { - addEmptyDirVolume(&podSpec, "pgbackrest-tmp", "pgbackrest", "/tmp") + if c.OpConfig.ReadOnlyRootFilesystem != nil && *c.OpConfig.ReadOnlyRootFilesystem && strings.Contains(spiloContainer.Name, "pgbackrest") { + addEmptyDirVolume(&podSpec, "pgbackrest-tmp", spiloContainer.Name, "/tmp") } if sharePgSocketWithSidecars != nil && *sharePgSocketWithSidecars { From aed60bcb5f51546494d612bb22fb4d22c39aefc3 Mon Sep 17 00:00:00 2001 From: matthias Date: Wed, 10 Dec 2025 07:58:52 +0100 Subject: [PATCH 2/9] optimize handling with exporter containers for emptydir --- pkg/cluster/k8sres.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index f778b879..ddd923e1 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -878,13 +878,12 @@ func (c *Cluster) generatePodTemplate( podSpec.PriorityClassName = priorityClassName } - if c.Postgresql.Spec.Monitoring != nil { - addEmptyDirVolume(&podSpec, "exporter-tmp", "postgres-exporter", "/tmp") - } - - if c.OpConfig.ReadOnlyRootFilesystem != nil && *c.OpConfig.ReadOnlyRootFilesystem && !strings.Contains(spiloContainer.Name, "pgbackrest") { + if c.OpConfig.ReadOnlyRootFilesystem != nil && *c.OpConfig.ReadOnlyRootFilesystem && spiloContainer.Name == "postgres" { addRunVolume(&podSpec, "postgres-run", "postgres", "/run") addEmptyDirVolume(&podSpec, "postgres-tmp", "postgres", "/tmp") + if c.Postgresql.Spec.Monitoring != nil { + addEmptyDirVolume(&podSpec, "exporter-tmp", "postgres-exporter", "/tmp") + } } if c.OpConfig.ReadOnlyRootFilesystem != nil && *c.OpConfig.ReadOnlyRootFilesystem && strings.Contains(spiloContainer.Name, "pgbackrest") { From ee0cdee49dba2085dd91714658fdae54ddfb456e Mon Sep 17 00:00:00 2001 From: matthias Date: Thu, 11 Dec 2025 08:03:12 +0100 Subject: [PATCH 3/9] expand tde-object in crd --- pkg/apis/cpo.opensource.cybertec.at/v1/postgresql_type.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/apis/cpo.opensource.cybertec.at/v1/postgresql_type.go b/pkg/apis/cpo.opensource.cybertec.at/v1/postgresql_type.go index 21908897..3662ed57 100644 --- a/pkg/apis/cpo.opensource.cybertec.at/v1/postgresql_type.go +++ b/pkg/apis/cpo.opensource.cybertec.at/v1/postgresql_type.go @@ -316,7 +316,8 @@ type Configuration struct { } type TDE struct { - Enable bool `json:"enable"` + Enable bool `json:"enable"` + Keybits *int32 `json:"keybits"` } // Monitoring Sidecar defines a container to be run in the same pod as the Postgres container. From e9ca73fa3e30f67949e49631e7b5ba5f0e8e4ec6 Mon Sep 17 00:00:00 2001 From: matthias Date: Thu, 11 Dec 2025 08:03:51 +0100 Subject: [PATCH 4/9] add keybits to tde and allow to create tde-secret upfront manually --- pkg/cluster/k8sres.go | 25 ++++++++++++--- pkg/cluster/k8sres_test.go | 63 ++++++++++++++++++++++++++++---------- pkg/cluster/sync.go | 14 +++++++-- 3 files changed, 79 insertions(+), 23 deletions(-) diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index ddd923e1..77726142 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -83,6 +83,11 @@ type spiloConfiguration struct { Bootstrap pgBootstrap `json:"bootstrap"` } +type TDEConfig struct { + Enabled bool + KeyBits string +} + func (c *Cluster) statefulSetName() string { return c.Name } @@ -327,7 +332,7 @@ func (c *Cluster) generateResourceRequirements( return &result, nil } -func generateSpiloJSONConfiguration(pg *cpov1.PostgresqlParam, patroni *cpov1.Patroni, opConfig *config.Config, enableTDE bool, logger *logrus.Entry) (string, error) { +func generateSpiloJSONConfiguration(pg *cpov1.PostgresqlParam, patroni *cpov1.Patroni, opConfig *config.Config, tdeOptions TDEConfig, logger *logrus.Entry) (string, error) { config := spiloConfiguration{} config.Bootstrap = pgBootstrap{} @@ -350,8 +355,9 @@ func generateSpiloJSONConfiguration(pg *cpov1.PostgresqlParam, patroni *cpov1.Pa map[string]string{"auth-local": "trust"}} } - if enableTDE { + if tdeOptions.Enabled { config.Bootstrap.Initdb = append(config.Bootstrap.Initdb, map[string]string{"encryption-key-command": "/tmp/tde.sh"}) + config.Bootstrap.Initdb = append(config.Bootstrap.Initdb, map[string]string{"key-bits": tdeOptions.KeyBits}) } initdbOptionNames := []string{} @@ -1392,11 +1398,20 @@ func (c *Cluster) generateStatefulSet(spec *cpov1.PostgresSpec) (*appsv1.Statefu } } - enableTDE := false + // Keybits must be converted to strings for initdb processing. + tdeOptions := TDEConfig{Enabled: false} if spec.TDE != nil && spec.TDE.Enable { - enableTDE = true + tdeOptions.Enabled = true + tdeOptions.KeyBits = strconv.Itoa(256) + ptr := c.Postgresql.Spec.TDE.Keybits + if ptr != nil { + val := *ptr + if val == 128 || val == 192 { + tdeOptions.KeyBits = fmt.Sprintf("%d", val) + } + } } - spiloConfiguration, err := generateSpiloJSONConfiguration(&spec.PostgresqlParam, &spec.Patroni, &c.OpConfig, enableTDE, c.logger) + spiloConfiguration, err := generateSpiloJSONConfiguration(&spec.PostgresqlParam, &spec.Patroni, &c.OpConfig, tdeOptions, c.logger) if err != nil { return nil, fmt.Errorf("could not generate Spilo JSON configuration: %v", err) } diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index 40834086..4748f1a3 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -65,26 +65,30 @@ func TestGenerateSpiloJSONConfiguration(t *testing.T) { }, k8sutil.KubernetesClient{}, cpov1.Postgresql{}, logger, eventRecorder) tests := []struct { - subtest string - pgParam *cpov1.PostgresqlParam - patroni *cpov1.Patroni - opConfig *config.Config - result string + subtest string + pgParam *cpov1.PostgresqlParam + patroni *cpov1.Patroni + opConfig *config.Config + tdeConfig TDEConfig + result string }{ { subtest: "Patroni default configuration", - pgParam: &cpov1.PostgresqlParam{PgVersion: "15"}, + pgParam: &cpov1.PostgresqlParam{PgVersion: "17"}, patroni: &cpov1.Patroni{}, opConfig: &config.Config{ Auth: config.Auth{ PamRoleName: "humans", }, }, - result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/15/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"users":{"humans":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{}}}`, + tdeConfig: TDEConfig{ + Enabled: false, + }, + result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/17/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"users":{"humans":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{}}}`, }, { subtest: "Patroni configured", - pgParam: &cpov1.PostgresqlParam{PgVersion: "15"}, + pgParam: &cpov1.PostgresqlParam{PgVersion: "17"}, patroni: &cpov1.Patroni{ InitDB: map[string]string{ "encoding": "UTF8", @@ -107,11 +111,14 @@ func TestGenerateSpiloJSONConfiguration(t *testing.T) { PamRoleName: "humans", }, }, - result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/15/bin","pg_hba":["hostssl all all 0.0.0.0/0 scram-sha-256","host all all 0.0.0.0/0 scram-sha-256"]},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"users":{"humans":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"synchronous_mode":true,"synchronous_mode_strict":true,"synchronous_node_count":1,"slots":{"permanent_logical_1":{"database":"foo","plugin":"pgoutput","type":"logical"}},"failsafe_mode":true}}}`, + tdeConfig: TDEConfig{ + Enabled: false, + }, + result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/17/bin","pg_hba":["hostssl all all 0.0.0.0/0 scram-sha-256","host all all 0.0.0.0/0 scram-sha-256"]},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"users":{"humans":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"synchronous_mode":true,"synchronous_mode_strict":true,"synchronous_node_count":1,"slots":{"permanent_logical_1":{"database":"foo","plugin":"pgoutput","type":"logical"}},"failsafe_mode":true}}}`, }, { subtest: "Patroni failsafe_mode configured globally", - pgParam: &cpov1.PostgresqlParam{PgVersion: "15"}, + pgParam: &cpov1.PostgresqlParam{PgVersion: "17"}, patroni: &cpov1.Patroni{}, opConfig: &config.Config{ Auth: config.Auth{ @@ -119,11 +126,14 @@ func TestGenerateSpiloJSONConfiguration(t *testing.T) { }, EnablePatroniFailsafeMode: util.True(), }, - result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/15/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"users":{"humans":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"failsafe_mode":true}}}`, + tdeConfig: TDEConfig{ + Enabled: false, + }, + result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/17/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"users":{"humans":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"failsafe_mode":true}}}`, }, { subtest: "Patroni failsafe_mode configured globally, disabled for cluster", - pgParam: &cpov1.PostgresqlParam{PgVersion: "15"}, + pgParam: &cpov1.PostgresqlParam{PgVersion: "17"}, patroni: &cpov1.Patroni{ FailsafeMode: util.False(), }, @@ -133,11 +143,14 @@ func TestGenerateSpiloJSONConfiguration(t *testing.T) { }, EnablePatroniFailsafeMode: util.True(), }, - result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/15/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"users":{"humans":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"failsafe_mode":false}}}`, + tdeConfig: TDEConfig{ + Enabled: false, + }, + result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/17/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"users":{"humans":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"failsafe_mode":false}}}`, }, { subtest: "Patroni failsafe_mode disabled globally, configured for cluster", - pgParam: &cpov1.PostgresqlParam{PgVersion: "15"}, + pgParam: &cpov1.PostgresqlParam{PgVersion: "17"}, patroni: &cpov1.Patroni{ FailsafeMode: util.True(), }, @@ -147,12 +160,30 @@ func TestGenerateSpiloJSONConfiguration(t *testing.T) { }, EnablePatroniFailsafeMode: util.False(), }, - result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/15/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"users":{"humans":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"failsafe_mode":true}}}`, + tdeConfig: TDEConfig{ + Enabled: false, + }, + result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/17/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"}],"users":{"humans":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{"failsafe_mode":true}}}`, + }, + { + subtest: "TDE enabled with 256 bits", + pgParam: &cpov1.PostgresqlParam{PgVersion: "17"}, + patroni: &cpov1.Patroni{}, + opConfig: &config.Config{ + Auth: config.Auth{ + PamRoleName: "humans", + }, + }, + tdeConfig: TDEConfig{ + Enabled: true, + KeyBits: "256", + }, + result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/17/bin"},"bootstrap":{"initdb":[{"auth-host":"scram-sha-256"},{"auth-local":"trust"},{"encryption-key-command": "/tmp/tde.sh"},{"key-bits":"256"}],"users":{"humans":{"password":"","options":["CREATEDB","NOLOGIN"]}},"dcs":{}}}`, }, } for _, tt := range tests { cluster.OpConfig = *tt.opConfig - result, err := generateSpiloJSONConfiguration(tt.pgParam, tt.patroni, tt.opConfig, false, logger) + result, err := generateSpiloJSONConfiguration(tt.pgParam, tt.patroni, tt.opConfig, tt.tdeConfig, logger) if err != nil { t.Errorf("Unexpected error: %v", err) } diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index 0da424ad..b39a37f4 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -1684,7 +1684,15 @@ func (c *Cluster) syncPgbackrestJob(forceRemove bool) error { func (c *Cluster) createTDESecret() error { c.logger.Info("creating TDE secret") c.setProcessName("creating TDE secret") - generatedKey := make([]byte, 16) + var bits int32 = 256 + ptr := c.Postgresql.Spec.TDE.Keybits + if ptr != nil { + val := *ptr + if val == 128 || val == 192 { + bits = val + } + } + generatedKey := make([]byte, (bits / 8)) rand.Read(generatedKey) generatedSecret := v1.Secret{ @@ -1701,10 +1709,12 @@ func (c *Cluster) createTDESecret() error { if err == nil { c.Secrets[secret.UID] = secret c.logger.Debugf("created new secret %s, namespace: %s, uid: %s", util.NameFromMeta(secret.ObjectMeta), generatedSecret.Namespace, secret.UID) + } + if k8sutil.ResourceAlreadyExists(err) { + c.logger.Warningf("TDE secret already exists, skip key generation and use existing one.") } else { return fmt.Errorf("could not create secret for TDE %s: in namespace %s: %v", util.NameFromMeta(secret.ObjectMeta), generatedSecret.Namespace, err) } - return nil } From 60619b04b23a3e97c95576d887e801e088200611 Mon Sep 17 00:00:00 2001 From: matthias Date: Thu, 11 Dec 2025 08:04:39 +0100 Subject: [PATCH 5/9] add new tde-object --- charts/postgres-operator/crds/postgresqls.yaml | 13 +++++++++++++ manifests/postgresql.crd.yaml | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index 515c0072..3d14d689 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -694,6 +694,19 @@ spec: type: string recoveryEventType: type: string + tde: + nullable: true + properties: + enable: + type: boolean + keybits: + type: integer + format: int32 + default: 256 + enum: + - 128 + - 192 + - 256 teamId: type: string tls: diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index 955d31dc..671b930b 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -692,6 +692,20 @@ spec: type: string recoveryEventType: type: string + tde: + nullable: true + properties: + enable: + type: boolean + keybits: + type: integer + format: int32 + default: 256 + enum: + - 128 + - 192 + - 256 + type: object teamId: type: string tls: From db606e188217b713e086084c5e540c9ee7350b5b Mon Sep 17 00:00:00 2001 From: matthias Date: Thu, 11 Dec 2025 08:09:04 +0100 Subject: [PATCH 6/9] add keybits to crd-docu --- docs/hugo/content/en/crd/crd-postgresql.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/hugo/content/en/crd/crd-postgresql.md b/docs/hugo/content/en/crd/crd-postgresql.md index 062f20b6..92325e82 100644 --- a/docs/hugo/content/en/crd/crd-postgresql.md +++ b/docs/hugo/content/en/crd/crd-postgresql.md @@ -57,7 +57,7 @@ weight: 331 | spiloRunAsUser | int | false | Sets the user ID which should be used in the container to run the process. This must be set to run the container without root. | | [standby](#standby) | map | false | Enables the creation of a standby cluster at the time of the creation of a new cluster | | [streams](#streams) | array | false | Enables change data capture streams for defined database tables | -| [tde](#tde) | map | false | Enables the activation of TDE if a new cluster is created | +| [tde](#tde) | map | false | Enables TDE and allows you to define options such as key bits. | | teamId | string | true | name of the team the cluster belongs to. Will be removed soon | | [tls](#tls) | map | false | Custom TLS certificate | | [tolerations](#tolerations) | array | false | a list of tolerations that apply to the cluster pods. Each element of that list is a dictionary with the following fields: @@ -256,9 +256,10 @@ key, operator, value, effect and tolerationSeconds | #### tde -| Name | Type | required | Description | -| ------------------------------ |:-------:| ---------:| ------------------:| -| enable | boolean | true | enable TDE during initDB | +| Name | Type | default | Description | +| ------------------------------ |:-------:| ---------:| -------------------------:| +| enable | boolean | false | enable TDE during initDB | +| keybits | integer | 256 | used to specify the key length in bits. Accepted values are 128, 192 and 256 (default). More Informations: [PGEE-Documentation](https://repository.cybertec.at/doc/18ee/encryption.html) | {{< back >}} From 5503a8cf60d195a2b7435354e5c0dbe715c0e15b Mon Sep 17 00:00:00 2001 From: matthias Date: Thu, 11 Dec 2025 09:28:40 +0100 Subject: [PATCH 7/9] updated tde documentation (keybits, key- and secret-handling --- docs/hugo/content/en/tde/_index.md | 51 ++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/docs/hugo/content/en/tde/_index.md b/docs/hugo/content/en/tde/_index.md index ce8a20e1..0d3767a4 100644 --- a/docs/hugo/content/en/tde/_index.md +++ b/docs/hugo/content/en/tde/_index.md @@ -27,7 +27,7 @@ Further information on TDE and PGEE can be found here: [CYBERTEC TDE](https://ww ## Securing clusters with TDE -The CYBERTEC pg operator, together with Patroni, takes over the setup and administration of the TDE functionality in conjunction with the cost-effective PGEE containers +The CYBERTEC-pg-operator, together with Patroni, takes over the setup and administration of the TDE functionality in conjunction with the cost-effective PGEE containers ### Preconditions - CYBERTEC-pgee-container @@ -36,7 +36,7 @@ The CYBERTEC pg operator, together with Patroni, takes over the setup and admini ### Deploy a TDE-Cluster Setting up a TDE cluster is basically the same as setting up a conventional cluster. -The only difference is the defined Postgres. container and the object TDE.enabled: true, which instructs the operator to initialise the database with the TDE functionality. +The only difference is the defined Postgres-container and the object TDE.enabled: true, which instructs the operator to initialise the database with the TDE functionality. ```yaml apiVersion: cpo.opensource.cybertec.at/v1 @@ -58,20 +58,65 @@ spec: memory: 500Mi tde: enable: true + keybits: 256 teamId: acid volume: size: 5Gi ``` - `dockerImage` - Must contain a PostgreSQL image of the pgee container suite - `tde.enabled`- initialises the DB with TDE +- `tde.keybits`- Defines keylength in bits. Possible Values: 128,192,256 Default: 256 {{< hint type=important >}} Please note that the activation of TDE is only possible when creating new clusters. Subsequent activation is not possible. {{< /hint >}} +### Key-Management + +For TDE, we or the operator must work with the required encryption key. The key is transferred to the Postgres containers using a secret. There are two basic options for the necessary key management. + +#### Automatic key generation (default) +If no existing secret is provided, the operator automatically generates a cryptographically secure key when creating a new cluster. This is stored in the secret in the cluster's namespace. + +It is important to note for key management that TDE allows you to choose from the available key lengths (128 bit, 192 bit, 256 bit). By default, the operator chooses 256 bit. The keybits parameter allows you to adjust this if desired. See [CRD](crd/crd-postgresql). +Further information on TDE can also be found in the [PGEE-Documentation](https://repository.cybertec.at/doc/18ee/encryption.html) + +#### Use of your own keys (Bring Your Own Key) +You have the option of defining your own encryption key before the cluster is created. +To do this, create a secret with the desired key in advance. +When starting, the operator checks whether a corresponding secret already exists and uses this instead of a newly generated key. +Use case: This enables the integration of external secret store solutions (e.g. HashiCorp Vault) to stream or synchronise keys directly into the Kubernetes secret. + +{{< hint type=important >}} If you provide your own key, you must ensure that the length of the key (in bytes) matches the configured keybits exactly. +| keybits | Keylength (Byte) | Notes | +| -------- |:----------------:| --------------:| +| 128 | 16 Byte | | +| 192 | 24 Byte | | +| 256 | 32 Byte | (Default) | + +A discrepancy between the configured bit length and the actual byte length of the provided key will result in errors when starting the database. +{{< /hint >}} + +The secret name follows the following fixed naming convention: [CLUSTERNAME]-tde + +```yaml +kind: Secret +apiVersion: v1 +metadata: + name: [CLUSTERNAME]-tde +stringData: + key: [TDE-KEY] +type: Opaque +``` + ### Check TDE-Status ```sh + +sh-5.1$ /usr/pgsql-17/bin/pg_controldata | grep -i "Encryption" +Data encryption: on +Encryption key length: 128 + [postgres@tde-cluster-1-0 ~]$ psql -psql (17.4 EE 1.4.1) +psql (18.1 EE 1.5.0, server 17.7 EE 1.4.4) ____ ____ _____ _____ | _ \ / ___| ____| ____| | |_) | | _| _| | _| From 8f7540392261053b70b25668c4be78bbc5c4a384 Mon Sep 17 00:00:00 2001 From: matthias Date: Thu, 11 Dec 2025 20:10:16 +0100 Subject: [PATCH 8/9] change default keybits to 128 for the beginning --- charts/postgres-operator/crds/postgresqls.yaml | 2 +- manifests/postgresql.crd.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index 3d14d689..49627603 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -702,7 +702,7 @@ spec: keybits: type: integer format: int32 - default: 256 + default: 128 enum: - 128 - 192 diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index 671b930b..4370bba4 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -700,7 +700,7 @@ spec: keybits: type: integer format: int32 - default: 256 + default: 128 enum: - 128 - 192 From 5612e00ac147db46d51400c1fad5276e94bea8da Mon Sep 17 00:00:00 2001 From: matthias Date: Thu, 11 Dec 2025 20:10:43 +0100 Subject: [PATCH 9/9] debug an fix issues --- pkg/apis/cpo.opensource.cybertec.at/v1/crds.go | 12 ++++++++++++ .../v1/zz_generated.deepcopy.go | 7 ++++++- pkg/cluster/sync.go | 18 ++++++++++++------ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/pkg/apis/cpo.opensource.cybertec.at/v1/crds.go b/pkg/apis/cpo.opensource.cybertec.at/v1/crds.go index e035058c..bf7a3934 100644 --- a/pkg/apis/cpo.opensource.cybertec.at/v1/crds.go +++ b/pkg/apis/cpo.opensource.cybertec.at/v1/crds.go @@ -1444,6 +1444,18 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{ "enable": { Type: "boolean", }, + "keybits": { + Type: "integer", + Format: "int32", + Enum: []apiextv1.JSON{ + {Raw: []byte("128")}, + {Raw: []byte("192")}, + {Raw: []byte("256")}, + }, + Default: &apiextv1.JSON{ + Raw: []byte("128"), + }, + }, }, }, "monitor": { diff --git a/pkg/apis/cpo.opensource.cybertec.at/v1/zz_generated.deepcopy.go b/pkg/apis/cpo.opensource.cybertec.at/v1/zz_generated.deepcopy.go index 001cf58e..61b6327b 100644 --- a/pkg/apis/cpo.opensource.cybertec.at/v1/zz_generated.deepcopy.go +++ b/pkg/apis/cpo.opensource.cybertec.at/v1/zz_generated.deepcopy.go @@ -1077,7 +1077,7 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) { if in.TDE != nil { in, out := &in.TDE, &out.TDE *out = new(TDE) - **out = **in + (*in).DeepCopyInto(*out) } if in.Monitoring != nil { in, out := &in.Monitoring, &out.Monitoring @@ -1625,6 +1625,11 @@ func (in *StreamTable) DeepCopy() *StreamTable { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TDE) DeepCopyInto(out *TDE) { *out = *in + if in.Keybits != nil { + in, out := &in.Keybits, &out.Keybits + *out = new(int32) + **out = **in + } return } diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index b39a37f4..d91669c2 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -1684,14 +1684,16 @@ func (c *Cluster) syncPgbackrestJob(forceRemove bool) error { func (c *Cluster) createTDESecret() error { c.logger.Info("creating TDE secret") c.setProcessName("creating TDE secret") - var bits int32 = 256 + + var bits int32 = 128 ptr := c.Postgresql.Spec.TDE.Keybits if ptr != nil { val := *ptr - if val == 128 || val == 192 { + if val == 256 || val == 192 { bits = val } } + generatedKey := make([]byte, (bits / 8)) rand.Read(generatedKey) @@ -1706,15 +1708,19 @@ func (c *Cluster) createTDESecret() error { }, } secret, err := c.KubeClient.Secrets(generatedSecret.Namespace).Create(context.TODO(), &generatedSecret, metav1.CreateOptions{}) + if err == nil { c.Secrets[secret.UID] = secret c.logger.Debugf("created new secret %s, namespace: %s, uid: %s", util.NameFromMeta(secret.ObjectMeta), generatedSecret.Namespace, secret.UID) - } - if k8sutil.ResourceAlreadyExists(err) { - c.logger.Warningf("TDE secret already exists, skip key generation and use existing one.") } else { - return fmt.Errorf("could not create secret for TDE %s: in namespace %s: %v", util.NameFromMeta(secret.ObjectMeta), generatedSecret.Namespace, err) + + if k8sutil.ResourceAlreadyExists(err) { + c.logger.Warningf("TDE secret already exists, skip key generation and use existing one.") + } else { + return fmt.Errorf("could not create secret for TDE %s: in namespace %s: %v", generatedSecret.Name, generatedSecret.Namespace, err) + } } + return nil }