A Kubernetes operator that synchronizes secrets from Bitwarden into Kubernetes Secret objects. Built with kopf and powered by the Bitwarden CLI, this operator enables GitOps-friendly secret management by allowing you to define secrets as Kubernetes Custom Resources while keeping sensitive data secure in Bitwarden.
- 🔐 Automatic Secret Synchronization - Sync secrets from Bitwarden to Kubernetes automatically
- 🔄 Continuous Updates - Configurable sync intervals keep secrets up-to-date
- 🎯 Multiple Secret Types - Support for generic secrets, registry credentials, and templated configurations
- 📝 Template Engine - Use Jinja2 templates with Bitwarden lookups for complex configurations
- 🏷️ Custom Labels & Annotations - Add metadata to generated secrets
- 🗑️ Garbage Collection - Automatic cleanup of managed secrets when CRDs are deleted
- 🔒 Self-hosted & SaaS - Works with both Bitwarden SaaS and self-hosted instances
- ⚡ Efficient CLI usage - Uses the Bitwarden CLI efficiently for session handling and syncs
Note: This operator uses the Bitwarden CLI. For local testing you can install it from the official project or use the bundled CLI inside the operator image.
Before installing the operator, you'll need:
- Bitwarden Account - Either SaaS or self-hosted instance
- Email Address - Your Bitwarden account email
- Master Password - Your Bitwarden master password
- Kubernetes Cluster - Version 1.16+ recommended
- Bitwarden CLI - The operator container includes the Bitwarden CLI, but for local testing you can install it from the official Bitwarden CLI releases or your package manager
You will need a ClientID and ClientSecret (where to get these) as well as your password.
You have two options for providing Bitwarden credentials:
Create a values.yaml file:
env:
- name: BW_EMAIL
value: "your-email@example.com"
- name: BW_PASSWORD
value: "YourSuperSecurePassword"
- name: BW_CLIENTID
value: "user.your-client-id"
- name: BW_CLIENTSECRET
value: "YoUrCliEntSecRet"
# Optional: for self-hosted Bitwarden
- name: BW_HOST
value: "https://bitwarden.your.tld.org"
# Optional: custom identity URL
- name: BW_IDENTITY_URL
value: "https://identity.your.tld.org"
xCreate a Kubernetes secret with your credentials:
kubectl create namespace bw-operator
kubectl create secret generic bitwarden-credentials \
--namespace bw-operator \
--from-literal=BW_EMAIL='your-email@example.com' \
--from-literal=BW_PASSWORD='YourSuperSecurePassword' \
--from-literal=BW_CLIENTID='user.your-client-id' \
--from-literal=BW_CLIENTSECRET='YoUrCliEntSecRet'
# Optional for self-hosted:
# --from-literal=BW_HOST='https://bitwarden.your.tld.org'Then reference it in values.yaml:
externalConfigSecret:
enabled: true
name: "bitwarden-credentials"# Add the Helm repository
helm repo add bitwarden-operator https://lerentis.github.io/bitwarden-crd-operator
helm repo update
# Create namespace
kubectl create namespace bw-operator
# Install the operator
helm upgrade --install \
--namespace bw-operator \
-f values.yaml \
bw-operator \
bitwarden-operator/bitwarden-crd-operator# Check if the operator is running
kubectl get pods -n bw-operator
# Check operator logs
kubectl logs -n bw-operator -l app.kubernetes.io/name=bitwarden-crd-operatorThe Bitwarden API distinguishes between simple login entries (username/password), custom fields, and attachments; the operator exposes these via the secretScope setting to control how items are mapped into Kubernetes Secrets.
- login — Use when you need the entry's username or password; specify
secretScope: login. - fields — Use for custom key/value fields stored on an item; specify
secretScope: fields. - attachment — Use for files attached to a Bitwarden item; specify
secretScope: attachment.
Login entry:
- element:
secretName: username
secretRef: my-username-key
secretScope: loginCustom field:
- element:
secretName: api_key
secretRef: my-api-key
secretScope: fieldsAttachment (file):
- element:
secretName: some-file.txt
secretRef: file-key
secretScope: attachmentThe BitwardenSecret custom resource allows you to map specific fields from a Bitwarden item to a Kubernetes Secret.
- Open your Bitwarden vault in a web browser
- Click on the item you want to use
- Look at the URL - the ID is the last part:
https://vault.bitwarden.com/#/vault?itemId=YOUR-ITEM-ID
apiVersion: "lerentis.uploadfilter24.eu/v1beta8"
kind: BitwardenSecret
metadata:
name: example-secret
namespace: default
spec:
content:
- element:
secretName: username # Field name in Bitwarden
secretRef: db-username # Key name in Kubernetes Secret
secretScope: login # Scope: login, fields, or attachment
- element:
secretName: password
secretRef: db-password
secretScope: login
- element:
secretName: api_key # Custom field in Bitwarden
secretRef: api-key
secretScope: fields # Use 'fields' for custom fields
id: "88781348-c81c-4367-9801-550360c21295" # Bitwarden item ID
name: "database-credentials" # Name of Kubernetes Secret
namespace: "production" # Target namespace
secretType: Opaque # Optional: Default is Opaque
labels: # Optional
app: myapp
environment: production
annotations: # Optional
custom.annotation: example-value| Field | Description | Required | Default |
|---|---|---|---|
id |
Bitwarden item ID | Yes | - |
name |
Name of the Kubernetes Secret to create | Yes | - |
namespace |
Target namespace for the Secret | Yes | - |
secretType |
Kubernetes Secret type | No | Opaque |
labels |
Labels to add to the Secret | No | {} |
annotations |
Annotations to add to the Secret | No | {} |
content[].element.secretName |
Field name in Bitwarden | Yes | - |
content[].element.secretRef |
Key name in Kubernetes Secret | Yes | - |
content[].element.secretScope |
Field scope: login, fields, or attachment |
Yes | - |
login: Use for standard login fields (username,password)fields: Use for custom fields you've added to the Bitwarden itemattachment: Use for file attachments (use filename assecretName)
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: database-credentials
namespace: production
labels:
app: myapp
environment: production
annotations:
managed: bitwarden-secret.lerentis.uploadfilter24.eu
managedObject: default/example-secret
custom.annotation: example-value
data:
db-username: <base64-encoded-value>
db-password: <base64-encoded-value>
api-key: <base64-encoded-value>The RegistryCredential creates Docker registry authentication secrets (pull secrets) from Bitwarden items. This is useful for authenticating with private container registries.
apiVersion: "lerentis.uploadfilter24.eu/v1beta8"
kind: RegistryCredential
metadata:
name: docker-hub-credentials
namespace: default
spec:
usernameRef: username # Field in Bitwarden (usually 'username')
passwordRef: password # Field in Bitwarden (usually 'password')
registry: "docker.io" # Registry URL
id: "3b249ec7-9ce7-440a-9558-f34f3ab10680"
name: "dockerhub-pull-secret" # Name of the Secret
namespace: "production" # Target namespace
labels:
app: myapp
annotations:
description: "Docker Hub credentials"| Registry | URL |
|---|---|
| Docker Hub | docker.io or https://index.docker.io/v1/ |
| GitHub Container Registry | ghcr.io |
| Google Container Registry | gcr.io |
| Amazon ECR | <account-id>.dkr.ecr.<region>.amazonaws.com |
| Azure Container Registry | <registry-name>.azurecr.io |
Reference the created secret in your Pod spec:
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
imagePullSecrets:
- name: dockerhub-pull-secret
containers:
- name: app
image: my-private-repo/my-app:latestapiVersion: v1
kind: Secret
type: kubernetes.io/dockerconfigjson
metadata:
name: dockerhub-pull-secret
namespace: production
annotations:
managed: registry-credential.lerentis.uploadfilter24.eu
managedObject: default/docker-hub-credentials
data:
.dockerconfigjson: <base64-encoded-docker-config>The BitwardenTemplate is the most flexible option, allowing you to create complex configuration files using Jinja2 templates with the bitwarden_lookup function to inject secrets directly from Bitwarden.
- Application configuration files with embedded secrets
- Multi-file configurations
- Complex YAML/JSON structures
- Environment-specific configurations
apiVersion: "lerentis.uploadfilter24.eu/v1beta8"
kind: BitwardenTemplate
metadata:
name: app-config
namespace: default
spec:
name: "application-config"
namespace: "production"
secretType: Opaque # Optional
labels:
app: myapp
annotations:
description: "Application configuration with secrets"
content:
- element:
filename: app-config.yaml
template: |
database:
host: db.example.com
port: 5432
username: {{ bitwarden_lookup("88781348-c81c-4367-9801-550360c21295", "login", "username") }}
password: {{ bitwarden_lookup("88781348-c81c-4367-9801-550360c21295", "login", "password") }}
api:
enabled: true
key: {{ bitwarden_lookup("466fc4b0-ffca-4444-8d88-b59d4de3d928", "fields", "api_key") }}
secret: {{ bitwarden_lookup("466fc4b0-ffca-4444-8d88-b59d4de3d928", "fields", "api_secret") }}
tls:
cert: {{ bitwarden_lookup("cert-item-id", "attachment", "server.crt") }}
key: {{ bitwarden_lookup("cert-item-id", "attachment", "server.key") }}
- element:
filename: additional-config.json
template: |
{
"service": {
"name": "my-service",
"token": "{{ bitwarden_lookup("token-item-id", "fields", "service_token") }}"
}
}Signature: bitwarden_lookup(item_id, scope, field)
| Parameter | Description | Valid Values |
|---|---|---|
item_id |
The Bitwarden item ID | UUID string |
scope |
The type of field to retrieve | login, fields, attachment |
field |
The specific field name | See table below |
Field values based on scope:
| Scope | Valid Field Values | Description |
|---|---|---|
login |
username, password |
Standard login credentials |
fields |
<custom-field-name> |
Custom fields you've added in Bitwarden |
attachment |
<filename> |
Name of attached file |
Since templates use Jinja2, you can use control structures:
template: |
{% if bitwarden_lookup("item-id", "fields", "enable_feature") == "true" %}
feature:
enabled: true
api_key: {{ bitwarden_lookup("item-id", "fields", "feature_api_key") }}
{% else %}
feature:
enabled: false
{% endif %}apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: application-config
namespace: production
annotations:
managed: bitwarden-template.lerentis.uploadfilter24.eu
managedObject: default/app-config
description: "Application configuration with secrets"
labels:
app: myapp
data:
app-config.yaml: <base64-encoded-rendered-template>
additional-config.json: <base64-encoded-rendered-template>The operator can be configured using environment variables, either directly in values.yaml or via an external secret.
| Variable | Description | Default | Required |
|---|---|---|---|
BW_EMAIL |
Bitwarden account email address | - | Yes |
BW_PASSWORD |
Bitwarden master password | - | Yes |
BW_HOST |
Bitwarden server URL (for self-hosted) | https://api.bitwarden.com/ |
No |
BW_CLIENTID |
OAuth client ID (optional for some deployments) | - | No |
BW_CLIENTSECRET |
OAuth client secret (optional) | - | No |
BW_SYNC_INTERVAL |
How often to sync with Bitwarden (seconds) | 900 (15 min) |
No |
BW_RELOGIN_INTERVAL |
How long to keep session unlocked / re-login interval (seconds) | 3600 (1 hour) |
No |
BW_FORCE_SYNC |
Force sync before every secret retrieval | false |
No |
DEBUG |
Enable debug logging | - | No |
The operator interacts with the Bitwarden CLI and may perform syncs according to the configured interval:
- Normal Mode (
BW_FORCE_SYNC=false): Periodic syncs based onBW_SYNC_INTERVALseconds (default: 15 minutes) - Force Sync Mode (
BW_FORCE_SYNC=true): Syncs before every secret retrieval - Automatic Background Sync: The Bitwarden CLI may also perform periodic syncs depending on its configuration
BW_FORCE_SYNC can lead to rate limiting if you have many secrets or frequent updates. Use with caution.
The operator relies on the Bitwarden CLI for session handling:
- The CLI may keep sessions unlocked for a configured interval (
BW_RELOGIN_INTERVAL) - The operator assumes the CLI handles authentication state; adjust
BW_RELOGIN_INTERVALor credentials as needed - No manual re-login should be required when the CLI session is valid
# values.yaml
env:
- name: BW_EMAIL
value: "vault-operator@example.com"
- name: BW_PASSWORD
value: "your-master-password"
# Optional: For self-hosted Bitwarden
- name: BW_HOST
value: "https://vault.example.com"
- name: BW_SYNC_INTERVAL
value: "600" # Sync every 10 minutes
- name: BW_RELOGIN_INTERVAL
value: "7200" # Keep session or re-login interval for 2 hours
- name: DEBUG
value: "true" # Enable debug loggingComplete working examples can be found in the repository:
example.yaml- BitwardenSecret examplesexample_template.yaml- BitwardenTemplate examplesexample_dockerlogin.yaml- RegistryCredential examples
kubectl logs -n bw-operator -l app.kubernetes.io/name=bitwarden-crd-operator -fSecret not created:
- Verify the Bitwarden item ID is correct
- Check operator logs for authentication errors
- Ensure the target namespace exists
Authentication failures:
- Verify
BW_EMAILandBW_PASSWORDare correct - Check if
BW_HOSTis needed (for self-hosted instances) - Ensure the Bitwarden CLI is installed and accessible (check logs for "Signin successful. Session exported" or "Already unlocked")
Secrets not updating:
- Check
BW_SYNC_INTERVALsetting - Verify the operator pod is running
- Look for sync errors in logs
When you delete a BitwardenSecret, RegistryCredential, or BitwardenTemplate custom resource, the operator automatically deletes the associated Kubernetes Secret if it's in the same namespace (owner references are used).
Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests.
Built with kopf - Kubernetes Operators Framework
