From a341e3d932d5acbeb8a70156996521a6b3b45653 Mon Sep 17 00:00:00 2001 From: Florian Schauer Date: Sun, 19 Jul 2026 04:45:11 +0200 Subject: [PATCH 01/11] 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 | 36 ++++------ go.mod | 2 + go.sum | 4 +- manifest.yaml | 36 ++++------ web/docs/object_stores.md | 66 +++++++++++++++++++ 6 files changed, 102 insertions(+), 48 deletions(-) diff --git a/.wordlist.txt b/.wordlist.txt index ca97df685..0ca777bcb 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 @@ -95,6 +98,7 @@ creds csi customresourcedefinition declaratively +decrypt deps desc devel @@ -131,6 +135,7 @@ namespaces nonResourceURLs objectstore objectstores +openssl pluginConfiguration podName postgres @@ -155,6 +160,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 fe68d8714..f506b9796 100644 --- a/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml +++ b/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml @@ -144,13 +144,12 @@ spec: description: |- Compress a backup file (a tar file per tablespace) while streaming it to the object store. Available options are empty string (no - compression, default), `gzip`, `bzip2`, `lz4`, `snappy`, and `zstd`. + compression, default), `gzip`, `bzip2`, `lz4`, and `snappy`. enum: - bzip2 - gzip - lz4 - snappy - - zstd type: string encryption: description: |- @@ -256,16 +255,6 @@ spec: HistoryTags is a list of key value pairs that will be passed to the Barman --history-tags option. type: object - s3AddressingStyle: - description: |- - How bucket names appear in S3 requests: `virtual` (bucket.endpoint/key), - `path` (endpoint/bucket/key) or `auto` (boto3 decides, same as unset). - Requires s3Credentials and Barman 3.18 or later. - enum: - - auto - - virtual - - path - type: string s3Credentials: description: The credentials to use to upload data to S3 properties: @@ -328,13 +317,18 @@ spec: type: object sseCustomerKey: description: |- - Reference to the secret holding the key for Server-Side Encryption with - Customer-provided keys (SSE-C): a base64-encoded 256-bit AES key. - barman-cloud uses it to encrypt the objects it uploads and to read them - back. Meant for S3-compatible providers that only support SSE-C - (e.g. Hetzner Object Storage). Cannot be combined with `encryption`. - Works with any authentication method. Changing the key makes the - existing objects unreadable: use a new `destinationPath` or `serverName`. + 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 @@ -443,10 +437,6 @@ spec: message: use the 'serverName' plugin parameter in the Cluster resource reason: FieldValueForbidden rule: '!has(self.serverName)' - - fieldPath: .s3AddressingStyle - message: s3AddressingStyle requires s3Credentials - reason: FieldValueForbidden - rule: '!has(self.s3AddressingStyle) || has(self.s3Credentials)' instanceSidecarConfiguration: description: The configuration for the sidecar that runs in the instance pods diff --git a/go.mod b/go.mod index a428eb052..da96f63ab 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 50e620a22..2a82a6327 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.7.0 h1:F6U2qNzucaHss4pZbznOZ1ZdVVubRgpCI0iEcuO3IK4= -github.com/cloudnative-pg/barman-cloud v0.7.0/go.mod h1:PVldcjsh/1VCS18Ka7w0dqXEx6tAyXzW+BnuvYLaulY= github.com/cloudnative-pg/cloudnative-pg v1.30.1 h1:d1jxp0jQi7/3zrEhsv9ycD+G5ssmkNvDvTmogaGccCw= github.com/cloudnative-pg/cloudnative-pg v1.30.1/go.mod h1:k931EKEiGsGwHid+Lwo3vt4ZvnmnaIUYS0SJ29Bpivw= 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 bc89a459f..59d2f92d5 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -143,13 +143,12 @@ spec: description: |- Compress a backup file (a tar file per tablespace) while streaming it to the object store. Available options are empty string (no - compression, default), `gzip`, `bzip2`, `lz4`, `snappy`, and `zstd`. + compression, default), `gzip`, `bzip2`, `lz4`, and `snappy`. enum: - bzip2 - gzip - lz4 - snappy - - zstd type: string encryption: description: |- @@ -255,16 +254,6 @@ spec: HistoryTags is a list of key value pairs that will be passed to the Barman --history-tags option. type: object - s3AddressingStyle: - description: |- - How bucket names appear in S3 requests: `virtual` (bucket.endpoint/key), - `path` (endpoint/bucket/key) or `auto` (boto3 decides, same as unset). - Requires s3Credentials and Barman 3.18 or later. - enum: - - auto - - virtual - - path - type: string s3Credentials: description: The credentials to use to upload data to S3 properties: @@ -327,13 +316,18 @@ spec: type: object sseCustomerKey: description: |- - Reference to the secret holding the key for Server-Side Encryption with - Customer-provided keys (SSE-C): a base64-encoded 256-bit AES key. - barman-cloud uses it to encrypt the objects it uploads and to read them - back. Meant for S3-compatible providers that only support SSE-C - (e.g. Hetzner Object Storage). Cannot be combined with `encryption`. - Works with any authentication method. Changing the key makes the - existing objects unreadable: use a new `destinationPath` or `serverName`. + 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 @@ -442,10 +436,6 @@ spec: message: use the 'serverName' plugin parameter in the Cluster resource reason: FieldValueForbidden rule: '!has(self.serverName)' - - fieldPath: .s3AddressingStyle - message: s3AddressingStyle requires s3Credentials - reason: FieldValueForbidden - rule: '!has(self.s3AddressingStyle) || has(self.s3Credentials)' 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 39f7d7884..94538c42e 100644 --- a/web/docs/object_stores.md +++ b/web/docs/object_stores.md @@ -203,6 +203,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., RustFS) that use HTTPS with certificates From addb8fbd5a25e66d593158dafd33664f3f684970 Mon Sep 17 00:00:00 2001 From: Andrei Nistor Date: Tue, 8 Sep 2026 14:39:57 +0300 Subject: [PATCH 02/11] 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 54c498f6114de5c50e3fb62637200bf9ee21f8d8 Mon Sep 17 00:00:00 2001 From: Armando Ruocco Date: Wed, 30 Sep 2026 11:02:23 +0200 Subject: [PATCH 03/11] 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 da96f63ab..1d945be57 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 2a82a6327..80f2ed207 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 84970c2494926980b38532779c9b5708d89994d5 Mon Sep 17 00:00:00 2001 From: Armando Ruocco Date: Wed, 30 Sep 2026 13:58:35 +0200 Subject: [PATCH 04/11] feat: reject ObjectStores combining SSE-C with bucket-managed encryption The barman-cloud CLI 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 f506b9796..e622c58dd 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 59d2f92d5..b7adfa3b2 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 94538c42e..26d43dac1 100644 --- a/web/docs/object_stores.md +++ b/web/docs/object_stores.md @@ -261,8 +261,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 7cd2d0ce64afca66f831e4b17c17e931f41d0d0b Mon Sep 17 00:00:00 2001 From: Armando Ruocco Date: Wed, 30 Sep 2026 14:03:37 +0200 Subject: [PATCH 05/11] 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 17acc6f34..cf89d4cde 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 148466999..0a54b1ec3 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 31e36a8fd75c5fea311cb1cff2b53e925f637949 Mon Sep 17 00:00:00 2001 From: Armando Ruocco Date: Wed, 30 Sep 2026 15:17:22 +0200 Subject: [PATCH 06/11] 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 1d945be57..b80347f6c 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.20260930090711-c22eb3251f8d +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 80f2ed207..2bfa49b35 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.20260930090711-c22eb3251f8d h1:Wa3v1gdrrLbyBYckei+YWvEWhdfTyV7EXUp7LzMK3JE= -github.com/schaurian/barman-cloud v0.5.2-0.20260930090711-c22eb3251f8d/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= From 69a0c24d9bc15ce02fe83798ecd6b7d3694a2490 Mon Sep 17 00:00:00 2001 From: Marco Nenciarini Date: Fri, 16 Oct 2026 11:52:17 +0200 Subject: [PATCH 07/11] refactor: list the secrets with the barman-cloud helper Point the temporary replace at the current head of barman-cloud#284, which lists the secrets referenced by the credentials. Use it instead of the local list, so a new credential field cannot be left out of the instance role. Keep a role test for the SSE-C key secret. Signed-off-by: Marco Nenciarini --- go.mod | 2 +- go.sum | 4 +- internal/cnpgi/operator/specs/role.go | 2 +- internal/cnpgi/operator/specs/role_test.go | 10 + internal/cnpgi/operator/specs/secrets.go | 71 ----- internal/cnpgi/operator/specs/secrets_test.go | 244 ------------------ 6 files changed, 14 insertions(+), 319 deletions(-) delete mode 100644 internal/cnpgi/operator/specs/secrets.go delete mode 100644 internal/cnpgi/operator/specs/secrets_test.go diff --git a/go.mod b/go.mod index b80347f6c..1d54ab970 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.20260930125700-96d274df9e0b +replace github.com/cloudnative-pg/barman-cloud => github.com/schaurian/barman-cloud v0.5.2-0.20261016102524-dd2190da9483 diff --git a/go.sum b/go.sum index 2bfa49b35..9f128e7d9 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.20260930125700-96d274df9e0b h1:a0l65CAtN5JKSyc6qyd6UCsu24mr8r2WcRSXNJO1Fek= -github.com/schaurian/barman-cloud v0.5.2-0.20260930125700-96d274df9e0b/go.mod h1:eqSPRGz/s8M0Mea8mkqiVQUTkoSquAhvJF49feh+Ks4= +github.com/schaurian/barman-cloud v0.5.2-0.20261016102524-dd2190da9483 h1:PKRt6WUbQpBHoZPYmUwjF2J2qyFLn6WIg3rMarU3vdo= +github.com/schaurian/barman-cloud v0.5.2-0.20261016102524-dd2190da9483/go.mod h1:lxmfYjLD8M6glf4u/c3Tii0BvHcVJHCMvB8gZj6OMSc= 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/internal/cnpgi/operator/specs/role.go b/internal/cnpgi/operator/specs/role.go index a53278d61..1a68d4204 100644 --- a/internal/cnpgi/operator/specs/role.go +++ b/internal/cnpgi/operator/specs/role.go @@ -55,7 +55,7 @@ func BuildRoleRules(barmanObjects []barmancloudv1.ObjectStore) []rbacv1.PolicyRu for _, barmanObject := range barmanObjects { barmanObjectsSet.Put(barmanObject.Name) - for _, secret := range CollectSecretNamesFromCredentials(&barmanObject.Spec.Configuration.BarmanCredentials) { + for _, secret := range barmanObject.Spec.Configuration.BarmanCredentials.SecretNames() { secretsSet.Put(secret) } } diff --git a/internal/cnpgi/operator/specs/role_test.go b/internal/cnpgi/operator/specs/role_test.go index bb5c9a13b..dd7ca3684 100644 --- a/internal/cnpgi/operator/specs/role_test.go +++ b/internal/cnpgi/operator/specs/role_test.go @@ -87,6 +87,16 @@ var _ = Describe("BuildRoleRules", func() { Expect(rules[2].ResourceNames).To(BeEmpty()) }) + It("should grant access to the SSE-C customer key secret", func() { + store := newTestObjectStore("store-a", "secret-a") + store.Spec.Configuration.AWS.SSECustomerKey = &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{Name: "sse-c-key"}, + Key: "key", + } + rules := BuildRoleRules([]barmancloudv1.ObjectStore{store}) + Expect(rules[2].ResourceNames).To(ConsistOf("secret-a", "sse-c-key")) + }) + It("should deduplicate secret names across ObjectStores", func() { objects := []barmancloudv1.ObjectStore{ newTestObjectStore("store-a", "shared-secret"), diff --git a/internal/cnpgi/operator/specs/secrets.go b/internal/cnpgi/operator/specs/secrets.go deleted file mode 100644 index 32b745f4a..000000000 --- a/internal/cnpgi/operator/specs/secrets.go +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright © contributors to CloudNativePG, established as -CloudNativePG a Series of LF Projects, LLC. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package specs - -import ( - barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api" - machineryapi "github.com/cloudnative-pg/machinery/pkg/api" -) - -// CollectSecretNamesFromCredentials collects the names of the secrets -func CollectSecretNamesFromCredentials(barmanCredentials *barmanapi.BarmanCredentials) []string { - var references []*machineryapi.SecretKeySelector - if barmanCredentials.AWS != nil { - references = append( - references, - barmanCredentials.AWS.AccessKeyIDReference, - barmanCredentials.AWS.SecretAccessKeyReference, - barmanCredentials.AWS.RegionReference, - barmanCredentials.AWS.SessionToken, - barmanCredentials.AWS.SSECustomerKey, - ) - } - if barmanCredentials.Azure != nil { - // When using default Azure credentials or managed identity, no secrets are required - if !barmanCredentials.Azure.UseDefaultAzureCredentials && - !barmanCredentials.Azure.InheritFromAzureAD { - references = append( - references, - barmanCredentials.Azure.ConnectionString, - barmanCredentials.Azure.StorageAccount, - barmanCredentials.Azure.StorageKey, - barmanCredentials.Azure.StorageSasToken, - ) - } - } - if barmanCredentials.Google != nil { - references = append( - references, - barmanCredentials.Google.ApplicationCredentials, - ) - } - - result := make([]string, 0, len(references)) - for _, reference := range references { - if reference == nil { - continue - } - result = append(result, reference.Name) - } - - // TODO: stringset belongs to machinery :( - - return result -} diff --git a/internal/cnpgi/operator/specs/secrets_test.go b/internal/cnpgi/operator/specs/secrets_test.go deleted file mode 100644 index d674eb5b1..000000000 --- a/internal/cnpgi/operator/specs/secrets_test.go +++ /dev/null @@ -1,244 +0,0 @@ -/* -Copyright © contributors to CloudNativePG, established as -CloudNativePG a Series of LF Projects, LLC. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package specs - -import ( - barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api" - machineryapi "github.com/cloudnative-pg/machinery/pkg/api" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("CollectSecretNamesFromCredentials", func() { - Context("when collecting secrets from AWS credentials", func() { - It("should return secret names from S3 credentials", func() { - credentials := &barmanapi.BarmanCredentials{ - AWS: &barmanapi.S3Credentials{ - AccessKeyIDReference: &machineryapi.SecretKeySelector{ - LocalObjectReference: machineryapi.LocalObjectReference{ - Name: "aws-secret", - }, - Key: "access-key-id", - }, - SecretAccessKeyReference: &machineryapi.SecretKeySelector{ - LocalObjectReference: machineryapi.LocalObjectReference{ - Name: "aws-secret", - }, - Key: "secret-access-key", - }, - }, - } - - secrets := CollectSecretNamesFromCredentials(credentials) - 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{} - - secrets := CollectSecretNamesFromCredentials(credentials) - Expect(secrets).To(BeEmpty()) - }) - }) - - Context("when collecting secrets from Azure credentials", func() { - It("should return secret names when using explicit credentials", func() { - credentials := &barmanapi.BarmanCredentials{ - Azure: &barmanapi.AzureCredentials{ - ConnectionString: &machineryapi.SecretKeySelector{ - LocalObjectReference: machineryapi.LocalObjectReference{ - Name: "azure-secret", - }, - Key: "connection-string", - }, - }, - } - - secrets := CollectSecretNamesFromCredentials(credentials) - Expect(secrets).To(ContainElement("azure-secret")) - }) - - It("should return empty list when using UseDefaultAzureCredentials", func() { - credentials := &barmanapi.BarmanCredentials{ - Azure: &barmanapi.AzureCredentials{ - UseDefaultAzureCredentials: true, - ConnectionString: &machineryapi.SecretKeySelector{ - LocalObjectReference: machineryapi.LocalObjectReference{ - Name: "azure-secret", - }, - Key: "connection-string", - }, - }, - } - - secrets := CollectSecretNamesFromCredentials(credentials) - Expect(secrets).To(BeEmpty()) - }) - - It("should return empty list when using InheritFromAzureAD", func() { - credentials := &barmanapi.BarmanCredentials{ - Azure: &barmanapi.AzureCredentials{ - InheritFromAzureAD: true, - }, - } - - secrets := CollectSecretNamesFromCredentials(credentials) - Expect(secrets).To(BeEmpty()) - }) - - It("should return secret names for storage account and key", func() { - credentials := &barmanapi.BarmanCredentials{ - Azure: &barmanapi.AzureCredentials{ - StorageAccount: &machineryapi.SecretKeySelector{ - LocalObjectReference: machineryapi.LocalObjectReference{ - Name: "azure-storage", - }, - Key: "account-name", - }, - StorageKey: &machineryapi.SecretKeySelector{ - LocalObjectReference: machineryapi.LocalObjectReference{ - Name: "azure-storage", - }, - Key: "account-key", - }, - }, - } - - secrets := CollectSecretNamesFromCredentials(credentials) - Expect(secrets).To(ContainElement("azure-storage")) - }) - }) - - Context("when collecting secrets from Google credentials", func() { - It("should return secret names from Google credentials", func() { - credentials := &barmanapi.BarmanCredentials{ - Google: &barmanapi.GoogleCredentials{ - ApplicationCredentials: &machineryapi.SecretKeySelector{ - LocalObjectReference: machineryapi.LocalObjectReference{ - Name: "google-secret", - }, - Key: "credentials.json", - }, - }, - } - - secrets := CollectSecretNamesFromCredentials(credentials) - Expect(secrets).To(ContainElement("google-secret")) - }) - }) - - Context("when collecting secrets from multiple cloud providers", func() { - It("should return secret names from all providers", func() { - credentials := &barmanapi.BarmanCredentials{ - AWS: &barmanapi.S3Credentials{ - AccessKeyIDReference: &machineryapi.SecretKeySelector{ - LocalObjectReference: machineryapi.LocalObjectReference{ - Name: "aws-secret", - }, - Key: "access-key-id", - }, - }, - Azure: &barmanapi.AzureCredentials{ - ConnectionString: &machineryapi.SecretKeySelector{ - LocalObjectReference: machineryapi.LocalObjectReference{ - Name: "azure-secret", - }, - Key: "connection-string", - }, - }, - Google: &barmanapi.GoogleCredentials{ - ApplicationCredentials: &machineryapi.SecretKeySelector{ - LocalObjectReference: machineryapi.LocalObjectReference{ - Name: "google-secret", - }, - Key: "credentials.json", - }, - }, - } - - secrets := CollectSecretNamesFromCredentials(credentials) - Expect(secrets).To(ContainElements("aws-secret", "azure-secret", "google-secret")) - }) - - It("should skip Azure secrets when using UseDefaultAzureCredentials with other providers", func() { - credentials := &barmanapi.BarmanCredentials{ - AWS: &barmanapi.S3Credentials{ - AccessKeyIDReference: &machineryapi.SecretKeySelector{ - LocalObjectReference: machineryapi.LocalObjectReference{ - Name: "aws-secret", - }, - Key: "access-key-id", - }, - }, - Azure: &barmanapi.AzureCredentials{ - UseDefaultAzureCredentials: true, - ConnectionString: &machineryapi.SecretKeySelector{ - LocalObjectReference: machineryapi.LocalObjectReference{ - Name: "azure-secret", - }, - Key: "connection-string", - }, - }, - } - - secrets := CollectSecretNamesFromCredentials(credentials) - Expect(secrets).To(ContainElement("aws-secret")) - Expect(secrets).NotTo(ContainElement("azure-secret")) - }) - }) - - Context("when handling nil references", func() { - It("should skip nil secret references", func() { - credentials := &barmanapi.BarmanCredentials{ - AWS: &barmanapi.S3Credentials{ - AccessKeyIDReference: &machineryapi.SecretKeySelector{ - LocalObjectReference: machineryapi.LocalObjectReference{ - Name: "aws-secret", - }, - Key: "access-key-id", - }, - SecretAccessKeyReference: nil, - }, - } - - secrets := CollectSecretNamesFromCredentials(credentials) - Expect(secrets).To(ContainElement("aws-secret")) - Expect(len(secrets)).To(Equal(1)) - }) - }) -}) From 2e8c5366d98806a7844d2f3f7b89da9f3ad73688 Mon Sep 17 00:00:00 2001 From: Marco Nenciarini Date: Fri, 16 Oct 2026 12:03:00 +0200 Subject: [PATCH 08/11] chore: regenerate the CRD and the manifest Pick up the new description of sseCustomerKey, and the fields that main already carries in the barman-cloud library. Signed-off-by: Marco Nenciarini --- .../barmancloud.cnpg.io_objectstores.yaml | 36 ++++++++++++------- manifest.yaml | 36 ++++++++++++------- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml b/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml index e622c58dd..125824f9a 100644 --- a/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml +++ b/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml @@ -144,12 +144,13 @@ spec: description: |- Compress a backup file (a tar file per tablespace) while streaming it to the object store. Available options are empty string (no - compression, default), `gzip`, `bzip2`, `lz4`, and `snappy`. + compression, default), `gzip`, `bzip2`, `lz4`, `snappy`, and `zstd`. enum: - bzip2 - gzip - lz4 - snappy + - zstd type: string encryption: description: |- @@ -255,6 +256,16 @@ spec: HistoryTags is a list of key value pairs that will be passed to the Barman --history-tags option. type: object + s3AddressingStyle: + description: |- + How bucket names appear in S3 requests: `virtual` (bucket.endpoint/key), + `path` (endpoint/bucket/key) or `auto` (boto3 decides, same as unset). + Requires s3Credentials and Barman 3.18 or later. + enum: + - auto + - virtual + - path + type: string s3Credentials: description: The credentials to use to upload data to S3 properties: @@ -317,18 +328,13 @@ spec: 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. + Reference to the secret holding the key for Server-Side Encryption with + Customer-provided keys (SSE-C): a base64-encoded 256-bit AES key. + barman-cloud uses it to encrypt the objects it uploads and to read them + back. Meant for S3-compatible providers that only support SSE-C + (e.g. Hetzner Object Storage). Cannot be combined with `encryption`. + Works with any authentication method. Changing the key makes the + existing objects unreadable: use a new `destinationPath` or `serverName`. properties: key: description: The key to select @@ -447,6 +453,10 @@ spec: reason: FieldValueForbidden rule: '!has(self.s3Credentials) || !has(self.s3Credentials.sseCustomerKey) || !has(self.wal) || !has(self.wal.encryption)' + - fieldPath: .s3AddressingStyle + message: s3AddressingStyle requires s3Credentials + reason: FieldValueForbidden + rule: '!has(self.s3AddressingStyle) || has(self.s3Credentials)' instanceSidecarConfiguration: description: The configuration for the sidecar that runs in the instance pods diff --git a/manifest.yaml b/manifest.yaml index b7adfa3b2..b21a7c3cc 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -143,12 +143,13 @@ spec: description: |- Compress a backup file (a tar file per tablespace) while streaming it to the object store. Available options are empty string (no - compression, default), `gzip`, `bzip2`, `lz4`, and `snappy`. + compression, default), `gzip`, `bzip2`, `lz4`, `snappy`, and `zstd`. enum: - bzip2 - gzip - lz4 - snappy + - zstd type: string encryption: description: |- @@ -254,6 +255,16 @@ spec: HistoryTags is a list of key value pairs that will be passed to the Barman --history-tags option. type: object + s3AddressingStyle: + description: |- + How bucket names appear in S3 requests: `virtual` (bucket.endpoint/key), + `path` (endpoint/bucket/key) or `auto` (boto3 decides, same as unset). + Requires s3Credentials and Barman 3.18 or later. + enum: + - auto + - virtual + - path + type: string s3Credentials: description: The credentials to use to upload data to S3 properties: @@ -316,18 +327,13 @@ spec: 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. + Reference to the secret holding the key for Server-Side Encryption with + Customer-provided keys (SSE-C): a base64-encoded 256-bit AES key. + barman-cloud uses it to encrypt the objects it uploads and to read them + back. Meant for S3-compatible providers that only support SSE-C + (e.g. Hetzner Object Storage). Cannot be combined with `encryption`. + Works with any authentication method. Changing the key makes the + existing objects unreadable: use a new `destinationPath` or `serverName`. properties: key: description: The key to select @@ -446,6 +452,10 @@ spec: reason: FieldValueForbidden rule: '!has(self.s3Credentials) || !has(self.s3Credentials.sseCustomerKey) || !has(self.wal) || !has(self.wal.encryption)' + - fieldPath: .s3AddressingStyle + message: s3AddressingStyle requires s3Credentials + reason: FieldValueForbidden + rule: '!has(self.s3AddressingStyle) || has(self.s3Credentials)' instanceSidecarConfiguration: description: The configuration for the sidecar that runs in the instance pods From 4df81f76af23f467cbd0f175b1706de23741067d Mon Sep 17 00:00:00 2001 From: Marco Nenciarini Date: Fri, 16 Oct 2026 14:22:16 +0200 Subject: [PATCH 09/11] chore: use the barman-cloud commit that adds SSE-C Drop the temporary replace. The library merged #284, and the release that carries it is not out yet. Signed-off-by: Marco Nenciarini --- go.mod | 2 -- go.sum | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 1d54ab970..a428eb052 100644 --- a/go.mod +++ b/go.mod @@ -136,5 +136,3 @@ 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.20261016102524-dd2190da9483 diff --git a/go.sum b/go.sum index 9f128e7d9..50e620a22 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ 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.7.0 h1:F6U2qNzucaHss4pZbznOZ1ZdVVubRgpCI0iEcuO3IK4= +github.com/cloudnative-pg/barman-cloud v0.7.0/go.mod h1:PVldcjsh/1VCS18Ka7w0dqXEx6tAyXzW+BnuvYLaulY= github.com/cloudnative-pg/cloudnative-pg v1.30.1 h1:d1jxp0jQi7/3zrEhsv9ycD+G5ssmkNvDvTmogaGccCw= github.com/cloudnative-pg/cloudnative-pg v1.30.1/go.mod h1:k931EKEiGsGwHid+Lwo3vt4ZvnmnaIUYS0SJ29Bpivw= github.com/cloudnative-pg/cnpg-i v0.6.0 h1:LA//DLkFOLIjU0ASOpFkydZhGir9IAIDfgSsTTX9IpU= @@ -185,8 +187,6 @@ 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.20261016102524-dd2190da9483 h1:PKRt6WUbQpBHoZPYmUwjF2J2qyFLn6WIg3rMarU3vdo= -github.com/schaurian/barman-cloud v0.5.2-0.20261016102524-dd2190da9483/go.mod h1:lxmfYjLD8M6glf4u/c3Tii0BvHcVJHCMvB8gZj6OMSc= 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 ddbaadb4c2c63a6919cf769363fea8ef26a41147 Mon Sep 17 00:00:00 2001 From: Marco Nenciarini Date: Fri, 16 Oct 2026 15:09:20 +0200 Subject: [PATCH 10/11] docs: tidy the SSE-C section Replace the em-dashes, split the long closing paragraph, and say that a new key needs a new destinationPath or serverName. Signed-off-by: Marco Nenciarini --- web/docs/object_stores.md | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/web/docs/object_stores.md b/web/docs/object_stores.md index 26d43dac1..02874f40d 100644 --- a/web/docs/object_stores.md +++ b/web/docs/object_stores.md @@ -205,7 +205,7 @@ spec: ### Server-Side Encryption with Customer Keys (SSE-C) -Some S3-compatible providers — most notably **Hetzner Object Storage** — do +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 @@ -227,7 +227,7 @@ kubectl create secret generic aws-sse-c \ :::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 +it, your backups and WAL files become permanently unrecoverable. The provider cannot decrypt them for you. ::: @@ -255,19 +255,15 @@ spec: [...] ``` -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 `ObjectStore` that sets both is rejected when it is -created or updated. +The same key is applied to **every** operation (base backups, WAL archiving, +WAL restore, and data restore). It must remain unchanged and available for +the whole lifetime of the backups it protects. To use a new key, use a new +`destinationPath` or `serverName`. -:::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. -::: +`sseCustomerKey` can be combined with any authentication method, including +`inheritFromIAMRole`. It cannot be combined with the bucket-managed +`encryption` setting (SSE-S3 / SSE-KMS) of the `data` and `wal` sections. An +`ObjectStore` that sets both is rejected when it is created or updated. ### Using Object Storage with a Private CA From 7d7c9a4f3c00e92e44469ca8fe9ed7c7f6501918 Mon Sep 17 00:00:00 2001 From: Marco Nenciarini Date: Fri, 16 Oct 2026 16:00:03 +0200 Subject: [PATCH 11/11] fix: satisfy staticcheck QF1008 in the role rules Call SecretNames on the configuration, as the embedded BarmanCredentials field is promoted. Signed-off-by: Marco Nenciarini --- internal/cnpgi/operator/specs/role.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cnpgi/operator/specs/role.go b/internal/cnpgi/operator/specs/role.go index 1a68d4204..8d484a8c2 100644 --- a/internal/cnpgi/operator/specs/role.go +++ b/internal/cnpgi/operator/specs/role.go @@ -55,7 +55,7 @@ func BuildRoleRules(barmanObjects []barmancloudv1.ObjectStore) []rbacv1.PolicyRu for _, barmanObject := range barmanObjects { barmanObjectsSet.Put(barmanObject.Name) - for _, secret := range barmanObject.Spec.Configuration.BarmanCredentials.SecretNames() { + for _, secret := range barmanObject.Spec.Configuration.SecretNames() { secretsSet.Put(secret) } }