From ba74d62973d67983480d0cb93673c7257eec795b Mon Sep 17 00:00:00 2001 From: Florian Schauer Date: Sun, 19 Jul 2026 04:45:11 +0200 Subject: [PATCH 1/6] feat: support S3 SSE-C (customer-provided encryption keys) Surface the new `sseCustomerKey` field on `s3Credentials` through the ObjectStore CRD so users can enable Server-Side Encryption with Customer-provided keys (SSE-C). This is required by S3-compatible providers that only support SSE-C for encryption at rest, such as Hetzner Object Storage. The field flows through the embedded BarmanObjectStoreConfiguration from the barman-cloud library, so this change is limited to bumping the dependency, regenerating the CRD and the consolidated manifest, and documenting usage in the object stores guide. Depends on cloudnative-pg/barman-cloud#284 (temporarily pinned via a replace directive until that change is released). Closes #646 Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Florian Schauer --- .wordlist.txt | 6 ++ .../barmancloud.cnpg.io_objectstores.yaml | 25 +++++++ go.mod | 2 + go.sum | 4 +- manifest.yaml | 25 +++++++ web/docs/object_stores.md | 66 +++++++++++++++++++ 6 files changed, 126 insertions(+), 2 deletions(-) diff --git a/.wordlist.txt b/.wordlist.txt index 3df20daf1..54196d984 100644 --- a/.wordlist.txt +++ b/.wordlist.txt @@ -1,3 +1,4 @@ +AES AKS AccessDenied AdditionalContainerArgs @@ -21,11 +22,13 @@ EnvVar GCP GKE Gi +Hetzner IAM IRSA IfNotPresent InstanceSidecarConfiguration JSON +KMS Kustomize Lifecycle Linode @@ -94,6 +97,7 @@ creds csi customresourcedefinition declaratively +decrypt deps desc devel @@ -130,6 +134,7 @@ namespaces nonResourceURLs objectstore objectstores +openssl pluginConfiguration podName postgres @@ -154,6 +159,7 @@ serverName serviceaccount sha sig +sse storageClass subcommand tfddg diff --git a/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml b/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml index bb7db1e6e..9e09b452c 100644 --- a/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml +++ b/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml @@ -315,6 +315,31 @@ spec: - key - name type: object + sseCustomerKey: + description: |- + The reference to the secret containing the key for + Server-Side Encryption with Customer-provided keys (SSE-C). + When set, every object barman-cloud uploads to and downloads from + S3 is encrypted with this key using the AWS SSE-C protocol + (the `--sse-customer-key` barman-cloud option). + The referenced value must be a base64-encoded 256-bit (32-byte) + AES key. This is meant for S3-compatible providers that only + support customer-provided keys (e.g. Hetzner Object Storage) and + cannot be combined with the bucket-managed `encryption` field + (SSE-S3/SSE-KMS), which barman-cloud rejects together with SSE-C. + It can be combined with any authentication method, including + inheritFromIAMRole. + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object type: object serverName: description: |- diff --git a/go.mod b/go.mod index 84565bc2f..dcd466dfb 100644 --- a/go.mod +++ b/go.mod @@ -136,3 +136,5 @@ require ( sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) + +replace github.com/cloudnative-pg/barman-cloud => github.com/schaurian/barman-cloud v0.5.2-0.20260908142745-14f89b687e74 diff --git a/go.sum b/go.sum index 4607e0c5d..e1e1e409e 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,6 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudnative-pg/api v1.30.0 h1:L8hnvV/tPEQA1xYEi41FUBFA7FUNVGju8+SlgFlDDjI= github.com/cloudnative-pg/api v1.30.0/go.mod h1:XrKBbOWObL33si0FNuwX4uHNf5JShiZyOUqd6LxbJQo= -github.com/cloudnative-pg/barman-cloud v0.6.0 h1:OtBFmCDyVUAcgFa++FIoCCJwPfd5TtqK3PH6DGPcpkA= -github.com/cloudnative-pg/barman-cloud v0.6.0/go.mod h1:eqSPRGz/s8M0Mea8mkqiVQUTkoSquAhvJF49feh+Ks4= github.com/cloudnative-pg/cloudnative-pg v1.30.0 h1:fnhVq44xXx97MNiuvJsPrX1vSjYbgdyBK5MSGfdHdp0= github.com/cloudnative-pg/cloudnative-pg v1.30.0/go.mod h1:QkolwBOWZ+GvAiJt6KpDSymwkpf0K19/p4Q6MQlTM8U= github.com/cloudnative-pg/cnpg-i v0.6.0 h1:LA//DLkFOLIjU0ASOpFkydZhGir9IAIDfgSsTTX9IpU= @@ -187,6 +185,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/schaurian/barman-cloud v0.5.2-0.20260908142745-14f89b687e74 h1:H8jjBODttfJJyMLtv0dwvsMRaJr+IvdikaFS4EiDLj4= +github.com/schaurian/barman-cloud v0.5.2-0.20260908142745-14f89b687e74/go.mod h1:eqSPRGz/s8M0Mea8mkqiVQUTkoSquAhvJF49feh+Ks4= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/snorwin/jsonpatch v1.5.0 h1:0m56YSt9cHiJOn8U+OcqdPGcDQZmhPM/zsG7Dv5QQP0= diff --git a/manifest.yaml b/manifest.yaml index 118beaf42..226e50437 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -314,6 +314,31 @@ spec: - key - name type: object + sseCustomerKey: + description: |- + The reference to the secret containing the key for + Server-Side Encryption with Customer-provided keys (SSE-C). + When set, every object barman-cloud uploads to and downloads from + S3 is encrypted with this key using the AWS SSE-C protocol + (the `--sse-customer-key` barman-cloud option). + The referenced value must be a base64-encoded 256-bit (32-byte) + AES key. This is meant for S3-compatible providers that only + support customer-provided keys (e.g. Hetzner Object Storage) and + cannot be combined with the bucket-managed `encryption` field + (SSE-S3/SSE-KMS), which barman-cloud rejects together with SSE-C. + It can be combined with any authentication method, including + inheritFromIAMRole. + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object type: object serverName: description: |- diff --git a/web/docs/object_stores.md b/web/docs/object_stores.md index 11b1ff8cd..c3fc96827 100644 --- a/web/docs/object_stores.md +++ b/web/docs/object_stores.md @@ -194,6 +194,72 @@ spec: [...] ``` +### Server-Side Encryption with Customer Keys (SSE-C) + +Some S3-compatible providers — most notably **Hetzner Object Storage** — do +not offer bucket-managed server-side encryption (SSE-S3 / SSE-KMS) and instead +only support **Server-Side Encryption with Customer-provided keys (SSE-C)**. +With SSE-C the encryption key never leaves your control: it is supplied with +every request, and the provider uses it to encrypt and decrypt objects without +storing it. + +To enable SSE-C, set the `sseCustomerKey` field in the `s3Credentials` block to +a secret reference holding a **base64-encoded 256-bit (32-byte) AES key**. + +Generate the key and store it in a Kubernetes secret: + +```sh +# Generate a random 256-bit key, base64-encoded +openssl rand 32 | base64 > encryption.key + +kubectl create secret generic aws-sse-c \ + --from-file=key=encryption.key +``` + +:::warning +Keep this key safe and backed up **outside** the object store. If you lose +it, your backups and WAL files become permanently unrecoverable — the +provider cannot decrypt them for you. +::: + +Reference it in your `ObjectStore` definition: + +```yaml +apiVersion: barmancloud.cnpg.io/v1 +kind: ObjectStore +metadata: + name: hetzner-store +spec: + configuration: + destinationPath: "s3://BUCKET_NAME/path/to/folder" + endpointURL: "https://fsn1.your-objectstorage.com" + s3Credentials: + accessKeyId: + name: aws-creds + key: ACCESS_KEY_ID + secretAccessKey: + name: aws-creds + key: ACCESS_SECRET_KEY + sseCustomerKey: + name: aws-sse-c + key: key + [...] +``` + +The same key is applied to **every** operation — base backups, WAL archiving, +WAL restore, and data restore — so it must remain unchanged and available for +the whole lifetime of the backups it protects. `sseCustomerKey` can be combined +with any authentication method, including `inheritFromIAMRole`, but not with +the bucket-managed `encryption` setting (SSE-S3 / SSE-KMS) of the `data` and +`wal` sections: `barman-cloud` rejects `--sse-customer-key` together with +`--encryption`, so an object store that sets both fails at the first backup or +WAL archive. + +:::note +SSE-C relies on the `--sse-customer-key` option introduced in Barman 3.20.0, +which the plugin sidecar image ships starting from version 0.15.0. +::: + ### Using Object Storage with a Private CA For object storage services (e.g., MinIO) that use HTTPS with certificates From eed228aa80d71af6bd8410e0f377a599d84c3447 Mon Sep 17 00:00:00 2001 From: Andrei Nistor Date: Tue, 8 Sep 2026 14:39:57 +0300 Subject: [PATCH 2/6] fix: grant the instance role access to the SSE-C key secret The sidecar reads credential secrets through a Role whose resource names are collected from the credential references. Without the new reference in that list, a key kept in its own secret, as in the docs example, is forbidden and every barman-cloud command fails. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Andrei Nistor --- internal/cnpgi/operator/specs/secrets.go | 1 + internal/cnpgi/operator/specs/secrets_test.go | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/internal/cnpgi/operator/specs/secrets.go b/internal/cnpgi/operator/specs/secrets.go index 89811ad2b..32b745f4a 100644 --- a/internal/cnpgi/operator/specs/secrets.go +++ b/internal/cnpgi/operator/specs/secrets.go @@ -34,6 +34,7 @@ func CollectSecretNamesFromCredentials(barmanCredentials *barmanapi.BarmanCreden barmanCredentials.AWS.SecretAccessKeyReference, barmanCredentials.AWS.RegionReference, barmanCredentials.AWS.SessionToken, + barmanCredentials.AWS.SSECustomerKey, ) } if barmanCredentials.Azure != nil { diff --git a/internal/cnpgi/operator/specs/secrets_test.go b/internal/cnpgi/operator/specs/secrets_test.go index d6fa706bf..d674eb5b1 100644 --- a/internal/cnpgi/operator/specs/secrets_test.go +++ b/internal/cnpgi/operator/specs/secrets_test.go @@ -51,6 +51,23 @@ var _ = Describe("CollectSecretNamesFromCredentials", func() { Expect(secrets).To(ContainElement("aws-secret")) }) + It("should include the SSE-C customer key secret", func() { + credentials := &barmanapi.BarmanCredentials{ + AWS: &barmanapi.S3Credentials{ + InheritFromIAMRole: true, + SSECustomerKey: &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{ + Name: "sse-c-key", + }, + Key: "key", + }, + }, + } + + secrets := CollectSecretNamesFromCredentials(credentials) + Expect(secrets).To(ConsistOf("sse-c-key")) + }) + It("should handle nil AWS credentials", func() { credentials := &barmanapi.BarmanCredentials{} From 09adaa8419b1220f8881ad1e046b9ebbbbed04f7 Mon Sep 17 00:00:00 2001 From: Armando Ruocco Date: Wed, 30 Sep 2026 11:02:23 +0200 Subject: [PATCH 3/6] fix: use a separate SSE-C key file for each object store Point the temporary barman-cloud replace at the head of cloudnative-pg/barman-cloud#284 that keeps each SSE-C key in its own file. A replica cluster archives WAL to its own object store while restoring it from the source one in the same sidecar, and with a single shared file each operation could run with the key of the other object store. Signed-off-by: Armando Ruocco --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index dcd466dfb..2669d212b 100644 --- a/go.mod +++ b/go.mod @@ -137,4 +137,4 @@ require ( sigs.k8s.io/yaml v1.6.0 // indirect ) -replace github.com/cloudnative-pg/barman-cloud => github.com/schaurian/barman-cloud v0.5.2-0.20260908142745-14f89b687e74 +replace github.com/cloudnative-pg/barman-cloud => github.com/schaurian/barman-cloud v0.5.2-0.20260930090711-c22eb3251f8d diff --git a/go.sum b/go.sum index e1e1e409e..24a1c39c1 100644 --- a/go.sum +++ b/go.sum @@ -185,8 +185,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= -github.com/schaurian/barman-cloud v0.5.2-0.20260908142745-14f89b687e74 h1:H8jjBODttfJJyMLtv0dwvsMRaJr+IvdikaFS4EiDLj4= -github.com/schaurian/barman-cloud v0.5.2-0.20260908142745-14f89b687e74/go.mod h1:eqSPRGz/s8M0Mea8mkqiVQUTkoSquAhvJF49feh+Ks4= +github.com/schaurian/barman-cloud v0.5.2-0.20260930090711-c22eb3251f8d h1:Wa3v1gdrrLbyBYckei+YWvEWhdfTyV7EXUp7LzMK3JE= +github.com/schaurian/barman-cloud v0.5.2-0.20260930090711-c22eb3251f8d/go.mod h1:eqSPRGz/s8M0Mea8mkqiVQUTkoSquAhvJF49feh+Ks4= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/snorwin/jsonpatch v1.5.0 h1:0m56YSt9cHiJOn8U+OcqdPGcDQZmhPM/zsG7Dv5QQP0= From 14d9d9c56707b6a04e04d48ce2b98370b89e6a36 Mon Sep 17 00:00:00 2001 From: Armando Ruocco Date: Wed, 30 Sep 2026 13:58:35 +0200 Subject: [PATCH 4/6] feat: reject ObjectStores combining SSE-C with bucket-managed encryption barman-cloud refuses --sse-customer-key together with --encryption, so an ObjectStore setting both sseCustomerKey and data or wal encryption was accepted and then failed at its first backup or WAL archive. Reject it when it is created or updated instead. Signed-off-by: Armando Ruocco --- api/v1/objectstore_types.go | 2 ++ config/crd/bases/barmancloud.cnpg.io_objectstores.yaml | 10 ++++++++++ manifest.yaml | 10 ++++++++++ web/docs/object_stores.md | 4 ++-- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/api/v1/objectstore_types.go b/api/v1/objectstore_types.go index 928d52c98..e9964bbb6 100644 --- a/api/v1/objectstore_types.go +++ b/api/v1/objectstore_types.go @@ -59,6 +59,8 @@ type InstanceSidecarConfiguration struct { type ObjectStoreSpec struct { // The configuration for the barman-cloud tool suite // +kubebuilder:validation:XValidation:rule="!has(self.serverName)",fieldPath=".serverName",reason="FieldValueForbidden",message="use the 'serverName' plugin parameter in the Cluster resource" + // +kubebuilder:validation:XValidation:rule="!has(self.s3Credentials) || !has(self.s3Credentials.sseCustomerKey) || !has(self.data) || !has(self.data.encryption)",fieldPath=".data.encryption",reason="FieldValueForbidden",message="cannot be used together with s3Credentials.sseCustomerKey" + // +kubebuilder:validation:XValidation:rule="!has(self.s3Credentials) || !has(self.s3Credentials.sseCustomerKey) || !has(self.wal) || !has(self.wal.encryption)",fieldPath=".wal.encryption",reason="FieldValueForbidden",message="cannot be used together with s3Credentials.sseCustomerKey" Configuration barmanapi.BarmanObjectStoreConfiguration `json:"configuration"` // RetentionPolicy is the retention policy to be used for backups diff --git a/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml b/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml index 9e09b452c..d3cabb8fa 100644 --- a/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml +++ b/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml @@ -437,6 +437,16 @@ spec: message: use the 'serverName' plugin parameter in the Cluster resource reason: FieldValueForbidden rule: '!has(self.serverName)' + - fieldPath: .data.encryption + message: cannot be used together with s3Credentials.sseCustomerKey + reason: FieldValueForbidden + rule: '!has(self.s3Credentials) || !has(self.s3Credentials.sseCustomerKey) + || !has(self.data) || !has(self.data.encryption)' + - fieldPath: .wal.encryption + message: cannot be used together with s3Credentials.sseCustomerKey + reason: FieldValueForbidden + rule: '!has(self.s3Credentials) || !has(self.s3Credentials.sseCustomerKey) + || !has(self.wal) || !has(self.wal.encryption)' instanceSidecarConfiguration: description: The configuration for the sidecar that runs in the instance pods diff --git a/manifest.yaml b/manifest.yaml index 226e50437..062ec8e5d 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -436,6 +436,16 @@ spec: message: use the 'serverName' plugin parameter in the Cluster resource reason: FieldValueForbidden rule: '!has(self.serverName)' + - fieldPath: .data.encryption + message: cannot be used together with s3Credentials.sseCustomerKey + reason: FieldValueForbidden + rule: '!has(self.s3Credentials) || !has(self.s3Credentials.sseCustomerKey) + || !has(self.data) || !has(self.data.encryption)' + - fieldPath: .wal.encryption + message: cannot be used together with s3Credentials.sseCustomerKey + reason: FieldValueForbidden + rule: '!has(self.s3Credentials) || !has(self.s3Credentials.sseCustomerKey) + || !has(self.wal) || !has(self.wal.encryption)' instanceSidecarConfiguration: description: The configuration for the sidecar that runs in the instance pods diff --git a/web/docs/object_stores.md b/web/docs/object_stores.md index 0fb6eb7a8..c7f60ca6e 100644 --- a/web/docs/object_stores.md +++ b/web/docs/object_stores.md @@ -252,8 +252,8 @@ the whole lifetime of the backups it protects. `sseCustomerKey` can be combined with any authentication method, including `inheritFromIAMRole`, but not with the bucket-managed `encryption` setting (SSE-S3 / SSE-KMS) of the `data` and `wal` sections: `barman-cloud` rejects `--sse-customer-key` together with -`--encryption`, so an object store that sets both fails at the first backup or -WAL archive. +`--encryption`, so an `ObjectStore` that sets both is rejected when it is +created or updated. :::note SSE-C relies on the `--sse-customer-key` option introduced in Barman 3.20.0, From a7480c8f5044f365c10e2b5a2adac9e29439efe9 Mon Sep 17 00:00:00 2001 From: Armando Ruocco Date: Wed, 30 Sep 2026 14:03:37 +0200 Subject: [PATCH 5/6] test: cover SSE-C with a different key for each object store Run the replica cluster scenario with the source and the replica object stores each encrypted with its own SSE-C key kept in its own secret. The designated primary archives WAL to one object store while restoring it from the other in the same sidecar, and the switchover exercises both directions. At the end, check that every object in each bucket is readable with its own key only, and that an ObjectStore combining SSE-C with bucket-managed encryption is rejected. Signed-off-by: Armando Ruocco --- test/e2e/internal/objectstore/objectstore.go | 7 ++ test/e2e/internal/objectstore/s3.go | 39 ++++++++++- .../internal/tests/replicacluster/fixtures.go | 34 +++++++++ .../tests/replicacluster/replica_cluster.go | 69 +++++++++++++++++++ 4 files changed, 147 insertions(+), 2 deletions(-) diff --git a/test/e2e/internal/objectstore/objectstore.go b/test/e2e/internal/objectstore/objectstore.go index ad3efe7d3..92de387cb 100644 --- a/test/e2e/internal/objectstore/objectstore.go +++ b/test/e2e/internal/objectstore/objectstore.go @@ -39,6 +39,8 @@ type Resources struct { Service *corev1.Service Secret *corev1.Secret PVC *corev1.PersistentVolumeClaim + // SSECustomerKey is the secret holding the SSE-C key of the object store, if any + SSECustomerKey *corev1.Secret } // Create creates the object store resources. @@ -53,6 +55,11 @@ func (osr Resources) Create(ctx context.Context, cl client.Client) error { return fmt.Errorf("failed to create secret: %w", err) } } + if osr.SSECustomerKey != nil { + if err := cl.Create(ctx, osr.SSECustomerKey); err != nil { + return fmt.Errorf("failed to create SSE-C key secret: %w", err) + } + } if osr.Deployment != nil { if err := cl.Create(ctx, osr.Deployment); err != nil { return fmt.Errorf("failed to create deployment: %w", err) diff --git a/test/e2e/internal/objectstore/s3.go b/test/e2e/internal/objectstore/s3.go index 0712d9ee6..e584e2c56 100644 --- a/test/e2e/internal/objectstore/s3.go +++ b/test/e2e/internal/objectstore/s3.go @@ -20,6 +20,8 @@ SPDX-License-Identifier: Apache-2.0 package objectstore import ( + "crypto/rand" + "encoding/base64" "fmt" "net" @@ -243,8 +245,8 @@ func newS3Secret(namespace, name string) *corev1.Secret { Namespace: namespace, }, Data: map[string][]byte{ - "ACCESS_KEY_ID": []byte("s3accesskey"), - "ACCESS_SECRET_KEY": []byte("s3secretkey123"), + "ACCESS_KEY_ID": []byte(S3AccessKeyID), + "ACCESS_SECRET_KEY": []byte(S3SecretAccessKey), }, } } @@ -272,6 +274,39 @@ func newS3PVC(namespace, name string) *corev1.PersistentVolumeClaim { } } +// S3AccessKeyID and S3SecretAccessKey are the credentials of the S3-compatible +// object store created by NewS3ObjectStoreResources. +const ( + S3AccessKeyID = "s3accesskey" + S3SecretAccessKey = "s3secretkey123" +) + +// SSECustomerKeySecretKey is the key of the SSE-C key in the secret created by +// NewSSECustomerKeySecret. +const SSECustomerKeySecretKey = "key" + +// NewSSECustomerKeySecret creates a secret holding a random base64-encoded +// 256-bit SSE-C key, and returns it together with the encoded key. +func NewSSECustomerKeySecret(namespace, name string) (*corev1.Secret, string) { + raw := make([]byte, 32) + _, _ = rand.Read(raw) + key := base64.StdEncoding.EncodeToString(raw) + + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Data: map[string][]byte{ + SSECustomerKeySecretKey: []byte(key), + }, + }, key +} + // NewS3ObjectStore creates a new ObjectStore pointing at the S3-compatible // object store created by NewS3ObjectStoreResources with the given name. func NewS3ObjectStore(namespace, name, s3Name string) *pluginBarmanCloudV1.ObjectStore { diff --git a/test/e2e/internal/tests/replicacluster/fixtures.go b/test/e2e/internal/tests/replicacluster/fixtures.go index 2f0ef4858..11ed39bd7 100644 --- a/test/e2e/internal/tests/replicacluster/fixtures.go +++ b/test/e2e/internal/tests/replicacluster/fixtures.go @@ -21,6 +21,7 @@ package replicacluster import ( cloudnativepgv1 "github.com/cloudnative-pg/api/pkg/api/v1" + machineryapi "github.com/cloudnative-pg/machinery/pkg/api" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -58,6 +59,9 @@ type replicaClusterTestResources struct { ReplicaObjectStore *pluginBarmanCloudV1.ObjectStore ReplicaCluster *cloudnativepgv1.Cluster ReplicaBackup *cloudnativepgv1.Backup + // SSECustomerKeys maps the S3 endpoint of each object store to the + // base64-encoded SSE-C key its objects must be encrypted with + SSECustomerKeys map[string]string } type s3ReplicaClusterFactory struct{} @@ -77,6 +81,36 @@ func (f s3ReplicaClusterFactory) createReplicaClusterTestResources(namespace str return result } +// s3SSECReplicaClusterFactory encrypts each object store with its own SSE-C +// key: the designated primary of the replica cluster archives WAL to one +// object store while restoring it from the other, in the same sidecar. +type s3SSECReplicaClusterFactory struct{} + +func (f s3SSECReplicaClusterFactory) createReplicaClusterTestResources( + namespace string, +) replicaClusterTestResources { + result := s3ReplicaClusterFactory{}.createReplicaClusterTestResources(namespace) + result.SSECustomerKeys = map[string]string{} + + for _, store := range []struct { + resources *objectstore.Resources + objectStore *pluginBarmanCloudV1.ObjectStore + }{ + {result.SrcObjectStoreResources, result.SrcObjectStore}, + {result.ReplicaObjectStoreResources, result.ReplicaObjectStore}, + } { + secret, key := objectstore.NewSSECustomerKeySecret(namespace, store.objectStore.Name+"-sse-c") + store.resources.SSECustomerKey = secret + store.objectStore.Spec.Configuration.AWS.SSECustomerKey = &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{Name: secret.Name}, + Key: objectstore.SSECustomerKeySecretKey, + } + result.SSECustomerKeys[store.objectStore.Spec.Configuration.EndpointURL] = key + } + + return result +} + type gcsReplicaClusterFactory struct{} func (f gcsReplicaClusterFactory) createReplicaClusterTestResources(namespace string) replicaClusterTestResources { diff --git a/test/e2e/internal/tests/replicacluster/replica_cluster.go b/test/e2e/internal/tests/replicacluster/replica_cluster.go index ee7934a23..9c04b533e 100644 --- a/test/e2e/internal/tests/replicacluster/replica_cluster.go +++ b/test/e2e/internal/tests/replicacluster/replica_cluster.go @@ -25,7 +25,9 @@ import ( "time" cloudnativepgv1 "github.com/cloudnative-pg/api/pkg/api/v1" + barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" @@ -34,11 +36,36 @@ import ( cluster2 "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/cluster" "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/command" nmsp "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/namespace" + "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/objectstore" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) +// checkSSECustomerKeyScript prints how many objects the bucket holds and how +// many of them are not readable with the given SSE-C key alone: readable +// without a key, with the other key, or not with the given one +const checkSSECustomerKeyScript = ` +import base64, sys, boto3, botocore +endpoint, access_key, secret_key, key, other_key = sys.argv[1:6] +s3 = boto3.client("s3", endpoint_url=endpoint, region_name="us-east-1", + aws_access_key_id=access_key, aws_secret_access_key=secret_key) +def readable(name, sse_key=None): + args = {} + if sse_key: + args = {"SSECustomerAlgorithm": "AES256", "SSECustomerKey": base64.b64decode(sse_key)} + try: + s3.head_object(Bucket="backups", Key=name, **args) + return True + except botocore.exceptions.ClientError: + return False +names = [o["Key"] for page in s3.get_paginator("list_objects_v2").paginate(Bucket="backups") + for o in page.get("Contents", [])] +bad = [n for n in names if readable(n) or readable(n, other_key) or not readable(n, key)] +print("objects=%d misencrypted=%d" % (len(names), len(bad))) +print(bad[:10]) +` + var _ = Describe("Replica cluster", func() { var namespace *corev1.Namespace var cl client.Client @@ -75,6 +102,18 @@ var _ = Describe("Replica cluster", func() { Expect(cl.Create(ctx, testResources.ReplicaObjectStore)).To(Succeed()) } + if testResources.SSECustomerKeys != nil { + By("rejecting an ObjectStore that combines SSE-C with bucket-managed encryption") + invalid := testResources.SrcObjectStore.DeepCopy() + invalid.ObjectMeta = metav1.ObjectMeta{Name: "invalid-sse-c", Namespace: namespace.Name} + invalid.Spec.Configuration.Wal = &barmanapi.WalBackupConfiguration{ + Encryption: barmanapi.EncryptionTypeAES256, + } + err := cl.Create(ctx, invalid) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("cannot be used together with s3Credentials.sseCustomerKey")) + } + By("Creating a CloudNativePG cluster") src := testResources.SrcCluster Expect(cl.Create(ctx, testResources.SrcCluster)).To(Succeed()) @@ -264,11 +303,41 @@ var _ = Describe("Replica cluster", func() { g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(BeEquivalentTo("2\n")) }).Within(2 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) + + for endpoint, key := range testResources.SSECustomerKeys { + By(fmt.Sprintf("checking every object in %s is encrypted with its own SSE-C key", endpoint)) + var otherKey string + for otherEndpoint, k := range testResources.SSECustomerKeys { + if otherEndpoint != endpoint { + otherKey = k + } + } + output, stdErr, err := command.ExecuteInContainer(ctx, + *clientSet, + cfg, + command.ContainerLocator{ + NamespaceName: replica.Namespace, + PodName: fmt.Sprintf("%v-1", replica.Name), + ContainerName: "plugin-barman-cloud", + }, + nil, + []string{ + "/venv/bin/python3", "-c", checkSSECustomerKeyScript, + endpoint, objectstore.S3AccessKeyID, objectstore.S3SecretAccessKey, key, otherKey, + }) + Expect(err).NotTo(HaveOccurred(), "stderr: %s", stdErr) + GinkgoWriter.Printf("%s: %s", endpoint, output) + Expect(output).To(MatchRegexp(`^objects=[1-9][0-9]* misencrypted=0\n`)) + } }, Entry( "with S3", s3ReplicaClusterFactory{}, ), + Entry( + "with S3 and a different SSE-C key for each object store", + s3SSECReplicaClusterFactory{}, + ), Entry( "with Azurite", azuriteReplicaClusterFactory{}, From eac40075094625d8a9350fef99a8652dd8dcefbc Mon Sep 17 00:00:00 2001 From: Armando Ruocco Date: Wed, 30 Sep 2026 15:17:22 +0200 Subject: [PATCH 6/6] fix: let object stores with an SSE-C key start WAL archiving Point the temporary barman-cloud replace at the head of cloudnative-pg/barman-cloud#284 that stops passing --sse-customer-key to barman-cloud-check-wal-archive. That command rejects the option, and it runs before the first WAL file is archived, so an object store with an SSE-C key could never start archiving. The SSE-C e2e test caught it. Signed-off-by: Armando Ruocco --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 56ae8452c..80ef40141 100644 --- a/go.mod +++ b/go.mod @@ -137,4 +137,4 @@ require ( sigs.k8s.io/yaml v1.6.0 // indirect ) -replace github.com/cloudnative-pg/barman-cloud => github.com/schaurian/barman-cloud v0.5.2-0.20260930113931-207fe819977a +replace github.com/cloudnative-pg/barman-cloud => github.com/schaurian/barman-cloud v0.5.2-0.20260930125700-96d274df9e0b diff --git a/go.sum b/go.sum index fbe7aae95..9f2360daf 100644 --- a/go.sum +++ b/go.sum @@ -185,8 +185,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= -github.com/schaurian/barman-cloud v0.5.2-0.20260930113931-207fe819977a h1:NTgDs1PRIRYIA073a/zLiC1EQgXaXzsw4cKwCQ0wltY= -github.com/schaurian/barman-cloud v0.5.2-0.20260930113931-207fe819977a/go.mod h1:eqSPRGz/s8M0Mea8mkqiVQUTkoSquAhvJF49feh+Ks4= +github.com/schaurian/barman-cloud v0.5.2-0.20260930125700-96d274df9e0b h1:a0l65CAtN5JKSyc6qyd6UCsu24mr8r2WcRSXNJO1Fek= +github.com/schaurian/barman-cloud v0.5.2-0.20260930125700-96d274df9e0b/go.mod h1:eqSPRGz/s8M0Mea8mkqiVQUTkoSquAhvJF49feh+Ks4= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/snorwin/jsonpatch v1.5.0 h1:0m56YSt9cHiJOn8U+OcqdPGcDQZmhPM/zsG7Dv5QQP0=