From 76e12b2044aa59bc4fa93cde26094aa1b610d951 Mon Sep 17 00:00:00 2001 From: Florian Schauer Date: Sun, 19 Jul 2026 04:34:41 +0200 Subject: [PATCH] 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 | 14 ++++++++++ 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, 112 insertions(+) diff --git a/pkg/api/config.go b/pkg/api/config.go index 0e731081..849cde75 100644 --- a/pkg/api/config.go +++ b/pkg/api/config.go @@ -109,6 +109,20 @@ 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 orthogonal to the bucket-managed `encryption` + // field (SSE-S3/SSE-KMS) and is meant for S3-compatible providers + // that only support customer-provided keys (e.g. Hetzner Object + // Storage). 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" )