Millet Porridge

English version of https://corvo.myseu.cn

0%

Managing Secrets in a K8s Cluster with Bitwarden

I’ve always used a self-hosted Vaultwarden to manage passwords. Recently I suddenly realized the passwords in my K3s cluster were all written directly in the Git repo — neither secure nor elegant. So I decided to migrate these passwords to Bitwarden with automatic syncing to the cluster.

The whole process was actually smooth; I record the operation flow here for reference.

Why Do This

Although I use ArgoCD to publish services automatically, passwords and other sensitive information were written directly in the Git repo. This is not only insecure but also troublesome to maintain.

1755960391002.png

I wanted a controller that automatically syncs Secrets — moving passwords from Bitwarden into the K8s cluster automatically, improving security and management efficiency. Conveniently, the ArgoCD docs recommend using an independent Secrets management system, such as external-secrets.

Architecture Design

The plan is simple: deploy Bitwarden’s forwarding service and the external-secrets controller in the cluster; Secrets are automatically created into the corresponding namespaces.

For the detailed flow see: argocd bitwarden

1755962102555.png

Implementation Steps

I use ArgoCD for IaC management; below I illustrate with the Application approach.

Installing external-secrets

Install external-secrets with Helm, enabling ClusterSecretStore CRD support.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: external-secrets
namespace: argocd
spec:
project: default
source:
repoURL: 'https://external-secrets.github.io/external-secrets/'
chart: external-secrets
targetRevision: 0.19.2
helm:
parameters:
- name: bitwarden-sdk-server.enabled
value: "false"
- name: "crds.createClusterSecretStore"
value: "true"
destination:
server: 'https://kubernetes.default.svc'
namespace: external-secrets
syncPolicy:
automated:
selfHeal: true
prune: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true

Deploying the Bitwarden Service

Deploy the Bitwarden CLI service, ensuring passwords can be fetched normally. You can run bw list items in the container to verify.

1755963071234.png

1
2
3
4
5
6
7
8
9
10
11
12
kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
name: bitwarden-cli
namespace: external-secrets
type: Opaque
data:
BW_HOST: $(echo -n "https://xxxxx" | base64 | tr -d '\n')
BW_USERNAME: $(echo -n "" | base64 | tr -d '\n')
BW_PASSWORD: $(echo -n "" | base64 | tr -d '\n')
EOF
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Reference docs:
# https://bitwarden.corvo.fun/#/vault?organizationId=unassigned&search=ext&itemId=502e2c9a-fefc-4d6d-93a9-0f66d7d4191a
# https://external-secrets.io/latest/examples/bitwarden/#deploy-bitwarden-credentials

---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bitwarden-cli
spec:
template:
spec:
containers:
- name: bitwarden-cli
image: ghcr.io/charlesthomas/bitwarden-cli:2025.6.1
# ...
---
apiVersion: v1
kind: Service
metadata:
name: bitwarden-cli
spec:
# ...
---
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: external-secret-2-bw-cli
spec:
# ...

Configuring external-secrets’ Password Retrieval

Tell external-secrets how to fetch passwords from Bitwarden.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
---
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: bitwarden-login
spec:
provider:
webhook:
url: "http://bitwarden-cli:8087/object/item/{{ .remoteRef.key }}"
headers:
Content-Type: application/json
result:
jsonPath: "$.data.login.{{ .remoteRef.property }}"
---
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: bitwarden-fields
spec:
provider:
webhook:
url: "http://bitwarden-cli:8087/object/item/{{ .remoteRef.key }}"
result:
jsonPath: "$.data.fields[[email protected]==\"{{ .remoteRef.property }}\"].value"
---
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: bitwarden-notes
spec:
provider:
webhook:
url: "http://bitwarden-cli:8087/object/item/{{ .remoteRef.key }}"
result:
jsonPath: "$.data.notes"
---
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: bitwarden-attachments
spec:
provider:
webhook:
url: "http://bitwarden-cli:8087/object/attachment/{{ .remoteRef.property }}?itemid={{ .remoteRef.key }}"
result: {}

Auto-Syncing Passwords to the Cluster

If you’ve already saved passwords in Bitwarden, just create an ExternalSecret and the controller will sync it to the K8s cluster automatically.

1755962739832.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: xx
spec:
refreshInterval: 1h
secretStoreRef:
name: bitwarden-secretsmanager
kind: SecretStore
data:
- secretKey: AAA
sourceRef:
storeRef:
name: bitwarden-notes
kind: ClusterSecretStore
remoteRef:
key: AAA

A K8s Secret is finally generated automatically; the data has synced to the cluster and can be mounted and used directly.

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
type: Opaque
kind: Secret
metadata:
name: xx
namespace: default
ownerReferences:
- apiVersion: external-secrets.io/v1
kind: ExternalSecret
name: xx
data:
AAA: QkJCQ0NDRERE

Summary and Suggestions

I used to manage passwords with AWS SSM but never really adopted it due to cost. Now managing K8s passwords with Bitwarden is both secure and convenient. If your production environment has high security requirements, I still suggest using professional password management tools.

  1. Bitwarden is my self-hosted password management tool, already with automatic backup and disaster recovery, so managing cluster passwords with it is reassuring.
  2. Bitwarden supports permission control. If using it to manage K8s secrets, I suggest creating a separate organization for storage and using a dedicated user for reading. This way, even if the cluster is compromised, other passwords stay safe.