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
4 changes: 2 additions & 2 deletions .github/workflows/build-ghcr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.23 as builder
FROM golang:1.23 AS builder
ARG TARGETOS
ARG TARGETARCH

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
3 changes: 3 additions & 0 deletions api/v1/bitwardensecret_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions config/crd/bases/k8s.bitwarden.com_bitwardensecrets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions config/samples/k8s_v1_bitwardensecret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ spec:
authToken:
secretName: bw-auth-token
secretKey: token
# namespace: bitwarden
8 changes: 6 additions & 2 deletions internal/controller/bitwardensecret_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
Expand Down
37 changes: 37 additions & 0 deletions internal/controller/test/reconciler_success_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
})
5 changes: 5 additions & 0 deletions internal/controller/test/testutils/fixture.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading