Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support encryption for backup data #6723

Merged
merged 2 commits into from
Mar 5, 2024
Merged
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
5 changes: 5 additions & 0 deletions apis/dataprotection/v1alpha1/backup_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ type BackupStatus struct {
// +optional
BackupMethod *BackupMethod `json:"backupMethod,omitempty"`

// Records the encryption config for this backup.
//
// +optional
EncryptionConfig *EncryptionConfig `json:"encryptionConfig,omitempty"`

// Records the actions status for this backup.
//
// +optional
Expand Down
6 changes: 6 additions & 0 deletions apis/dataprotection/v1alpha1/backuppolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ type BackupPolicySpec struct {
// +optional
// +kubebuilder:default=false
UseKopia bool `json:"useKopia"`

// Specifies the parameters for encrypting backup data.
// Encryption will be disabled if the field is not set.
//
// +optional
EncryptionConfig *EncryptionConfig `json:"encryptionConfig,omitempty"`
}

type BackupTarget struct {
Expand Down
22 changes: 22 additions & 0 deletions apis/dataprotection/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"strings"
"time"
"unicode"

corev1 "k8s.io/api/core/v1"
)

// Phase defines the BackupPolicy and ActionSet CR .status.phase
Expand Down Expand Up @@ -228,3 +230,23 @@ const (
VolumeClaimRestorePolicyParallel VolumeClaimRestorePolicy = "Parallel"
VolumeClaimRestorePolicySerial VolumeClaimRestorePolicy = "Serial"
)

// EncryptionConfig defines the parameters for encrypting backup data.
type EncryptionConfig struct {
// Specifies the encryption algorithm. Currently supported algorithms are:
//
// - AES-128-CFB
// - AES-192-CFB
// - AES-256-CFB
//
// +kubebuilder:validation:Required
// +kubebuilder:default=AES-256-CFB
// +kubebuilder:validation:Enum={AES-128-CFB,AES-192-CFB,AES-256-CFB}
Algorithm string `json:"algorithm"`

// Selects the key of a secret in the current namespace, the value of the secret
// is used as the encryption key.
//
// +kubebuilder:validation:Required
PassPhraseSecretKeyRef *corev1.SecretKeySelector `json:"passPhraseSecretKeyRef"`
}
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,43 @@ spec:
repository.
pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$
type: string
encryptionConfig:
description: Specifies the parameters for encrypting backup data.
Encryption will be disabled if the field is not set.
properties:
algorithm:
default: AES-256-CFB
description: "Specifies the encryption algorithm. Currently supported
algorithms are: \n - AES-128-CFB - AES-192-CFB - AES-256-CFB"
enum:
- AES-128-CFB
- AES-192-CFB
- AES-256-CFB
type: string
passPhraseSecretKeyRef:
description: Selects the key of a secret in the current namespace,
the value of the secret is used as the encryption key.
properties:
key:
description: The key of the secret to select from. Must be
a valid secret key.
type: string
name:
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
type: string
optional:
description: Specify whether the Secret or its key must be
defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
required:
- algorithm
- passPhraseSecretKeyRef
type: object
pathPrefix:
description: Specifies the directory inside the backup repository
to store the backup. This path is relative to the path of the backup
Expand Down
36 changes: 36 additions & 0 deletions config/crd/bases/dataprotection.kubeblocks.io_backups.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,42 @@ spec:
description: Records the duration of the backup operation. When converted
to a string, the format is "1h2m0.5s".
type: string
encryptionConfig:
description: Records the encryption config for this backup.
properties:
algorithm:
default: AES-256-CFB
description: "Specifies the encryption algorithm. Currently supported
algorithms are: \n - AES-128-CFB - AES-192-CFB - AES-256-CFB"
enum:
- AES-128-CFB
- AES-192-CFB
- AES-256-CFB
type: string
passPhraseSecretKeyRef:
description: Selects the key of a secret in the current namespace,
the value of the secret is used as the encryption key.
properties:
key:
description: The key of the secret to select from. Must be
a valid secret key.
type: string
name:
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
type: string
optional:
description: Specify whether the Secret or its key must be
defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
required:
- algorithm
- passPhraseSecretKeyRef
type: object
expiration:
description: Indicates when this backup becomes eligible for garbage
collection. A 'null' value implies that the backup will not be cleaned
Expand Down
15 changes: 15 additions & 0 deletions controllers/dataprotection/backup_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,18 @@ func (r *BackupReconciler) prepareBackupRequest(
}
}

// check encryption config
if backupPolicy.Spec.EncryptionConfig != nil {
secretKeyRef := backupPolicy.Spec.EncryptionConfig.PassPhraseSecretKeyRef
if secretKeyRef == nil {
return nil, fmt.Errorf("encryptionConfig.passPhraseSecretKeyRef if empty")
}
err := checkSecretKeyRef(reqCtx, r.Client, request.Namespace, secretKeyRef)
if err != nil {
return nil, fmt.Errorf("failed to check encryption key reference: %w", err)
}
}

request.BackupPolicy = backupPolicy
if !snapshotVolumes {
// if use volume snapshot, ignore backup repo
Expand Down Expand Up @@ -355,6 +367,9 @@ func (r *BackupReconciler) patchBackupStatus(
if request.BackupPolicy.Spec.UseKopia {
request.Status.KopiaRepoPath = dpbackup.BuildKopiaRepoPath(request.Backup, request.BackupPolicy.Spec.PathPrefix)
}
if request.BackupPolicy.Spec.EncryptionConfig != nil {
request.Status.EncryptionConfig = request.BackupPolicy.Spec.EncryptionConfig
}
// init action status
actions, err := request.BuildActions()
if err != nil {
Expand Down
90 changes: 90 additions & 0 deletions controllers/dataprotection/backup_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package dataprotection

import (
"fmt"
"slices"
"time"

. "github.com/onsi/ginkgo/v2"
Expand Down Expand Up @@ -329,6 +330,95 @@ var _ = Describe("Backup Controller test", func() {
})
})

Context("creates a backup with encryption", func() {
const (
encryptionKeySecretName = "backup-encryption"
keyName = "password"
)
It("should fail if encryption key secret is not present", func() {
By("set encryptionConfig")
Expect(testapps.ChangeObj(&testCtx, backupPolicy, func(bp *dpv1alpha1.BackupPolicy) {
backupPolicy.Spec.EncryptionConfig = &dpv1alpha1.EncryptionConfig{
Algorithm: "AES-256-CFB",
PassPhraseSecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: encryptionKeySecretName,
},
Key: keyName,
},
}
})).Should(Succeed())

By("create a backup")
backup := testdp.NewFakeBackup(&testCtx, nil)

By("check the backup, and it should be failed")
Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(backup), func(g Gomega, fetched *dpv1alpha1.Backup) {
g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.BackupPhaseFailed))
})).Should(Succeed())
})

It("should run the backup with encryption envs", func() {
By("set encryptionConfig")
Expect(testapps.ChangeObj(&testCtx, backupPolicy, func(bp *dpv1alpha1.BackupPolicy) {
backupPolicy.Spec.EncryptionConfig = &dpv1alpha1.EncryptionConfig{
Algorithm: "AES-256-CFB",
PassPhraseSecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: encryptionKeySecretName,
},
Key: keyName,
},
}
})).Should(Succeed())

By("create the encryption key secret")
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: encryptionKeySecretName,
Namespace: testCtx.DefaultNamespace,
},
StringData: map[string]string{
keyName: "whatever",
},
}
testapps.CreateK8sResource(&testCtx, secret)

By("create a backup")
backup := testdp.NewFakeBackup(&testCtx, nil)

By("check the backup")
Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(backup), func(g Gomega, fetched *dpv1alpha1.Backup) {
g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.BackupPhaseRunning))
g.Expect(fetched.Status.EncryptionConfig).ShouldNot(BeNil())
})).Should(Succeed())

By("check the backup job")
getJobKey := func(index int) client.ObjectKey {
return client.ObjectKey{
Name: dpbackup.GenerateBackupJobName(backup, fmt.Sprintf("%s-%d", dpbackup.BackupDataJobNamePrefix, index)),
Namespace: backup.Namespace,
}
}
Eventually(testapps.CheckObj(&testCtx, getJobKey(0), func(g Gomega, job *batchv1.Job) {
g.Expect(len(job.Spec.Template.Spec.Containers)).ShouldNot(BeZero())
expectedEnvs := []string{
dptypes.DPDatasafedEncryptionAlgorithm,
dptypes.DPDatasafedEncryptionPassPhrase,
}
for _, c := range job.Spec.Template.Spec.Containers {
count := 0
for _, env := range c.Env {
if slices.Contains(expectedEnvs, env.Name) {
count++
}
}
g.Expect(count).To(BeEquivalentTo(len(expectedEnvs)))
}
})).Should(Succeed())
})
})

Context("deletes a backup", func() {
var (
backupKey types.NamespacedName
Expand Down
23 changes: 23 additions & 0 deletions controllers/dataprotection/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,29 @@ func EnsureWorkerServiceAccount(reqCtx intctrlutil.RequestCtx, cli client.Client
return saName, nil
}

func checkSecretKeyRef(reqCtx intctrlutil.RequestCtx, cli client.Client,
namespace string, ref *corev1.SecretKeySelector) error {
if ref == nil {
return fmt.Errorf("ref is nil")
}
secret := &corev1.Secret{}
err := cli.Get(reqCtx.Ctx, client.ObjectKey{
Namespace: namespace,
Name: ref.Name,
}, secret)
if err != nil {
if apierrors.IsNotFound(err) {
return fmt.Errorf("secret (%s/%s) is not found", ref.Name, namespace)
}
return err
}
if _, has := secret.Data[ref.Key]; !has {
return fmt.Errorf("secret (%s/%s) doesn't contain key %s",
ref.Name, namespace, ref.Key)
}
return nil
}

// ============================================================================
// refObjectMapper
// ============================================================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,43 @@ spec:
repository.
pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$
type: string
encryptionConfig:
description: Specifies the parameters for encrypting backup data.
Encryption will be disabled if the field is not set.
properties:
algorithm:
default: AES-256-CFB
description: "Specifies the encryption algorithm. Currently supported
algorithms are: \n - AES-128-CFB - AES-192-CFB - AES-256-CFB"
enum:
- AES-128-CFB
- AES-192-CFB
- AES-256-CFB
type: string
passPhraseSecretKeyRef:
description: Selects the key of a secret in the current namespace,
the value of the secret is used as the encryption key.
properties:
key:
description: The key of the secret to select from. Must be
a valid secret key.
type: string
name:
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
type: string
optional:
description: Specify whether the Secret or its key must be
defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
required:
- algorithm
- passPhraseSecretKeyRef
type: object
pathPrefix:
description: Specifies the directory inside the backup repository
to store the backup. This path is relative to the path of the backup
Expand Down
36 changes: 36 additions & 0 deletions deploy/helm/crds/dataprotection.kubeblocks.io_backups.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,42 @@ spec:
description: Records the duration of the backup operation. When converted
to a string, the format is "1h2m0.5s".
type: string
encryptionConfig:
description: Records the encryption config for this backup.
properties:
algorithm:
default: AES-256-CFB
description: "Specifies the encryption algorithm. Currently supported
algorithms are: \n - AES-128-CFB - AES-192-CFB - AES-256-CFB"
enum:
- AES-128-CFB
- AES-192-CFB
- AES-256-CFB
type: string
passPhraseSecretKeyRef:
description: Selects the key of a secret in the current namespace,
the value of the secret is used as the encryption key.
properties:
key:
description: The key of the secret to select from. Must be
a valid secret key.
type: string
name:
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
type: string
optional:
description: Specify whether the Secret or its key must be
defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
required:
- algorithm
- passPhraseSecretKeyRef
type: object
expiration:
description: Indicates when this backup becomes eligible for garbage
collection. A 'null' value implies that the backup will not be cleaned
Expand Down
Loading
Loading