From 5a71717ffc53c49c34969dc423b53116dcab18e0 Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Sat, 5 Jul 2025 14:37:58 -0500 Subject: [PATCH 1/4] Implement a new authtoken namespace parameter to be able to fetch from outside the current bitwardensecret namespace --- .github/workflows/build-ghcr.yml | 2 +- .github/workflows/scan.yml | 1 + api/v1/bitwardensecret_types.go | 3 ++ .../k8s.bitwarden.com_bitwardensecrets.yaml | 5 +++ .../controller/bitwardensecret_controller.go | 8 +++- .../test/reconciler_success_test.go | 37 +++++++++++++++++++ internal/controller/test/testutils/fixture.go | 5 +++ 7 files changed, 58 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-ghcr.yml b/.github/workflows/build-ghcr.yml index 5df979c..b566850 100644 --- a/.github/workflows/build-ghcr.yml +++ b/.github/workflows/build-ghcr.yml @@ -15,7 +15,7 @@ jobs: name: Build Docker images runs-on: ubuntu-22.04 env: - _GHCR_REGISTRY: ghcr.io/bitwarden + _GHCR_REGISTRY: ghcr.io/${{github.repository_owner}} _PROJECT_NAME: sm-operator steps: diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 653bfae..97f308c 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -14,6 +14,7 @@ on: jobs: check-run: name: Check PR run + if: github.repository == 'bitwarden/sm-kubernetes' uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main sast: diff --git a/api/v1/bitwardensecret_types.go b/api/v1/bitwardensecret_types.go index 40fd0ec..3cdc1f7 100644 --- a/api/v1/bitwardensecret_types.go +++ b/api/v1/bitwardensecret_types.go @@ -60,6 +60,9 @@ type AuthToken struct { // The key of the Kubernetes secret where the authorization token is stored // +kubebuilder:Required SecretKey string `json:"secretKey"` + // The namespace where the authorization token secret is stored. If not specified, defaults to the same namespace as the BitwardenSecret + // +kubebuilder:Optional + Namespace string `json:"namespace,omitempty"` } type SecretMap struct { diff --git a/config/crd/bases/k8s.bitwarden.com_bitwardensecrets.yaml b/config/crd/bases/k8s.bitwarden.com_bitwardensecrets.yaml index bed8914..727db9d 100644 --- a/config/crd/bases/k8s.bitwarden.com_bitwardensecrets.yaml +++ b/config/crd/bases/k8s.bitwarden.com_bitwardensecrets.yaml @@ -43,6 +43,11 @@ spec: description: The secret key reference for the authorization token used to connect to Secrets Manager properties: + namespace: + description: The namespace where the authorization token secret + is stored. If not specified, defaults to the same namespace + as the BitwardenSecret + type: string secretKey: description: The key of the Kubernetes secret where the authorization token is stored diff --git a/internal/controller/bitwardensecret_controller.go b/internal/controller/bitwardensecret_controller.go index d287cdc..1a3a81e 100644 --- a/internal/controller/bitwardensecret_controller.go +++ b/internal/controller/bitwardensecret_controller.go @@ -102,9 +102,13 @@ func (r *BitwardenSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ //Need to retrieve the Bitwarden authorization token authK8sSecret := &corev1.Secret{} + authNamespace := req.NamespacedName.Namespace + if bwSecret.Spec.AuthToken.Namespace != "" { + authNamespace = bwSecret.Spec.AuthToken.Namespace + } namespacedAuthK8sSecret := types.NamespacedName{ Name: bwSecret.Spec.AuthToken.SecretName, - Namespace: req.NamespacedName.Namespace, + Namespace: authNamespace, } err = r.Get(ctx, namespacedAuthK8sSecret, authK8sSecret) @@ -119,7 +123,7 @@ func (r *BitwardenSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ data, ok := authK8sSecret.Data[bwSecret.Spec.AuthToken.SecretKey] if !ok || authK8sSecret.Data == nil { - err := fmt.Errorf("auth token secret key %s not found in %s/%s", bwSecret.Spec.AuthToken.SecretKey, req.NamespacedName.Namespace, bwSecret.Spec.AuthToken.SecretName) + err := fmt.Errorf("auth token secret key %s not found in %s/%s", bwSecret.Spec.AuthToken.SecretKey, authNamespace, bwSecret.Spec.AuthToken.SecretName) logErr := r.LogError(logger, ctx, bwSecret, err, "Invalid authorization token secret") return ctrl.Result{RequeueAfter: time.Duration(r.RefreshIntervalSeconds) * time.Second}, logErr } diff --git a/internal/controller/test/reconciler_success_test.go b/internal/controller/test/reconciler_success_test.go index e90abaa..0cddbbe 100644 --- a/internal/controller/test/reconciler_success_test.go +++ b/internal/controller/test/reconciler_success_test.go @@ -146,4 +146,41 @@ var _ = Describe("BitwardenSecret Reconciler - Success Tests", Ordered, func() { g.Expect(condition).To(BeNil()) }) }) + + It("should successfully sync with auth token from different namespace", func() { + fixture.SetupDefaultCtrlMocks(false, nil) + + // Create auth secret in a different namespace + authNamespace := fixture.CreateNamespace() + _, err := fixture.CreateDefaultAuthSecret(authNamespace) + Expect(err).NotTo(HaveOccurred()) + + // Create BitwardenSecret with cross-namespace auth token using fixture method + bwSecret, err := fixture.CreateBitwardenSecretWithAuthNamespace(testutils.BitwardenSecretName, namespace, fixture.OrgId, testutils.SynchronizedSecretName, testutils.AuthSecretName, testutils.AuthSecretKey, authNamespace, fixture.SecretMap, true) + Expect(err).NotTo(HaveOccurred()) + Expect(bwSecret).NotTo(BeNil()) + + req := reconcile.Request{NamespacedName: types.NamespacedName{Name: testutils.BitwardenSecretName, Namespace: namespace}} + + result, err := fixture.Reconciler.Reconcile(fixture.Ctx, req) + Expect(err).NotTo(HaveOccurred()) + Expect(result.RequeueAfter).To(Equal(time.Duration(fixture.Reconciler.RefreshIntervalSeconds) * time.Second)) + + Eventually(func(g Gomega) { + // Verify created secret in the BitwardenSecret's namespace + createdTargetSecret := &corev1.Secret{} + g.Expect(fixture.K8sClient.Get(fixture.Ctx, types.NamespacedName{Name: testutils.SynchronizedSecretName, Namespace: namespace}, createdTargetSecret)).Should(Succeed()) + g.Expect(createdTargetSecret.Labels[controller.LabelBwSecret]).To(Equal(string(bwSecret.UID))) + g.Expect(createdTargetSecret.Type).To(Equal(corev1.SecretTypeOpaque)) + g.Expect(len(createdTargetSecret.Data)).To(Equal(testutils.ExpectedNumOfSecrets)) + + // Verify SuccessfulSync condition and LastSuccessfulSyncTime + updatedBwSecret := &operatorsv1.BitwardenSecret{} + g.Expect(fixture.K8sClient.Get(fixture.Ctx, types.NamespacedName{Name: testutils.BitwardenSecretName, Namespace: namespace}, updatedBwSecret)).Should(Succeed()) + condition := apimeta.FindStatusCondition(updatedBwSecret.Status.Conditions, "SuccessfulSync") + g.Expect(condition).NotTo(BeNil()) + g.Expect(condition.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(updatedBwSecret.Status.LastSuccessfulSyncTime.Time).NotTo(BeZero()) + }).Should(Succeed()) + }) }) diff --git a/internal/controller/test/testutils/fixture.go b/internal/controller/test/testutils/fixture.go index a1cf5ab..22e4601 100644 --- a/internal/controller/test/testutils/fixture.go +++ b/internal/controller/test/testutils/fixture.go @@ -219,6 +219,10 @@ func (f *TestFixture) CreateDefaultBitwardenSecret(namespace string, secretMap [ } func (f *TestFixture) CreateBitwardenSecret(name, namespace, orgID, secretName, authSecretName, authSecretKey string, secretMap []operatorsv1.SecretMap, onlyMappedSecrets bool) (*operatorsv1.BitwardenSecret, error) { + return f.CreateBitwardenSecretWithAuthNamespace(name, namespace, orgID, secretName, authSecretName, authSecretKey, "", secretMap, onlyMappedSecrets) +} + +func (f *TestFixture) CreateBitwardenSecretWithAuthNamespace(name, namespace, orgID, secretName, authSecretName, authSecretKey, authNamespace string, secretMap []operatorsv1.SecretMap, onlyMappedSecrets bool) (*operatorsv1.BitwardenSecret, error) { bwSecret := &operatorsv1.BitwardenSecret{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -228,6 +232,7 @@ func (f *TestFixture) CreateBitwardenSecret(name, namespace, orgID, secretName, AuthToken: operatorsv1.AuthToken{ SecretName: authSecretName, SecretKey: authSecretKey, + Namespace: authNamespace, }, SecretName: secretName, OrganizationId: orgID, From 8eccf1eb2c5ffd90b068afbbf2c23ff4bd8c7b69 Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Sat, 5 Jul 2025 17:14:35 -0500 Subject: [PATCH 2/4] buildfixes --- .github/workflows/build-ghcr.yml | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-ghcr.yml b/.github/workflows/build-ghcr.yml index b566850..a971687 100644 --- a/.github/workflows/build-ghcr.yml +++ b/.github/workflows/build-ghcr.yml @@ -8,7 +8,7 @@ on: permissions: contents: read - packages: read + packages: write jobs: build-docker: diff --git a/Dockerfile b/Dockerfile index de55589..1418fd8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.23 as builder +FROM golang:1.23 AS builder ARG TARGETOS ARG TARGETARCH From be6daeecece3025bfe3f8d0e0c04853a174f7108 Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Sat, 5 Jul 2025 20:31:19 -0500 Subject: [PATCH 3/4] add documentation --- README.md | 2 +- config/samples/k8s_v1_bitwardensecret.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 731f661..f38a85c 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ Our operator is designed to look for the creation of a custom resource called a - **metadata.name**: The name of the BitwardenSecret object you are deploying - **spec.organizationId**: The Bitwarden organization ID you are pulling Secrets Manager data from - **spec.secretName**: The name of the Kubernetes secret that will be created and injected with Secrets Manager data. -- **spec.authToken**: The name of a secret inside of the Kubernetes namespace that the BitwardenSecrets object is being deployed into that contains the Secrets Manager machine account authorization token being used to access secrets. +- **spec.authToken**: Configuration for the Secrets Manager machine account authorization token. By default, looks for the secret in the same namespace as the BitwardenSecret, but can optionally specify a different namespace. Secrets Manager does not guarantee unique secret names across projects, so by default secrets will be created with the Secrets Manager secret UUID used as the key. To make your generated secret easier to use, you can create a map of Bitwarden Secret IDs to Kubernetes secret keys. The generated secret will replace the Bitwarden Secret IDs with the mapped friendly name you provide. Below are the map settings available: diff --git a/config/samples/k8s_v1_bitwardensecret.yaml b/config/samples/k8s_v1_bitwardensecret.yaml index cb470ce..e067de7 100644 --- a/config/samples/k8s_v1_bitwardensecret.yaml +++ b/config/samples/k8s_v1_bitwardensecret.yaml @@ -20,3 +20,4 @@ spec: authToken: secretName: bw-auth-token secretKey: token + # namespace: bitwarden From a49ed6686efe1f41e69f260dff250bdc95d468f0 Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Fri, 15 Aug 2025 08:33:42 -0500 Subject: [PATCH 4/4] remove repo check from scan workflow --- .github/workflows/scan.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 556b0d2..7e59f23 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -14,7 +14,6 @@ on: jobs: check-run: name: Check PR run - if: github.repository == 'bitwarden/sm-kubernetes' uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main permissions: contents: read