From eb2e95a64bdc5128652097e00bac4b4f59fb3bc8 Mon Sep 17 00:00:00 2001 From: Karel Vanden Houte Date: Thu, 22 Jun 2023 12:57:12 +0200 Subject: [PATCH 1/3] Add option for custom annotations and labels on sealing keypairs Signed-off-by: Karel Vanden Houte --- cmd/controller/main.go | 2 + helm/sealed-secrets/README.md | 4 +- helm/sealed-secrets/templates/deployment.yaml | 22 ++++++++++ helm/sealed-secrets/values.yaml | 6 +++ pkg/controller/keyregistry.go | 4 +- pkg/controller/keys.go | 37 +++++++++++++--- pkg/controller/keys_test.go | 42 ++++++++++++++++++- pkg/controller/main.go | 42 ++++++++++--------- pkg/controller/main_test.go | 20 ++++----- 9 files changed, 138 insertions(+), 41 deletions(-) diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 19fe79ac54..732e284ed1 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -42,6 +42,8 @@ func bindControllerFlags(f *controller.Flags, fs *flag.FlagSet) { fs.StringVar(&f.LabelSelector, "label-selector", "", "Label selector which can be used to filter sealed secrets.") fs.IntVar(&f.RateLimitPerSecond, "rate-limit", 2, "Number of allowed sustained request per second for verify endpoint") fs.IntVar(&f.RateLimitBurst, "rate-limit-burst", 2, "Number of requests allowed to exceed the rate limit per second for verify endpoint") + fs.StringVar(&f.PrivateKeyAnnotations, "privatekey-annotations", "", "Comma-separated list of additional annotations to be put on renewed sealing keys.") + fs.StringVar(&f.PrivateKeyLabels, "privatekey-labels", "", "Comma-separated list of additional labels to be put on renewed sealing keys.") fs.BoolVar(&f.OldGCBehavior, "old-gc-behavior", false, "Revert to old GC behavior where the controller deletes secrets instead of delegating that to k8s itself.") diff --git a/helm/sealed-secrets/README.md b/helm/sealed-secrets/README.md index 03f0024767..16f8773e21 100644 --- a/helm/sealed-secrets/README.md +++ b/helm/sealed-secrets/README.md @@ -95,7 +95,9 @@ The command removes all the Kubernetes components associated with the chart and | `keyrenewperiod` | Specifies key renewal period. Default 30 days | `""` | | `rateLimit` | Number of allowed sustained request per second for verify endpoint | `""` | | `rateLimitBurst` | Number of requests allowed to exceed the rate limit per second for verify endpoint | `""` | -| `additionalNamespaces` | List of namespaces used to manage the Sealed Secrets | `[]` | +| `additionalNamespaces` | List of namespaces used to manage the Sealed Secrets | `[]` +| `privateKeyAnnotations` | Map of annotations to be set on the sealing keypairs | `{}` +| `privateKeyLabels` | Map of labels to be set on the sealing keypairs | `{}` | | `logInfoStdout` | Specifies whether the Sealed Secrets controller will log info to stdout | `false` | | `command` | Override default container command | `[]` | | `args` | Override default container args | `[]` | diff --git a/helm/sealed-secrets/templates/deployment.yaml b/helm/sealed-secrets/templates/deployment.yaml index 079755a379..e9b76ce135 100644 --- a/helm/sealed-secrets/templates/deployment.yaml +++ b/helm/sealed-secrets/templates/deployment.yaml @@ -89,6 +89,28 @@ spec: - --additional-namespaces - {{ join "," .Values.additionalNamespaces | quote }} {{- end }} + {{- if $.Values.privateKeyAnnotations }} + {{- $privatekeyAnnotations := ""}} + {{- range $k, $v := $.Values.privateKeyAnnotations }} + {{- if not (and $v (kindIs "string" $v)) }} + {{ fail "Annotation values have to be strings"}} + {{- end }} + {{- $privatekeyAnnotations = printf "%s=%s,%s" $k $v $privatekeyAnnotations}} + {{- end }} + - --privatekey-annotations + - {{ trimSuffix "," $privatekeyAnnotations | quote }} + {{- end }} + {{- if $.Values.privateKeyLabels }} + {{- $privateKeyLabels := ""}} + {{- range $k, $v := $.Values.privateKeyLabels }} + {{- if not (and $v (kindIs "string" $v)) }} + {{ fail "Label values have to be strings"}} + {{- end }} + {{- $privateKeyLabels = printf "%s=%s,%s" $k $v $privateKeyLabels}} + {{- end }} + - --privatekey-labels + - {{ trimSuffix "," $privateKeyLabels | quote }} + {{- end }} {{- if .Values.logInfoStdout }} - --log-info-stdout {{- end }} diff --git a/helm/sealed-secrets/values.yaml b/helm/sealed-secrets/values.yaml index df5bf9e831..14de1d836d 100644 --- a/helm/sealed-secrets/values.yaml +++ b/helm/sealed-secrets/values.yaml @@ -76,6 +76,12 @@ rateLimitBurst: "" ## @param additionalNamespaces List of namespaces used to manage the Sealed Secrets ## additionalNamespaces: [] +## @param privateKeyAnnotations Map of annotations to be set on the sealing keypairs +## +privateKeyAnnotations: {} +## @param privateKeyLabels Map of labels to be set on the sealing keypairs +## +privateKeyLabels: {} ## @param logInfoStdout Specifies whether the Sealed Secrets controller will log info to stdout ## logInfoStdout: false diff --git a/pkg/controller/keyregistry.go b/pkg/controller/keyregistry.go index cb56c6c2c9..d35872bf31 100644 --- a/pkg/controller/keyregistry.go +++ b/pkg/controller/keyregistry.go @@ -47,13 +47,13 @@ func NewKeyRegistry(client kubernetes.Interface, namespace, keyPrefix, keyLabel } } -func (kr *KeyRegistry) generateKey(ctx context.Context, validFor time.Duration, cn string) (string, error) { +func (kr *KeyRegistry) generateKey(ctx context.Context, validFor time.Duration, cn string, privateKeyAnnotations string, privateKeyLabels string) (string, error) { key, cert, err := generatePrivateKeyAndCert(kr.keysize, validFor, cn) if err != nil { return "", err } certs := []*x509.Certificate{cert} - generatedName, err := writeKey(ctx, kr.client, key, certs, kr.namespace, kr.keyLabel, kr.keyPrefix) + generatedName, err := writeKey(ctx, kr.client, key, certs, kr.namespace, kr.keyLabel, kr.keyPrefix, privateKeyAnnotations, privateKeyLabels) if err != nil { return "", err } diff --git a/pkg/controller/keys.go b/pkg/controller/keys.go index 66d68c1f90..08710a6156 100644 --- a/pkg/controller/keys.go +++ b/pkg/controller/keys.go @@ -6,6 +6,7 @@ import ( "crypto/x509" "encoding/pem" "errors" + "strings" "time" "github.com/bitnami-labs/sealed-secrets/pkg/crypto" @@ -52,7 +53,7 @@ func writeKeyWithCreationTime(t metav1.Time) writeKeyOpt { return func(opts *writeKeyOpts) { opts.creationTime = t } } -func writeKey(ctx context.Context, client kubernetes.Interface, key *rsa.PrivateKey, certs []*x509.Certificate, namespace, label, prefix string, optSetters ...writeKeyOpt) (string, error) { +func writeKey(ctx context.Context, client kubernetes.Interface, key *rsa.PrivateKey, certs []*x509.Certificate, namespace, krLabel, prefix string, additionalAnnotations string, additionalLabels string, optSetters ...writeKeyOpt) (string, error) { var opts writeKeyOpts for _, o := range optSetters { o(&opts) @@ -62,13 +63,37 @@ func writeKey(ctx context.Context, client kubernetes.Interface, key *rsa.Private for _, cert := range certs { certbytes = append(certbytes, pem.EncodeToMemory(&pem.Block{Type: certUtil.CertificateBlockType, Bytes: cert.Raw})...) } + + labels := map[string]string{ + krLabel: "active", + } + + annotations := map[string]string{} + + if additionalLabels != "" { + for _, label := range removeDuplicates(strings.Split(additionalLabels, ",")) { + key := strings.Split(label, "=")[0] + value := strings.Split(label, "=")[1] + if key != krLabel { + labels[key] = value + } + } + } + + if additionalAnnotations != "" { + for _, label := range removeDuplicates(strings.Split(additionalAnnotations, ",")) { + key := strings.Split(label, "=")[0] + value := strings.Split(label, "=")[1] + annotations[key] = value + } + } + secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - GenerateName: prefix, - Labels: map[string]string{ - label: "active", - }, + Namespace: namespace, + GenerateName: prefix, + Labels: labels, + Annotations: annotations, CreationTimestamp: opts.creationTime, }, Data: map[string][]byte{ diff --git a/pkg/controller/keys_test.go b/pkg/controller/keys_test.go index 0c435a2254..7ff678939f 100644 --- a/pkg/controller/keys_test.go +++ b/pkg/controller/keys_test.go @@ -8,13 +8,16 @@ import ( "io" mathrand "math/rand" "reflect" + "strings" "testing" "time" "github.com/bitnami-labs/sealed-secrets/pkg/crypto" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/fake" + ktesting "k8s.io/client-go/testing" certUtil "k8s.io/client-go/util/cert" "k8s.io/client-go/util/keyutil" ) @@ -86,7 +89,12 @@ func TestWriteKey(t *testing.T) { client := fake.NewSimpleClientset() - _, err = writeKey(ctx, client, key, []*x509.Certificate{cert}, "myns", "label", "mykey") + namespace := "myns" + defaultLabel := "default-label" + myKey := "mykey" + additionalAnnotations := "testAnnotation1=additional.annotation,test.annotation.2=test/2" + additionalLabels := "testLabel1=additional.label,test.label.2=test/2" + _, err = writeKey(ctx, client, key, []*x509.Certificate{cert}, namespace, defaultLabel, myKey, additionalAnnotations, additionalLabels) if err != nil { t.Errorf("writeKey() failed with: %v", err) } @@ -95,7 +103,37 @@ func TestWriteKey(t *testing.T) { if a := findAction(client, "create", "secrets"); a == nil { t.Errorf("writeKey didn't create a secret") - } else if a.GetNamespace() != "myns" { + } else if a.GetNamespace() != namespace { t.Errorf("writeKey() created key in wrong namespace!") } + a := findAction(client, "create", "secrets").(ktesting.CreateActionImpl) + secret, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(a.Object) + generateName := secret["metadata"].(map[string]interface{})["generateName"].(string) + + if generateName != myKey { + t.Errorf("writeKey didn't set the correct name") + } + + labels := secret["metadata"].(map[string]interface{})["labels"] + annotations := secret["metadata"].(map[string]interface{})["annotations"] + + if labels.(map[string]interface{})[defaultLabel] != "active" { + t.Errorf("writeKey didn't set default label") + } + + for _, label := range strings.Split(additionalLabels, ",") { + labelKey := strings.Split(label, "=")[0] + labelValue := strings.Split(label, "=")[1] + if labels.(map[string]interface{})[labelKey] != labelValue { + t.Errorf("writeKey didn't set label " + labelKey + " to value '" + labelValue + "'") + } + } + + for _, annotation := range strings.Split(additionalAnnotations, ",") { + annotationKey := strings.Split(annotation, "=")[0] + annotationValue := strings.Split(annotation, "=")[1] + if annotations.(map[string]interface{})[annotationKey] != annotationValue { + t.Errorf("writeKey didn't set annotation '" + annotationKey + "' to value '" + annotationValue + "'") + } + } } diff --git a/pkg/controller/main.go b/pkg/controller/main.go index 9e67339be2..3424438d04 100644 --- a/pkg/controller/main.go +++ b/pkg/controller/main.go @@ -35,22 +35,24 @@ var ( // Flags to configure the controller type Flags struct { - KeyPrefix string - KeySize int - ValidFor time.Duration - MyCN string - KeyRenewPeriod time.Duration - AcceptV1Data bool - KeyCutoffTime string - NamespaceAll bool - AdditionalNamespaces string - LabelSelector string - RateLimitPerSecond int - RateLimitBurst int - OldGCBehavior bool - UpdateStatus bool - SkipRecreate bool - LogInfoToStdout bool + KeyPrefix string + KeySize int + ValidFor time.Duration + MyCN string + KeyRenewPeriod time.Duration + AcceptV1Data bool + KeyCutoffTime string + NamespaceAll bool + AdditionalNamespaces string + LabelSelector string + RateLimitPerSecond int + RateLimitBurst int + OldGCBehavior bool + UpdateStatus bool + SkipRecreate bool + LogInfoToStdout bool + PrivateKeyAnnotations string + PrivateKeyLabels string } func initKeyPrefix(keyPrefix string) (string, error) { @@ -109,18 +111,18 @@ func myNamespace() string { // Initialises the first key and starts the rotation job. returns an early trigger function. // A period of 0 deactivates automatic rotation, but manual rotation (e.g. triggered by SIGUSR1) // is still honoured. -func initKeyRenewal(ctx context.Context, registry *KeyRegistry, period, validFor time.Duration, cutoffTime time.Time, cn string) (func(), error) { +func initKeyRenewal(ctx context.Context, registry *KeyRegistry, period, validFor time.Duration, cutoffTime time.Time, cn string, privateKeyAnnotations string, privateKeyLabels string) (func(), error) { // Create a new key if it's the first key, // or if it's older than cutoff time. if len(registry.keys) == 0 || registry.mostRecentKey.orderingTime.Before(cutoffTime) { - if _, err := registry.generateKey(ctx, validFor, cn); err != nil { + if _, err := registry.generateKey(ctx, validFor, cn, privateKeyAnnotations, privateKeyLabels); err != nil { return nil, err } } // wrapper function to log error thrown by generateKey function keyGenFunc := func() { - if _, err := registry.generateKey(ctx, validFor, cn); err != nil { + if _, err := registry.generateKey(ctx, validFor, cn, privateKeyAnnotations, privateKeyLabels); err != nil { log.Errorf("Failed to generate new key : %v\n", err) } } @@ -181,7 +183,7 @@ func Main(f *Flags, version string) error { } } - trigger, err := initKeyRenewal(ctx, keyRegistry, f.KeyRenewPeriod, f.ValidFor, ct, f.MyCN) + trigger, err := initKeyRenewal(ctx, keyRegistry, f.KeyRenewPeriod, f.ValidFor, ct, f.MyCN, f.PrivateKeyAnnotations, f.PrivateKeyLabels) if err != nil { return err } diff --git a/pkg/controller/main_test.go b/pkg/controller/main_test.go index 718598ade3..881b9236ea 100644 --- a/pkg/controller/main_test.go +++ b/pkg/controller/main_test.go @@ -57,7 +57,7 @@ func TestInitKeyRegistry(t *testing.T) { // Add a key to the controller for second test validFor := time.Hour cn := "my-cn" - _, err = registry.generateKey(ctx, validFor, cn) + _, err = registry.generateKey(ctx, validFor, cn, "", "") if err != nil { t.Fatal(err) } @@ -91,7 +91,7 @@ func TestInitKeyRotation(t *testing.T) { validFor := time.Hour cn := "my-cn" - keyGenTrigger, err := initKeyRenewal(ctx, registry, 0, validFor, time.Time{}, cn) + keyGenTrigger, err := initKeyRenewal(ctx, registry, 0, validFor, time.Time{}, cn, "", "") if err != nil { t.Fatalf("initKeyRenewal() returned err: %v", err) } @@ -132,7 +132,7 @@ func TestInitKeyRotationTick(t *testing.T) { validFor := time.Hour cn := "my-cn" - _, err = initKeyRenewal(ctx, registry, 100*time.Millisecond, validFor, time.Time{}, cn) + _, err = initKeyRenewal(ctx, registry, 100*time.Millisecond, validFor, time.Time{}, cn, "", "") if err != nil { t.Fatalf("initKeyRenewal() returned err: %v", err) } @@ -173,7 +173,7 @@ func TestReuseKey(t *testing.T) { client := fake.NewSimpleClientset() client.PrependReactor("create", "secrets", generateNameReactor) - _, err = writeKey(ctx, client, key, []*x509.Certificate{cert}, "namespace", SealedSecretsKeyLabel, "prefix") + _, err = writeKey(ctx, client, key, []*x509.Certificate{cert}, "namespace", SealedSecretsKeyLabel, "prefix", "", "") if err != nil { t.Errorf("writeKey() failed with: %v", err) } @@ -187,7 +187,7 @@ func TestReuseKey(t *testing.T) { validFor := time.Hour cn := "my-cn" - _, err = initKeyRenewal(ctx, registry, 0, validFor, time.Time{}, cn) + _, err = initKeyRenewal(ctx, registry, 0, validFor, time.Time{}, cn, "", "") if err != nil { t.Fatalf("initKeyRenewal() returned err: %v", err) } @@ -222,7 +222,7 @@ func TestRenewStaleKey(t *testing.T) { client := fake.NewSimpleClientset() client.PrependReactor("create", "secrets", generateNameReactor) - _, err = writeKey(ctx, client, key, []*x509.Certificate{cert}, "namespace", SealedSecretsKeyLabel, "prefix") + _, err = writeKey(ctx, client, key, []*x509.Certificate{cert}, "namespace", SealedSecretsKeyLabel, "prefix", "", "") if err != nil { t.Errorf("writeKey() failed with: %v", err) } @@ -234,7 +234,7 @@ func TestRenewStaleKey(t *testing.T) { validFor := time.Hour cn := "my-cn" - _, err = initKeyRenewal(ctx, registry, period, validFor, time.Time{}, cn) + _, err = initKeyRenewal(ctx, registry, period, validFor, time.Time{}, cn, "", "") if err != nil { t.Fatalf("initKeyRenewal() returned err: %v", err) } @@ -278,7 +278,7 @@ func TestKeyCutoff(t *testing.T) { client := fake.NewSimpleClientset() client.PrependReactor("create", "secrets", generateNameReactor) - _, err = writeKey(ctx, client, key, []*x509.Certificate{cert}, "namespace", SealedSecretsKeyLabel, "prefix", + _, err = writeKey(ctx, client, key, []*x509.Certificate{cert}, "namespace", SealedSecretsKeyLabel, "prefix", "", "", writeKeyWithCreationTime(metav1.NewTime(time.Now().Add(-oldAge)))) if err != nil { t.Errorf("writeKey() failed with: %v", err) @@ -294,7 +294,7 @@ func TestKeyCutoff(t *testing.T) { // by setting cutoff to "now" we effectively force the creation of a new key. validFor := time.Hour cn := "my-cn" - _, err = initKeyRenewal(ctx, registry, period, validFor, time.Now(), cn) + _, err = initKeyRenewal(ctx, registry, period, validFor, time.Now(), cn, "", "") if err != nil { t.Fatalf("initKeyRenewal() returned err: %v", err) } @@ -358,7 +358,7 @@ func TestLegacySecret(t *testing.T) { validFor := time.Hour cn := "my-cn" - _, err = initKeyRenewal(ctx, registry, 0, validFor, time.Time{}, cn) + _, err = initKeyRenewal(ctx, registry, 0, validFor, time.Time{}, cn, "", "") if err != nil { t.Fatalf("initKeyRenewal() returned err: %v", err) } From bc076d48d9d474ccb1e7b5b0ff1915beed8b7a98 Mon Sep 17 00:00:00 2001 From: kvandenhoute Date: Tue, 27 Jun 2023 18:21:16 +0200 Subject: [PATCH 2/3] Update helm/sealed-secrets/README.md Co-authored-by: Alvaro Neira Ayuso Signed-off-by: kvandenhoute --- helm/sealed-secrets/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/sealed-secrets/README.md b/helm/sealed-secrets/README.md index 16f8773e21..d54131959a 100644 --- a/helm/sealed-secrets/README.md +++ b/helm/sealed-secrets/README.md @@ -97,7 +97,7 @@ The command removes all the Kubernetes components associated with the chart and | `rateLimitBurst` | Number of requests allowed to exceed the rate limit per second for verify endpoint | `""` | | `additionalNamespaces` | List of namespaces used to manage the Sealed Secrets | `[]` | `privateKeyAnnotations` | Map of annotations to be set on the sealing keypairs | `{}` -| `privateKeyLabels` | Map of labels to be set on the sealing keypairs | `{}` | +| `privateKeyLabels` | Map of labels to be set on the sealing keypairs | `{}` | | `logInfoStdout` | Specifies whether the Sealed Secrets controller will log info to stdout | `false` | | `command` | Override default container command | `[]` | | `args` | Override default container args | `[]` | From 8264cc7be2a8778e982c5022d92da59f3c7b810f Mon Sep 17 00:00:00 2001 From: kvandenhoute Date: Tue, 27 Jun 2023 18:21:25 +0200 Subject: [PATCH 3/3] Update helm/sealed-secrets/README.md Co-authored-by: Alvaro Neira Ayuso Signed-off-by: kvandenhoute --- helm/sealed-secrets/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/sealed-secrets/README.md b/helm/sealed-secrets/README.md index d54131959a..9dcd8d8a0c 100644 --- a/helm/sealed-secrets/README.md +++ b/helm/sealed-secrets/README.md @@ -96,7 +96,7 @@ The command removes all the Kubernetes components associated with the chart and | `rateLimit` | Number of allowed sustained request per second for verify endpoint | `""` | | `rateLimitBurst` | Number of requests allowed to exceed the rate limit per second for verify endpoint | `""` | | `additionalNamespaces` | List of namespaces used to manage the Sealed Secrets | `[]` -| `privateKeyAnnotations` | Map of annotations to be set on the sealing keypairs | `{}` +| `privateKeyAnnotations` | Map of annotations to be set on the sealing keypairs | `{}` | | `privateKeyLabels` | Map of labels to be set on the sealing keypairs | `{}` | | `logInfoStdout` | Specifies whether the Sealed Secrets controller will log info to stdout | `false` | | `command` | Override default container command | `[]` |