diff --git a/.github/workflows/build-ghcr.yml b/.github/workflows/build-ghcr.yml index 5df979c..a971687 100644 --- a/.github/workflows/build-ghcr.yml +++ b/.github/workflows/build-ghcr.yml @@ -8,14 +8,14 @@ on: permissions: contents: read - packages: read + packages: write jobs: build-docker: 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/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 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/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/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 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,