Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions pkg/api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Comment on lines +114 to +116

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this reads better, right? The previous statement was kind of saying that objects are encrypted during download.

Suggested change
// 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).
// When set, it is the key barman-cloud uses to encrypt objects uploaded
// to S3 and to decrypt them when they are read back, following 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"`
Expand Down
32 changes: 32 additions & 0 deletions pkg/api/webhooks/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
32 changes: 32 additions & 0 deletions pkg/api/webhooks/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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() {
Expand Down
5 changes: 5 additions & 0 deletions pkg/api/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions pkg/archiver/archiver.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ package archiver
import (
"context"
"fmt"
"slices"
"time"

"github.com/cloudnative-pg/machinery/pkg/log"
Expand Down Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions pkg/archiver/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
}))
})
})
11 changes: 11 additions & 0 deletions pkg/command/commandbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.SSECustomerKeyFilePath(credentials.AWS.SSECustomerKey))
}
case credentials.Azure != nil:
options = append(
options,
Expand Down
64 changes: 64 additions & 0 deletions pkg/command/commandbuilder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,67 @@ 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:///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")))
})
})
48 changes: 48 additions & 0 deletions pkg/credentials/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ package credentials
import (
"context"
"fmt"
"sync"

machineryapi "github.com/cloudnative-pg/machinery/pkg/api"
"github.com/cloudnative-pg/machinery/pkg/fileutils"
corev1 "k8s.io/api/core/v1"
"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 (
Expand Down Expand Up @@ -142,6 +144,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
}
Expand Down Expand Up @@ -207,6 +216,45 @@ 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
// 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,
namespace string,
s3credentials *barmanApi.S3Credentials,
) error {
if s3credentials.SSECustomerKey == nil {
return nil
}

key, err := extractValueFromSecret(ctx, c, s3credentials.SSECustomerKey, namespace)
if err != nil {
return err
}

sseCustomerKeyMutex.Lock()
defer sseCustomerKeyMutex.Unlock()
_, err = fileutils.WriteFileAtomic(
utils.SSECustomerKeyFilePath(s3credentials.SSECustomerKey), key, 0o600)

return err
}

// envSetAzureCredentials sets the Azure environment variables given the configuration
// inside the cluster
func envSetAzureCredentials(
Expand Down
11 changes: 11 additions & 0 deletions pkg/utils/barman.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])$`)
Expand Down Expand Up @@ -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)
}
Loading
Loading