From 55e2027620b70dcd9ed1d35c7e70eb16abb14e78 Mon Sep 17 00:00:00 2001 From: Florian Schauer Date: Sun, 19 Jul 2026 04:34:41 +0200 Subject: [PATCH 1/6] feat: add support for S3 SSE-C (customer-provided encryption keys) Add an `sseCustomerKey` field to the S3 credentials that references a secret holding a base64-encoded 256-bit AES key. When set, the key is materialized to a file and passed to every barman-cloud command through the `--sse-customer-key file://` option, enabling 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 (e.g. Hetzner Object Storage). The option is injected in the shared `appendCloudProviderOptions` chokepoint, so it applies to all barman-cloud-* commands (backup, wal-archive, wal-restore, restore, backup-list, backup-delete, check-wal-archive), and is orthogonal to the existing bucket-managed `encryption` (SSE-S3/SSE-KMS) option. The key is materialized before the auth-method branching so it works with every authentication method, including inheritFromIAMRole. Requires a barman release that ships the `--sse-customer-key` option (EnterpriseDB/barman#973). Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Florian Schauer --- pkg/api/config.go | 15 +++++++++++ pkg/api/zz_generated.deepcopy.go | 5 ++++ pkg/command/commandbuilder.go | 11 ++++++++ pkg/command/commandbuilder_test.go | 43 ++++++++++++++++++++++++++++++ pkg/credentials/env.go | 34 +++++++++++++++++++++++ pkg/utils/constants.go | 5 ++++ 6 files changed, 113 insertions(+) diff --git a/pkg/api/config.go b/pkg/api/config.go index 0e731081..86fc0945 100644 --- a/pkg/api/config.go +++ b/pkg/api/config.go @@ -109,6 +109,21 @@ type S3Credentials struct { // +optional SessionToken *machineryapi.SecretKeySelector `json:"sessionToken,omitempty"` + // 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. + // +optional + SSECustomerKey *machineryapi.SecretKeySelector `json:"sseCustomerKey,omitempty"` + // Use the role based authentication without providing explicitly the keys. // +optional InheritFromIAMRole bool `json:"inheritFromIAMRole,omitempty"` diff --git a/pkg/api/zz_generated.deepcopy.go b/pkg/api/zz_generated.deepcopy.go index 431bc60b..cdc8c889 100644 --- a/pkg/api/zz_generated.deepcopy.go +++ b/pkg/api/zz_generated.deepcopy.go @@ -210,6 +210,11 @@ func (in *S3Credentials) DeepCopyInto(out *S3Credentials) { *out = new(pkgapi.SecretKeySelector) **out = **in } + if in.SSECustomerKey != nil { + in, out := &in.SSECustomerKey, &out.SSECustomerKey + *out = new(pkgapi.SecretKeySelector) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3Credentials. diff --git a/pkg/command/commandbuilder.go b/pkg/command/commandbuilder.go index 70260b0b..52b02390 100644 --- a/pkg/command/commandbuilder.go +++ b/pkg/command/commandbuilder.go @@ -23,6 +23,7 @@ import ( "context" barmanApi "github.com/cloudnative-pg/barman-cloud/pkg/api" + "github.com/cloudnative-pg/barman-cloud/pkg/utils" ) // CloudWalRestoreOptions returns the options needed to execute the barman command successfully @@ -87,6 +88,16 @@ func appendCloudProviderOptions( options, "--cloud-provider", "aws-s3") + + // When Server-Side Encryption with Customer-provided keys (SSE-C) + // is configured, point barman-cloud at the key file that the + // credentials package materializes from the referenced secret. + if credentials.AWS.SSECustomerKey != nil { + options = append( + options, + "--sse-customer-key", + "file://"+utils.SSECustomerKeyFileLocation) + } case credentials.Azure != nil: options = append( options, diff --git a/pkg/command/commandbuilder_test.go b/pkg/command/commandbuilder_test.go index bb2d1a0d..0bdf859f 100644 --- a/pkg/command/commandbuilder_test.go +++ b/pkg/command/commandbuilder_test.go @@ -26,6 +26,7 @@ import ( machineryapi "github.com/cloudnative-pg/machinery/pkg/api" barmanApi "github.com/cloudnative-pg/barman-cloud/pkg/api" + "github.com/cloudnative-pg/barman-cloud/pkg/utils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -172,3 +173,45 @@ var _ = Describe("AppendCloudProviderOptions with Azure credentials", func() { )) }) }) + +var _ = Describe("AppendCloudProviderOptions with AWS credentials", func() { + var options []string + + BeforeEach(func() { + options = []string{} + }) + + It("should not add the SSE-C option when no customer key is set", func(ctx SpecContext) { + credentials := barmanApi.BarmanCredentials{ + AWS: &barmanApi.S3Credentials{ + InheritFromIAMRole: true, + }, + } + result, err := appendCloudProviderOptions(ctx, options, credentials) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal([]string{ + "--cloud-provider", "aws-s3", + })) + Expect(result).ToNot(ContainElement("--sse-customer-key")) + }) + + It("should add the SSE-C option pointing at the key file when a customer key is set", func(ctx SpecContext) { + credentials := barmanApi.BarmanCredentials{ + AWS: &barmanApi.S3Credentials{ + InheritFromIAMRole: true, + SSECustomerKey: &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{ + Name: "sse-c-key", + }, + Key: "key", + }, + }, + } + result, err := appendCloudProviderOptions(ctx, options, credentials) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal([]string{ + "--cloud-provider", "aws-s3", + "--sse-customer-key", "file://" + utils.SSECustomerKeyFileLocation, + })) + }) +}) diff --git a/pkg/credentials/env.go b/pkg/credentials/env.go index d7810288..56d1a06b 100644 --- a/pkg/credentials/env.go +++ b/pkg/credentials/env.go @@ -29,6 +29,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" barmanApi "github.com/cloudnative-pg/barman-cloud/pkg/api" + "github.com/cloudnative-pg/barman-cloud/pkg/utils" ) const ( @@ -142,6 +143,13 @@ func envSetAWSCredentials( return nil, fmt.Errorf("missing S3 credentials") } + // Materialize the SSE-C customer key, if any, before the auth-method + // handling below: SSE-C is orthogonal to authentication and must be + // available for every method, including inheritFromIAMRole. + if err := reconcileAWSSSECustomerKey(ctx, client, namespace, s3credentials); err != nil { + return nil, err + } + if s3credentials.InheritFromIAMRole { return env, nil } @@ -207,6 +215,32 @@ func envSetAWSCredentials( return env, nil } +// reconcileAWSSSECustomerKey materializes (or removes) the S3 SSE-C customer +// key file referenced by the S3 credentials. barman-cloud consumes it through +// the '--sse-customer-key file://' option, so the key must exist on disk next +// to the process that runs the barman-cloud commands. The referenced secret is +// expected to contain a base64-encoded 256-bit AES key, which barman-cloud +// validates when it reads the file. +func reconcileAWSSSECustomerKey( + ctx context.Context, + c client.Client, + namespace string, + s3credentials *barmanApi.S3Credentials, +) error { + if s3credentials.SSECustomerKey == nil { + return fileutils.RemoveFile(utils.SSECustomerKeyFileLocation) + } + + key, err := extractValueFromSecret(ctx, c, s3credentials.SSECustomerKey, namespace) + if err != nil { + return err + } + + _, err = fileutils.WriteFileAtomic(utils.SSECustomerKeyFileLocation, key, 0o600) + + return err +} + // envSetAzureCredentials sets the Azure environment variables given the configuration // inside the cluster func envSetAzureCredentials( diff --git a/pkg/utils/constants.go b/pkg/utils/constants.go index ed7dc775..410cc071 100644 --- a/pkg/utils/constants.go +++ b/pkg/utils/constants.go @@ -43,4 +43,9 @@ const ( // BarmanCloudCheckWalArchive is the command name for 'barman-cloud-check-wal-archive' BarmanCloudCheckWalArchive = "barman-cloud-check-wal-archive" + + // SSECustomerKeyFileLocation is the path where the S3 SSE-C customer key is + // materialized from its secret so that it can be passed to the barman-cloud + // commands via the '--sse-customer-key file://' option. + SSECustomerKeyFileLocation = "/controller/.sse-customer-key" ) From 0868c193e06325c5ec350865bd3162056bde440c Mon Sep 17 00:00:00 2001 From: Andrei Nistor Date: Tue, 8 Sep 2026 14:39:01 +0300 Subject: [PATCH 2/6] feat: reject sseCustomerKey combined with SSE-S3 or SSE-KMS The barman-cloud commands refuse --sse-customer-key together with --encryption, so a configuration setting both fails on the first backup. Reject it at validation time instead, on the data and wal encryption fields. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Andrei Nistor --- pkg/api/webhooks/config.go | 32 ++++++++++++++++++++++++++++++++ pkg/api/webhooks/config_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/pkg/api/webhooks/config.go b/pkg/api/webhooks/config.go index 7300e63f..a9d8e529 100644 --- a/pkg/api/webhooks/config.go +++ b/pkg/api/webhooks/config.go @@ -72,6 +72,38 @@ func ValidateBackupConfiguration( )) } + allErrors = append(allErrors, validateSSECustomerKey(barmanObjectStore, path)...) + + return allErrors +} + +// validateSSECustomerKey checks that SSE-C is not combined with the other +// server-side encryption modes, which barman-cloud rejects +func validateSSECustomerKey( + barmanObjectStore *api.BarmanObjectStoreConfiguration, + path *field.Path, +) field.ErrorList { + if barmanObjectStore.AWS == nil || barmanObjectStore.AWS.SSECustomerKey == nil { + return nil + } + + const message = "cannot be used together with s3Credentials.sseCustomerKey" + allErrors := field.ErrorList{} + if barmanObjectStore.Data != nil && barmanObjectStore.Data.Encryption != "" { + allErrors = append(allErrors, field.Invalid( + path.Child("data", "encryption"), + barmanObjectStore.Data.Encryption, + message, + )) + } + if barmanObjectStore.Wal != nil && barmanObjectStore.Wal.Encryption != "" { + allErrors = append(allErrors, field.Invalid( + path.Child("wal", "encryption"), + barmanObjectStore.Wal.Encryption, + message, + )) + } + return allErrors } diff --git a/pkg/api/webhooks/config_test.go b/pkg/api/webhooks/config_test.go index edf5e25c..8aa70ef6 100644 --- a/pkg/api/webhooks/config_test.go +++ b/pkg/api/webhooks/config_test.go @@ -20,6 +20,7 @@ SPDX-License-Identifier: Apache-2.0 package webhooks import ( + machineryapi "github.com/cloudnative-pg/machinery/pkg/api" "k8s.io/apimachinery/pkg/util/validation/field" api "github.com/cloudnative-pg/barman-cloud/pkg/api" @@ -40,6 +41,37 @@ var _ = Describe("Backup validation", func() { err := ValidateBackupConfiguration(nil, nil) Expect(err).To(BeEmpty()) }) + + Context("with an SSE-C customer key", func() { + var configuration *api.BarmanObjectStoreConfiguration + BeforeEach(func() { + configuration = &api.BarmanObjectStoreConfiguration{ + BarmanCredentials: api.BarmanCredentials{ + AWS: &api.S3Credentials{ + InheritFromIAMRole: true, + SSECustomerKey: &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{Name: "backup-keys"}, + Key: "sse-c", + }, + }, + }, + } + }) + + It("accepts it on its own", func() { + err := ValidateBackupConfiguration(configuration, field.NewPath("spec")) + Expect(err).To(BeEmpty()) + }) + + It("rejects it together with data or wal encryption", func() { + configuration.Data = &api.DataBackupConfiguration{Encryption: api.EncryptionTypeAES256} + configuration.Wal = &api.WalBackupConfiguration{Encryption: api.EncryptionTypeAES256} + err := ValidateBackupConfiguration(configuration, field.NewPath("spec")) + Expect(err).To(HaveLen(2)) + Expect(err[0].Field).To(Equal("spec.data.encryption")) + Expect(err[1].Field).To(Equal("spec.wal.encryption")) + }) + }) }) var _ = Describe("Retention Policy Validation", func() { From 64ca72a84917525437ddae58d87c4ff4d1a7cdf7 Mon Sep 17 00:00:00 2001 From: Armando Ruocco Date: Wed, 30 Sep 2026 10:42:21 +0200 Subject: [PATCH 3/6] fix: keep a separate SSE-C key file for each secret key A single process can run barman-cloud commands for different object stores at the same time. A replica cluster, for instance, archives WAL to its own object store while restoring it from the source one. With one shared key file, each command overwrote or removed the key of the other: a missing file made the command fail, and a different key made barman-cloud upload objects encrypted with the key of another object store, which the configured key can no longer decrypt. Materialize each key in a file whose path depends on the referenced secret and key, and point the barman-cloud commands at that same path. Signed-off-by: Armando Ruocco --- pkg/command/commandbuilder.go | 2 +- pkg/command/commandbuilder_test.go | 25 +++++++++++++++++++++++-- pkg/credentials/env.go | 21 +++++++++++++-------- pkg/utils/barman.go | 11 +++++++++++ pkg/utils/barman_test.go | 25 +++++++++++++++++++++++++ pkg/utils/constants.go | 8 ++++---- 6 files changed, 77 insertions(+), 15 deletions(-) diff --git a/pkg/command/commandbuilder.go b/pkg/command/commandbuilder.go index 52b02390..e7545db6 100644 --- a/pkg/command/commandbuilder.go +++ b/pkg/command/commandbuilder.go @@ -96,7 +96,7 @@ func appendCloudProviderOptions( options = append( options, "--sse-customer-key", - "file://"+utils.SSECustomerKeyFileLocation) + "file://"+utils.SSECustomerKeyFilePath(credentials.AWS.SSECustomerKey)) } case credentials.Azure != nil: options = append( diff --git a/pkg/command/commandbuilder_test.go b/pkg/command/commandbuilder_test.go index 0bdf859f..425e7a6c 100644 --- a/pkg/command/commandbuilder_test.go +++ b/pkg/command/commandbuilder_test.go @@ -26,7 +26,6 @@ import ( machineryapi "github.com/cloudnative-pg/machinery/pkg/api" barmanApi "github.com/cloudnative-pg/barman-cloud/pkg/api" - "github.com/cloudnative-pg/barman-cloud/pkg/utils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -211,7 +210,29 @@ var _ = Describe("AppendCloudProviderOptions with AWS credentials", func() { Expect(err).ToNot(HaveOccurred()) Expect(result).To(Equal([]string{ "--cloud-provider", "aws-s3", - "--sse-customer-key", "file://" + utils.SSECustomerKeyFileLocation, + "--sse-customer-key", "file:///controller/.sse-customer-keys/sse-c-key/key", })) }) + + It("should point object stores with different customer keys at different files", func(ctx SpecContext) { + keyFileOption := func(secretName string) string { + credentials := barmanApi.BarmanCredentials{ + AWS: &barmanApi.S3Credentials{ + InheritFromIAMRole: true, + SSECustomerKey: &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{ + Name: secretName, + }, + Key: "key", + }, + }, + } + result, err := appendCloudProviderOptions(ctx, []string{}, credentials) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(HaveLen(4)) + return result[3] + } + + Expect(keyFileOption("store-a-key")).ToNot(Equal(keyFileOption("store-b-key"))) + }) }) diff --git a/pkg/credentials/env.go b/pkg/credentials/env.go index 56d1a06b..ec2aa0cf 100644 --- a/pkg/credentials/env.go +++ b/pkg/credentials/env.go @@ -215,12 +215,16 @@ func envSetAWSCredentials( return env, nil } -// reconcileAWSSSECustomerKey materializes (or removes) the S3 SSE-C customer -// key file referenced by the S3 credentials. barman-cloud consumes it through -// the '--sse-customer-key file://' option, so the key must exist on disk next -// to the process that runs the barman-cloud commands. The referenced secret is -// expected to contain a base64-encoded 256-bit AES key, which barman-cloud -// validates when it reads the file. +// reconcileAWSSSECustomerKey materializes the S3 SSE-C customer key file +// referenced by the S3 credentials. barman-cloud consumes it through the +// '--sse-customer-key file://' option, so the key must exist on disk next to +// the process that runs the barman-cloud commands. The file path depends on +// the referenced secret and key: a single process can serve object stores +// with different keys concurrently (e.g. a replica cluster archiving to its +// own object store while restoring from the source one), and a shared file +// would let a command pick up the key of another object store. The +// referenced secret is expected to contain a base64-encoded 256-bit AES key, +// which barman-cloud validates when it reads the file. func reconcileAWSSSECustomerKey( ctx context.Context, c client.Client, @@ -228,7 +232,7 @@ func reconcileAWSSSECustomerKey( s3credentials *barmanApi.S3Credentials, ) error { if s3credentials.SSECustomerKey == nil { - return fileutils.RemoveFile(utils.SSECustomerKeyFileLocation) + return nil } key, err := extractValueFromSecret(ctx, c, s3credentials.SSECustomerKey, namespace) @@ -236,7 +240,8 @@ func reconcileAWSSSECustomerKey( return err } - _, err = fileutils.WriteFileAtomic(utils.SSECustomerKeyFileLocation, key, 0o600) + _, err = fileutils.WriteFileAtomic( + utils.SSECustomerKeyFilePath(s3credentials.SSECustomerKey), key, 0o600) return err } diff --git a/pkg/utils/barman.go b/pkg/utils/barman.go index cbeafc61..8989b938 100644 --- a/pkg/utils/barman.go +++ b/pkg/utils/barman.go @@ -23,7 +23,10 @@ import ( "errors" "fmt" "math" + "path" "regexp" + + machineryapi "github.com/cloudnative-pg/machinery/pkg/api" ) var regexPolicy = regexp.MustCompile(`([1-9][0-9]*)([dwm])$`) @@ -63,3 +66,11 @@ func MapToBarmanTagsFormat(option string, mapTags map[string]string) ([]string, return tags, nil } + +// SSECustomerKeyFilePath returns the path where the S3 SSE-C customer key +// referenced by the passed selector is materialized. The path depends on the +// referenced secret and key, so that object stores using different keys +// never share the same file, even when their commands run concurrently. +func SSECustomerKeyFilePath(selector *machineryapi.SecretKeySelector) string { + return path.Join(SSECustomerKeysDirectory, selector.Name, selector.Key) +} diff --git a/pkg/utils/barman_test.go b/pkg/utils/barman_test.go index dcc75287..e6aab5f8 100644 --- a/pkg/utils/barman_test.go +++ b/pkg/utils/barman_test.go @@ -20,6 +20,8 @@ SPDX-License-Identifier: Apache-2.0 package utils //nolint:revive import ( + machineryapi "github.com/cloudnative-pg/machinery/pkg/api" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -54,3 +56,26 @@ var _ = Describe("converting map to barman tags format", func() { Expect(MapToBarmanTagsFormat("test", tags)).To(BeEquivalentTo([]string{"test", "retentionDays,90days"})) }) }) + +var _ = Describe("SSE-C customer key file path", func() { + selector := func(name, key string) *machineryapi.SecretKeySelector { + return &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{Name: name}, + Key: key, + } + } + + It("never shares a file between different secret keys", func() { + paths := []string{ + SSECustomerKeyFilePath(selector("store-a", "key")), + SSECustomerKeyFilePath(selector("store-b", "key")), + SSECustomerKeyFilePath(selector("store-a", "other")), + SSECustomerKeyFilePath(selector("store-a.key", "x")), + SSECustomerKeyFilePath(selector("store-a", "key.x")), + } + Expect(paths).To(HaveLen(5)) + for i := range paths { + Expect(paths[i+1:]).ToNot(ContainElement(paths[i])) + } + }) +}) diff --git a/pkg/utils/constants.go b/pkg/utils/constants.go index 410cc071..76af7302 100644 --- a/pkg/utils/constants.go +++ b/pkg/utils/constants.go @@ -44,8 +44,8 @@ const ( // BarmanCloudCheckWalArchive is the command name for 'barman-cloud-check-wal-archive' BarmanCloudCheckWalArchive = "barman-cloud-check-wal-archive" - // SSECustomerKeyFileLocation is the path where the S3 SSE-C customer key is - // materialized from its secret so that it can be passed to the barman-cloud - // commands via the '--sse-customer-key file://' option. - SSECustomerKeyFileLocation = "/controller/.sse-customer-key" + // SSECustomerKeysDirectory is the directory where the S3 SSE-C customer + // keys are materialized from their secrets so that they can be passed to + // the barman-cloud commands via the '--sse-customer-key file://' option. + SSECustomerKeysDirectory = "/controller/.sse-customer-keys" ) From 58d73632c02e78123e487e6a99b4d8e1f538d113 Mon Sep 17 00:00:00 2001 From: Armando Ruocco Date: Wed, 30 Sep 2026 13:39:31 +0200 Subject: [PATCH 4/6] fix: serialize the writes of the SSE-C key files The object stores sharing a key secret share its file, and a replica cluster can archive and restore WAL at the same time. WriteFileAtomic names its temporary file after the current second, so two concurrent writes of the same key could use the same temporary file: one of them could then fail its rename, or truncate the key file the other had just moved into place, making barman-cloud read an invalid key. Signed-off-by: Armando Ruocco --- pkg/credentials/env.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/credentials/env.go b/pkg/credentials/env.go index ec2aa0cf..751ee343 100644 --- a/pkg/credentials/env.go +++ b/pkg/credentials/env.go @@ -22,6 +22,7 @@ package credentials import ( "context" "fmt" + "sync" machineryapi "github.com/cloudnative-pg/machinery/pkg/api" "github.com/cloudnative-pg/machinery/pkg/fileutils" @@ -225,6 +226,12 @@ func envSetAWSCredentials( // would let a command pick up the key of another object store. The // referenced secret is expected to contain a base64-encoded 256-bit AES key, // which barman-cloud validates when it reads the file. +// sseCustomerKeyMutex serializes the writes of the SSE-C customer key files. +// fileutils.WriteFileAtomic names its temporary file after the current second, +// so two concurrent writes of the same key file would share it, and one of +// them could truncate the file the other has just renamed into place. +var sseCustomerKeyMutex sync.Mutex + func reconcileAWSSSECustomerKey( ctx context.Context, c client.Client, @@ -240,6 +247,8 @@ func reconcileAWSSSECustomerKey( return err } + sseCustomerKeyMutex.Lock() + defer sseCustomerKeyMutex.Unlock() _, err = fileutils.WriteFileAtomic( utils.SSECustomerKeyFilePath(s3credentials.SSECustomerKey), key, 0o600) From b232ab214c3b2af6e336b03a0ea3b33bc4a0933b Mon Sep 17 00:00:00 2001 From: Armando Ruocco Date: Wed, 30 Sep 2026 14:57:00 +0200 Subject: [PATCH 5/6] fix: do not pass the SSE-C key to barman-cloud-check-wal-archive The barman-cloud-check-wal-archive command is the only one without the --sse-customer-key option, and it rejects it. Since the check runs before the first WAL file is archived, an object store with an SSE-C key could never start archiving. The command only lists the objects in the bucket, so it does not need the key. Signed-off-by: Armando Ruocco --- pkg/archiver/archiver.go | 7 +++++++ pkg/archiver/command_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/pkg/archiver/archiver.go b/pkg/archiver/archiver.go index 18e48155..d6b3bd9e 100644 --- a/pkg/archiver/archiver.go +++ b/pkg/archiver/archiver.go @@ -23,6 +23,7 @@ package archiver import ( "context" "fmt" + "slices" "time" "github.com/cloudnative-pg/machinery/pkg/log" @@ -154,6 +155,12 @@ func (archiver *WALArchiver) BarmanCloudCheckWalArchiveOptions( return nil, err } + // barman-cloud-check-wal-archive only lists the objects in the bucket: + // it does not need the SSE-C customer key, and it rejects the option + if i := slices.Index(options, "--sse-customer-key"); i >= 0 { + options = slices.Delete(options, i, i+2) + } + serverName := clusterName if len(configuration.ServerName) != 0 { serverName = configuration.ServerName diff --git a/pkg/archiver/command_test.go b/pkg/archiver/command_test.go index d8df9c33..048a425f 100644 --- a/pkg/archiver/command_test.go +++ b/pkg/archiver/command_test.go @@ -23,6 +23,8 @@ import ( "os" "strings" + machineryapi "github.com/cloudnative-pg/machinery/pkg/api" + barmanApi "github.com/cloudnative-pg/barman-cloud/pkg/api" . "github.com/onsi/ginkgo/v2" @@ -147,3 +149,28 @@ var _ = Describe("barmanCloudWalArchiveOptions", func() { )) }) }) + +var _ = Describe("BarmanCloudCheckWalArchiveOptions", func() { + It("does not pass the SSE-C customer key, which the command rejects", func(ctx SpecContext) { + config := &barmanApi.BarmanObjectStoreConfiguration{ + DestinationPath: "s3://bucket-name/", + EndpointURL: "http://s3:9000", + BarmanCredentials: barmanApi.BarmanCredentials{ + AWS: &barmanApi.S3Credentials{ + InheritFromIAMRole: true, + SSECustomerKey: &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{Name: "sse-c"}, + Key: "key", + }, + }, + }, + } + options, err := (&WALArchiver{}).BarmanCloudCheckWalArchiveOptions(ctx, config, "test-cluster") + Expect(err).ToNot(HaveOccurred()) + Expect(options).To(Equal([]string{ + "--endpoint-url", "http://s3:9000", + "--cloud-provider", "aws-s3", + "s3://bucket-name/", "test-cluster", + })) + }) +}) From 7f0d7a635a7736c22321121cccbb3abab7a8160c Mon Sep 17 00:00:00 2001 From: Armando Ruocco Date: Wed, 30 Sep 2026 15:40:10 +0200 Subject: [PATCH 6/6] docs: keep the doc comment of reconcileAWSSSECustomerKey attached to it The declaration of the mutex had split it from the function, so it documented the mutex instead. Signed-off-by: Armando Ruocco --- pkg/credentials/env.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/credentials/env.go b/pkg/credentials/env.go index 751ee343..3433aa12 100644 --- a/pkg/credentials/env.go +++ b/pkg/credentials/env.go @@ -216,6 +216,12 @@ func envSetAWSCredentials( return env, nil } +// sseCustomerKeyMutex serializes the writes of the SSE-C customer key files. +// fileutils.WriteFileAtomic names its temporary file after the current second, +// so two concurrent writes of the same key file would share it, and one of +// them could truncate the file the other has just renamed into place. +var sseCustomerKeyMutex sync.Mutex + // reconcileAWSSSECustomerKey materializes the S3 SSE-C customer key file // referenced by the S3 credentials. barman-cloud consumes it through the // '--sse-customer-key file://' option, so the key must exist on disk next to @@ -226,12 +232,6 @@ func envSetAWSCredentials( // would let a command pick up the key of another object store. The // referenced secret is expected to contain a base64-encoded 256-bit AES key, // which barman-cloud validates when it reads the file. -// sseCustomerKeyMutex serializes the writes of the SSE-C customer key files. -// fileutils.WriteFileAtomic names its temporary file after the current second, -// so two concurrent writes of the same key file would share it, and one of -// them could truncate the file the other has just renamed into place. -var sseCustomerKeyMutex sync.Mutex - func reconcileAWSSSECustomerKey( ctx context.Context, c client.Client,