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/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 bb7db1e6e..d3cabb8fa 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: |- @@ -412,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/go.mod b/go.mod index c86dd4bf9..80ef40141 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.20260930125700-96d274df9e0b diff --git a/go.sum b/go.sum index 7fbba1cd6..9f2360daf 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.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.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= 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{} diff --git a/manifest.yaml b/manifest.yaml index 118beaf42..062ec8e5d 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: |- @@ -411,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/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{}, diff --git a/web/docs/object_stores.md b/web/docs/object_stores.md index b5178f728..c7f60ca6e 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 `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, +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