From 761d40699af3c1a8516e9b387112b234e99b0751 Mon Sep 17 00:00:00 2001 From: Rohmilchkaese Date: Tue, 17 Feb 2026 22:11:25 +0100 Subject: [PATCH 1/2] feat: add official Helm chart Add a production-ready Helm chart under helm/vaultwarden/ with: - SQLite default, PostgreSQL/MySQL selectable - Database URL from secret or composed from parts (Zalano/CNPG operator support) - All sensitive values via secretKeyRef with existingSecret support - Hardened security: non-root (UID 1000), readOnlyRootFilesystem, drop ALL capabilities, seccomp RuntimeDefault, automountServiceAccountToken disabled - Ingress with ingressClassName, annotations, labels, TLS - PVC with helm.sh/resource-policy: keep, storageClassName 3-way handling - Service annotations/labels, NodePort, LoadBalancer support - Admin panel, SMTP, SSO/OIDC, push notifications, Yubico OTP - Template validation (fail on misconfiguration) - extraEnv, extraVolumes, extraVolumeMounts, initContainers - Comprehensive README with configuration reference Tested with helm lint --strict, helm template (6 scenarios), and deployed on a real k8s cluster (probes pass, logs clean). --- helm/vaultwarden/.helmignore | 12 + helm/vaultwarden/Chart.yaml | 18 + helm/vaultwarden/README.md | 401 ++++++++++++++++++ helm/vaultwarden/templates/NOTES.txt | 52 +++ helm/vaultwarden/templates/_helpers.tpl | 133 ++++++ helm/vaultwarden/templates/configmap.yaml | 40 ++ helm/vaultwarden/templates/deployment.yaml | 215 ++++++++++ helm/vaultwarden/templates/ingress.yaml | 45 ++ helm/vaultwarden/templates/pvc.yaml | 33 ++ helm/vaultwarden/templates/secret.yaml | 81 ++++ helm/vaultwarden/templates/service.yaml | 32 ++ .../vaultwarden/templates/serviceaccount.yaml | 14 + .../templates/tests/test-connection.yaml | 16 + helm/vaultwarden/values.yaml | 347 +++++++++++++++ 14 files changed, 1439 insertions(+) create mode 100644 helm/vaultwarden/.helmignore create mode 100644 helm/vaultwarden/Chart.yaml create mode 100644 helm/vaultwarden/README.md create mode 100644 helm/vaultwarden/templates/NOTES.txt create mode 100644 helm/vaultwarden/templates/_helpers.tpl create mode 100644 helm/vaultwarden/templates/configmap.yaml create mode 100644 helm/vaultwarden/templates/deployment.yaml create mode 100644 helm/vaultwarden/templates/ingress.yaml create mode 100644 helm/vaultwarden/templates/pvc.yaml create mode 100644 helm/vaultwarden/templates/secret.yaml create mode 100644 helm/vaultwarden/templates/service.yaml create mode 100644 helm/vaultwarden/templates/serviceaccount.yaml create mode 100644 helm/vaultwarden/templates/tests/test-connection.yaml create mode 100644 helm/vaultwarden/values.yaml diff --git a/helm/vaultwarden/.helmignore b/helm/vaultwarden/.helmignore new file mode 100644 index 00000000..24ae3363 --- /dev/null +++ b/helm/vaultwarden/.helmignore @@ -0,0 +1,12 @@ +# Patterns to ignore when building packages. +.DS_Store +*.swp +*.bak +*.tmp +*~ +.git +.gitignore +.project +.idea +*.tmproj +.vscode diff --git a/helm/vaultwarden/Chart.yaml b/helm/vaultwarden/Chart.yaml new file mode 100644 index 00000000..8b34ee7f --- /dev/null +++ b/helm/vaultwarden/Chart.yaml @@ -0,0 +1,18 @@ +apiVersion: v2 +name: vaultwarden +description: Vaultwarden - unofficial Bitwarden-compatible server written in Rust +type: application +version: 0.1.0 +appVersion: "1.35.3" +home: https://github.com/dani-garcia/vaultwarden +icon: https://raw.githubusercontent.com/dani-garcia/vaultwarden/main/resources/vaultwarden-icon.svg +sources: + - https://github.com/dani-garcia/vaultwarden +keywords: + - vaultwarden + - bitwarden + - password-manager + - secrets +maintainers: + - name: dani-garcia + url: https://github.com/dani-garcia diff --git a/helm/vaultwarden/README.md b/helm/vaultwarden/README.md new file mode 100644 index 00000000..30da661a --- /dev/null +++ b/helm/vaultwarden/README.md @@ -0,0 +1,401 @@ +# Vaultwarden Helm Chart + +Official Helm chart for [Vaultwarden](https://github.com/dani-garcia/vaultwarden) — an unofficial Bitwarden-compatible server written in Rust. + +## Quick Start + +```bash +helm install vaultwarden ./helm/vaultwarden \ + --set vaultwarden.domain=https://vault.example.com +``` + +This deploys vaultwarden with **SQLite** (the default). Data is persisted in a 5Gi PVC. + +> **For production deployments, we recommend PostgreSQL.** See [Production Setup with PostgreSQL](#production-setup-with-postgresql) below. + +## Production Setup with PostgreSQL + +```yaml +# values-production.yaml +vaultwarden: + domain: https://vault.example.com + signupsAllowed: false + admin: + enabled: true + existingSecret: vaultwarden-admin + existingSecretKey: admin-token + +database: + type: postgresql + existingSecret: vaultwarden-db-credentials + existingSecretKey: database-url + +ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-production + cert-manager.io/private-key-algorithm: ECDSA + cert-manager.io/private-key-size: "384" + cert-manager.io/private-key-rotation-policy: Always + hosts: + - host: vault.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: vault-tls + hosts: + - vault.example.com + +persistence: + storageClassName: longhorn # or your preferred storage class + size: 10Gi + +resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 1Gi +``` + +```bash +helm install vaultwarden ./helm/vaultwarden -f values-production.yaml +``` + +## Configuration + +### Image + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `image.repository` | Container image repository | `vaultwarden/server` | +| `image.tag` | Image tag (defaults to `appVersion`) | `""` | +| `image.pullPolicy` | Image pull policy | `IfNotPresent` | +| `replicaCount` | Number of replicas (keep at 1 for SQLite) | `1` | + +### Vaultwarden + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `vaultwarden.domain` | **(required)** Public URL of your instance | `""` | +| `vaultwarden.signupsAllowed` | Allow new user registrations | `false` | +| `vaultwarden.rocketPort` | HTTP server port | `8080` | +| `vaultwarden.websocket.enabled` | Enable websocket notifications | `true` | +| `vaultwarden.logging.level` | Log level (trace/debug/info/warn/error/off) | `info` | +| `vaultwarden.icons.service` | Icon service (internal/bitwarden/duckduckgo/google) | `internal` | + +### Admin Panel + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `vaultwarden.admin.enabled` | Enable the admin panel at `/admin` | `false` | +| `vaultwarden.admin.token` | Admin token (argon2 hash recommended) | `""` | +| `vaultwarden.admin.existingSecret` | Existing secret name for admin token | `""` | +| `vaultwarden.admin.existingSecretKey` | Key in existing secret | `admin-token` | + +### Database + +The chart supports two ways to configure the database connection for PostgreSQL/MySQL: + +**Option 1: Full connection URL** — provide a complete `DATABASE_URL` via a secret: + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `database.type` | Database backend: `sqlite`, `postgresql`, or `mysql` | `sqlite` | +| `database.url` | Full connection URL (inline, not recommended) | `""` | +| `database.existingSecret` | Secret containing the full database URL | `""` | +| `database.existingSecretKey` | Key in existing secret | `database-url` | + +```yaml +database: + type: postgresql + existingSecret: my-db-url-secret + existingSecretKey: database-url +``` + +**Option 2: Compose from parts** (recommended for Postgres operators) — the chart reads username and password from a credentials secret and assembles the `DATABASE_URL` automatically. This is ideal for Zalando Postgres Operator, CloudNativePG, or any operator that creates per-user credential secrets: + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `database.host` | Database hostname (triggers compose mode) | `""` | +| `database.port` | Database port | `5432` | +| `database.dbName` | Database name | `vaultwarden` | +| `database.credentialsSecret` | Secret with `username` and `password` keys | `""` | +| `database.credentialsSecretUsernameKey` | Key for username | `username` | +| `database.credentialsSecretPasswordKey` | Key for password | `password` | + +```yaml +# Example: Zalando Postgres Operator +database: + type: postgresql + host: vaultwarden-db.postgres-cluster + port: 5432 + dbName: vaultwarden + credentialsSecret: vaultwarden.user.vaultwarden-db.credentials.postgresql.acid.zalan.do +``` + +This renders as: + +```yaml +env: + - name: _DB_USER + valueFrom: + secretKeyRef: + name: vaultwarden.user.vaultwarden-db.credentials.postgresql.acid.zalan.do + key: username + - name: _DB_PASSWORD + valueFrom: + secretKeyRef: + name: vaultwarden.user.vaultwarden-db.credentials.postgresql.acid.zalan.do + key: password + - name: DATABASE_URL + value: postgresql://$(_DB_USER):$(_DB_PASSWORD)@vaultwarden-db.postgres-cluster:5432/vaultwarden +``` + +**Common settings:** + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `database.maxConnections` | Max database connections | `10` | +| `database.wal` | Enable WAL mode (SQLite only) | `true` | + +### SMTP (Email) + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `vaultwarden.smtp.host` | SMTP server hostname | `""` | +| `vaultwarden.smtp.from` | Sender email address | `""` | +| `vaultwarden.smtp.port` | SMTP port | `587` | +| `vaultwarden.smtp.security` | Security mode (starttls/force_tls/off) | `starttls` | +| `vaultwarden.smtp.username` | SMTP username | `""` | +| `vaultwarden.smtp.password` | SMTP password | `""` | +| `vaultwarden.smtp.existingSecret` | Existing secret for SMTP credentials | `""` | +| `vaultwarden.smtp.existingSecretUsernameKey` | Key in existing secret for username | `smtp-username` | +| `vaultwarden.smtp.existingSecretPasswordKey` | Key in existing secret for password | `smtp-password` | + +### SSO (OpenID Connect) + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `vaultwarden.sso.enabled` | Enable SSO authentication | `false` | +| `vaultwarden.sso.only` | Require SSO (disable password login) | `false` | +| `vaultwarden.sso.authority` | OIDC authority URL | `""` | +| `vaultwarden.sso.clientId` | OIDC client ID | `""` | +| `vaultwarden.sso.clientSecret` | OIDC client secret | `""` | +| `vaultwarden.sso.existingSecret` | Existing secret for SSO credentials | `""` | +| `vaultwarden.sso.existingSecretClientIdKey` | Key in existing secret for client ID | `sso-client-id` | +| `vaultwarden.sso.existingSecretClientSecretKey` | Key in existing secret for client secret | `sso-client-secret` | + +### Push Notifications + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `vaultwarden.push.enabled` | Enable push notifications | `false` | +| `vaultwarden.push.installationId` | Installation ID from bitwarden.com/host | `""` | +| `vaultwarden.push.installationKey` | Installation key from bitwarden.com/host | `""` | +| `vaultwarden.push.existingSecret` | Existing secret for push credentials | `""` | +| `vaultwarden.push.relayUri` | Push relay URI | `""` | +| `vaultwarden.push.identityUri` | Push identity URI | `""` | + +### Yubico OTP + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `vaultwarden.yubico.enabled` | Enable Yubico OTP | `false` | +| `vaultwarden.yubico.clientId` | Yubico client ID | `""` | +| `vaultwarden.yubico.secretKey` | Yubico secret key | `""` | +| `vaultwarden.yubico.existingSecret` | Existing secret for Yubico credentials | `""` | + +### Service + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `service.type` | Service type (`ClusterIP`, `NodePort`, `LoadBalancer`) | `ClusterIP` | +| `service.port` | Service port | `8080` | +| `service.nodePort` | Node port (when type is `NodePort`) | `""` | +| `service.loadBalancerIP` | Load balancer IP (when type is `LoadBalancer`) | `""` | +| `service.externalTrafficPolicy` | External traffic policy (`Local` or `Cluster`) | `""` | +| `service.annotations` | Service annotations (e.g. for external-dns) | `{}` | +| `service.labels` | Additional service labels | `{}` | + +### Ingress + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `ingress.enabled` | Enable ingress | `false` | +| `ingress.className` | Ingress class name (e.g. `nginx`, `traefik`, `haproxy`) | `""` | +| `ingress.annotations` | Ingress annotations (e.g. cert-manager, rate-limiting) | `{}` | +| `ingress.labels` | Additional ingress labels | `{}` | +| `ingress.hosts` | Ingress host rules | see `values.yaml` | +| `ingress.tls` | Ingress TLS configuration | `[]` | + +Example with full ingress configuration: + +```yaml +ingress: + enabled: true + className: traefik + annotations: + cert-manager.io/cluster-issuer: letsencrypt-production + cert-manager.io/private-key-algorithm: ECDSA + cert-manager.io/private-key-size: "384" + cert-manager.io/private-key-rotation-policy: Always + hosts: + - host: vault.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: vault-tls + hosts: + - vault.example.com +``` + +### Persistence + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `persistence.enabled` | Enable persistent storage | `true` | +| `persistence.storageClassName` | Storage class name (see below) | `nil` | +| `persistence.accessModes` | PVC access modes | `[ReadWriteOnce]` | +| `persistence.size` | Storage size | `5Gi` | +| `persistence.existingClaim` | Use an existing PVC | `""` | +| `persistence.annotations` | Additional PVC annotations | `{}` | +| `persistence.labels` | Additional PVC labels | `{}` | + +**Storage class behavior:** + +| Value | Behavior | +|-------|----------| +| `nil` (unset) | Uses the cluster default storage class | +| `"-"` | Disables dynamic provisioning (`storageClassName: ""`) | +| `"longhorn"` | Uses the specified storage class | + +**High availability (multiple replicas):** Running `replicaCount > 1` requires PostgreSQL (SQLite does not support concurrent access) and a storage class that supports `ReadWriteMany` (RWX) access mode, such as NFS, CephFS, or a cloud-native RWX provider (e.g. AWS EFS, Azure Files, GCP Filestore). Update your persistence accordingly: + +```yaml +replicaCount: 2 + +database: + type: postgresql + host: my-cluster.postgres + credentialsSecret: my-pg-credentials + +persistence: + storageClassName: efs-sc # or any RWX-capable storage class + accessModes: + - ReadWriteMany +``` + +### Security Context + +The chart runs vaultwarden as a non-root user (UID 1000) by default with a read-only root filesystem. The `ROCKET_PORT` is set to `8080` to avoid requiring privileged ports. + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `podSecurityContext.runAsUser` | Pod user ID | `1000` | +| `podSecurityContext.runAsGroup` | Pod group ID | `1000` | +| `podSecurityContext.runAsNonRoot` | Enforce non-root | `true` | +| `podSecurityContext.fsGroup` | Pod filesystem group | `1000` | +| `podSecurityContext.seccompProfile.type` | Seccomp profile | `RuntimeDefault` | +| `securityContext.readOnlyRootFilesystem` | Read-only root FS | `true` | +| `securityContext.allowPrivilegeEscalation` | Prevent privilege escalation | `false` | +| `securityContext.capabilities.drop` | Dropped capabilities | `["ALL"]` | + +### Scheduling + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `nodeSelector` | Node selector constraints | `{}` | +| `tolerations` | Pod tolerations | `[]` | +| `affinity` | Affinity rules | `{}` | +| `topologySpreadConstraints` | Topology spread constraints | `[]` | +| `priorityClassName` | Priority class for pod scheduling | `""` | + +### Other + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `serviceAccount.create` | Create a service account | `true` | +| `serviceAccount.annotations` | Service account annotations | `{}` | +| `serviceAccount.automountServiceAccountToken` | Automount SA token | `false` | +| `resources` | CPU/memory resources | see `values.yaml` | +| `revisionHistoryLimit` | Deployment revision history limit | `3` | +| `terminationGracePeriodSeconds` | Termination grace period | `30` | +| `startupProbe` | Startup probe config (for slow starts) | `{}` | +| `initContainers` | Init containers | `[]` | +| `extraEnv` | Additional environment variables | `[]` | +| `extraVolumes` | Additional volumes | `[]` | +| `extraVolumeMounts` | Additional volume mounts | `[]` | +| `podAnnotations` | Pod annotations | `{}` | +| `podLabels` | Additional pod labels | `{}` | + +## Using Existing Secrets + +For production deployments, use `existingSecret` references instead of putting credentials in `values.yaml`. All sensitive values support `existingSecret`: + +```bash +# Create secrets before installing the chart +kubectl create secret generic vaultwarden-admin \ + --from-literal=admin-token='$argon2id$...' + +kubectl create secret generic vaultwarden-db \ + --from-literal=database-url='postgresql://user:pass@host:5432/vaultwarden' + +kubectl create secret generic vaultwarden-smtp \ + --from-literal=smtp-username='user@example.com' \ + --from-literal=smtp-password='password' + +kubectl create secret generic vaultwarden-sso \ + --from-literal=sso-client-id='vaultwarden' \ + --from-literal=sso-client-secret='secret' + +kubectl create secret generic vaultwarden-push \ + --from-literal=push-installation-id='...' \ + --from-literal=push-installation-key='...' +``` + +Then reference them in your values: + +```yaml +vaultwarden: + admin: + enabled: true + existingSecret: vaultwarden-admin + smtp: + host: smtp.example.com + from: vault@example.com + existingSecret: vaultwarden-smtp + sso: + enabled: true + authority: https://auth.example.com/realms/main + existingSecret: vaultwarden-sso + push: + enabled: true + existingSecret: vaultwarden-push +database: + type: postgresql + existingSecret: vaultwarden-db +``` + +## Mounting Custom CA Certificates + +To trust custom CA certificates (e.g. for LDAP or SSO with self-signed certs): + +```yaml +extraVolumes: + - name: custom-certs + secret: + secretName: ca-bundle + +extraVolumeMounts: + - name: custom-certs + mountPath: /etc/ssl/certs/custom + readOnly: true + +extraEnv: + - name: SSL_CERT_DIR + value: /etc/ssl/certs:/etc/ssl/certs/custom +``` diff --git a/helm/vaultwarden/templates/NOTES.txt b/helm/vaultwarden/templates/NOTES.txt new file mode 100644 index 00000000..ce7a7498 --- /dev/null +++ b/helm/vaultwarden/templates/NOTES.txt @@ -0,0 +1,52 @@ +Vaultwarden has been deployed successfully! + +{{- if .Values.ingress.enabled }} + +Your vaultwarden instance is available at: +{{- range .Values.ingress.hosts }} + https://{{ .host }} +{{- end }} +{{- else }} + +To access vaultwarden, run: + kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "vaultwarden.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }} + +Then open http://localhost:{{ .Values.service.port }} in your browser. +{{- end }} + +{{- if not .Values.vaultwarden.domain }} + +WARNING: vaultwarden.domain is not set. You must set this to the URL +where vaultwarden will be accessible (e.g. https://vault.example.com). +{{- end }} + +{{- if .Values.vaultwarden.admin.enabled }} + +Admin panel is enabled at /admin +{{- if not .Values.vaultwarden.admin.existingSecret }} + Make sure to use an existingSecret for the admin token in production. +{{- end }} +{{- end }} + +{{- if eq .Values.database.type "sqlite" }} + +NOTE: You are using SQLite (default). For production deployments with +multiple users, consider switching to PostgreSQL: + database.type=postgresql + database.host= + database.credentialsSecret= +{{- end }} + +{{- if and (gt (int .Values.replicaCount) 1) (eq .Values.database.type "sqlite") }} + +WARNING: replicaCount > 1 is not supported with SQLite. Use PostgreSQL +for multi-replica deployments. +{{- end }} + +{{- if and (gt (int .Values.replicaCount) 1) .Values.persistence.enabled }} + +NOTE: Running multiple replicas with persistence requires a storage class +that supports ReadWriteMany (RWX) access mode, such as NFS, CephFS, or +a cloud-native RWX provider. Ensure persistence.accessModes includes +ReadWriteMany. +{{- end }} diff --git a/helm/vaultwarden/templates/_helpers.tpl b/helm/vaultwarden/templates/_helpers.tpl new file mode 100644 index 00000000..57cfbe62 --- /dev/null +++ b/helm/vaultwarden/templates/_helpers.tpl @@ -0,0 +1,133 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "vaultwarden.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "vaultwarden.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "vaultwarden.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels. +*/}} +{{- define "vaultwarden.labels" -}} +helm.sh/chart: {{ include "vaultwarden.chart" . }} +{{ include "vaultwarden.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels. +*/}} +{{- define "vaultwarden.selectorLabels" -}} +app.kubernetes.io/name: {{ include "vaultwarden.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use. +*/}} +{{- define "vaultwarden.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "vaultwarden.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Return the appropriate image tag. +*/}} +{{- define "vaultwarden.imageTag" -}} +{{- default .Chart.AppVersion .Values.image.tag }} +{{- end }} + +{{/* +Return the secret name for admin token. +*/}} +{{- define "vaultwarden.adminSecretName" -}} +{{- if .Values.vaultwarden.admin.existingSecret }} +{{- .Values.vaultwarden.admin.existingSecret }} +{{- else }} +{{- printf "%s-admin" (include "vaultwarden.fullname" .) }} +{{- end }} +{{- end }} + +{{/* +Return the secret name for SMTP credentials. +*/}} +{{- define "vaultwarden.smtpSecretName" -}} +{{- if .Values.vaultwarden.smtp.existingSecret }} +{{- .Values.vaultwarden.smtp.existingSecret }} +{{- else }} +{{- printf "%s-smtp" (include "vaultwarden.fullname" .) }} +{{- end }} +{{- end }} + +{{/* +Return the secret name for SSO credentials. +*/}} +{{- define "vaultwarden.ssoSecretName" -}} +{{- if .Values.vaultwarden.sso.existingSecret }} +{{- .Values.vaultwarden.sso.existingSecret }} +{{- else }} +{{- printf "%s-sso" (include "vaultwarden.fullname" .) }} +{{- end }} +{{- end }} + +{{/* +Return the secret name for push notification credentials. +*/}} +{{- define "vaultwarden.pushSecretName" -}} +{{- if .Values.vaultwarden.push.existingSecret }} +{{- .Values.vaultwarden.push.existingSecret }} +{{- else }} +{{- printf "%s-push" (include "vaultwarden.fullname" .) }} +{{- end }} +{{- end }} + +{{/* +Return the secret name for Yubico credentials. +*/}} +{{- define "vaultwarden.yubicoSecretName" -}} +{{- if .Values.vaultwarden.yubico.existingSecret }} +{{- .Values.vaultwarden.yubico.existingSecret }} +{{- else }} +{{- printf "%s-yubico" (include "vaultwarden.fullname" .) }} +{{- end }} +{{- end }} + +{{/* +Return the secret name for database URL. +*/}} +{{- define "vaultwarden.databaseSecretName" -}} +{{- if .Values.database.existingSecret }} +{{- .Values.database.existingSecret }} +{{- else }} +{{- printf "%s-database" (include "vaultwarden.fullname" .) }} +{{- end }} +{{- end }} diff --git a/helm/vaultwarden/templates/configmap.yaml b/helm/vaultwarden/templates/configmap.yaml new file mode 100644 index 00000000..fba8a28d --- /dev/null +++ b/helm/vaultwarden/templates/configmap.yaml @@ -0,0 +1,40 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "vaultwarden.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "vaultwarden.labels" . | nindent 4 }} +data: + ROCKET_PORT: {{ .Values.vaultwarden.rocketPort | quote }} + SIGNUPS_ALLOWED: {{ .Values.vaultwarden.signupsAllowed | quote }} + ENABLE_WEBSOCKET: {{ .Values.vaultwarden.websocket.enabled | quote }} + LOG_LEVEL: {{ .Values.vaultwarden.logging.level | quote }} + ICON_SERVICE: {{ .Values.vaultwarden.icons.service | quote }} + {{- if .Values.vaultwarden.domain }} + DOMAIN: {{ .Values.vaultwarden.domain | quote }} + {{- end }} + {{- if eq .Values.database.type "sqlite" }} + ENABLE_DB_WAL: {{ .Values.database.wal | quote }} + {{- end }} + DATABASE_MAX_CONNS: {{ .Values.database.maxConnections | quote }} + {{- if .Values.vaultwarden.smtp.host }} + SMTP_HOST: {{ .Values.vaultwarden.smtp.host | quote }} + SMTP_FROM: {{ .Values.vaultwarden.smtp.from | quote }} + SMTP_PORT: {{ .Values.vaultwarden.smtp.port | quote }} + SMTP_SECURITY: {{ .Values.vaultwarden.smtp.security | quote }} + {{- end }} + {{- if .Values.vaultwarden.sso.enabled }} + SSO_ENABLED: "true" + SSO_ONLY: {{ .Values.vaultwarden.sso.only | quote }} + SSO_AUTHORITY: {{ .Values.vaultwarden.sso.authority | quote }} + {{- end }} + {{- if .Values.vaultwarden.push.enabled }} + PUSH_ENABLED: "true" + {{- if .Values.vaultwarden.push.relayUri }} + PUSH_RELAY_URI: {{ .Values.vaultwarden.push.relayUri | quote }} + {{- end }} + {{- if .Values.vaultwarden.push.identityUri }} + PUSH_IDENTITY_URI: {{ .Values.vaultwarden.push.identityUri | quote }} + {{- end }} + {{- end }} diff --git a/helm/vaultwarden/templates/deployment.yaml b/helm/vaultwarden/templates/deployment.yaml new file mode 100644 index 00000000..724fdc6b --- /dev/null +++ b/helm/vaultwarden/templates/deployment.yaml @@ -0,0 +1,215 @@ +{{- /* Validation */}} +{{- if and .Values.vaultwarden.admin.enabled (not .Values.vaultwarden.admin.token) (not .Values.vaultwarden.admin.existingSecret) }} + {{- fail "vaultwarden.admin.enabled is true but neither admin.token nor admin.existingSecret is set" }} +{{- end }} +{{- if and (ne .Values.database.type "sqlite") .Values.database.host (not .Values.database.credentialsSecret) }} + {{- fail "database.host is set but database.credentialsSecret is not — provide the secret containing database credentials" }} +{{- end }} +{{- if and (ne .Values.database.type "sqlite") (not .Values.database.host) (not .Values.database.url) (not .Values.database.existingSecret) }} + {{- fail "database.type is not sqlite but no database connection is configured — set database.host (with credentialsSecret), database.url, or database.existingSecret" }} +{{- end }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "vaultwarden.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "vaultwarden.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + {{- if .Values.revisionHistoryLimit }} + revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} + {{- end }} + strategy: + type: Recreate + selector: + matchLabels: + {{- include "vaultwarden.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/configmap: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "vaultwarden.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "vaultwarden.serviceAccountName" . }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.priorityClassName }} + priorityClassName: {{ .Values.priorityClassName }} + {{- end }} + terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} + {{- with .Values.initContainers }} + initContainers: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: vaultwarden + image: "{{ .Values.image.repository }}:{{ include "vaultwarden.imageTag" . }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: {{ .Values.vaultwarden.rocketPort }} + name: http + protocol: TCP + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + envFrom: + - configMapRef: + name: {{ include "vaultwarden.fullname" . }} + env: + {{- /* Admin token */}} + {{- if .Values.vaultwarden.admin.enabled }} + - name: ADMIN_TOKEN + valueFrom: + secretKeyRef: + name: {{ include "vaultwarden.adminSecretName" . }} + key: {{ .Values.vaultwarden.admin.existingSecretKey | default "admin-token" }} + {{- end }} + {{- /* SMTP credentials */}} + {{- if and .Values.vaultwarden.smtp.host (or .Values.vaultwarden.smtp.username .Values.vaultwarden.smtp.existingSecret) }} + - name: SMTP_USERNAME + valueFrom: + secretKeyRef: + name: {{ include "vaultwarden.smtpSecretName" . }} + key: {{ .Values.vaultwarden.smtp.existingSecretUsernameKey | default "smtp-username" }} + - name: SMTP_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "vaultwarden.smtpSecretName" . }} + key: {{ .Values.vaultwarden.smtp.existingSecretPasswordKey | default "smtp-password" }} + {{- end }} + {{- /* SSO credentials */}} + {{- if .Values.vaultwarden.sso.enabled }} + - name: SSO_CLIENT_ID + valueFrom: + secretKeyRef: + name: {{ include "vaultwarden.ssoSecretName" . }} + key: {{ .Values.vaultwarden.sso.existingSecretClientIdKey | default "sso-client-id" }} + - name: SSO_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "vaultwarden.ssoSecretName" . }} + key: {{ .Values.vaultwarden.sso.existingSecretClientSecretKey | default "sso-client-secret" }} + {{- end }} + {{- /* Database URL — Option 2: compose from parts */}} + {{- if and (ne .Values.database.type "sqlite") .Values.database.host }} + - name: _DB_USER + valueFrom: + secretKeyRef: + name: {{ required "database.credentialsSecret is required when database.host is set" .Values.database.credentialsSecret }} + key: {{ .Values.database.credentialsSecretUsernameKey | default "username" }} + - name: _DB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.database.credentialsSecret }} + key: {{ .Values.database.credentialsSecretPasswordKey | default "password" }} + - name: DATABASE_URL + value: {{ .Values.database.type }}://$(_DB_USER):$(_DB_PASSWORD)@{{ .Values.database.host }}:{{ .Values.database.port }}/{{ .Values.database.dbName }} + {{- /* Database URL — Option 1: full URL from secret */}} + {{- else if ne .Values.database.type "sqlite" }} + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ include "vaultwarden.databaseSecretName" . }} + key: {{ .Values.database.existingSecretKey | default "database-url" }} + {{- end }} + {{- /* Push notifications */}} + {{- if .Values.vaultwarden.push.enabled }} + - name: PUSH_INSTALLATION_ID + valueFrom: + secretKeyRef: + name: {{ include "vaultwarden.pushSecretName" . }} + key: {{ .Values.vaultwarden.push.existingSecretInstallationIdKey | default "push-installation-id" }} + - name: PUSH_INSTALLATION_KEY + valueFrom: + secretKeyRef: + name: {{ include "vaultwarden.pushSecretName" . }} + key: {{ .Values.vaultwarden.push.existingSecretInstallationKeyKey | default "push-installation-key" }} + {{- end }} + {{- /* Yubico */}} + {{- if .Values.vaultwarden.yubico.enabled }} + - name: YUBICO_CLIENT_ID + valueFrom: + secretKeyRef: + name: {{ include "vaultwarden.yubicoSecretName" . }} + key: {{ .Values.vaultwarden.yubico.existingSecretClientIdKey | default "yubico-client-id" }} + - name: YUBICO_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ include "vaultwarden.yubicoSecretName" . }} + key: {{ .Values.vaultwarden.yubico.existingSecretSecretKeyKey | default "yubico-secret-key" }} + {{- end }} + {{- /* Extra env vars */}} + {{- with .Values.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: data + mountPath: /data + - name: tmp + mountPath: /tmp + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.startupProbe }} + startupProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: data + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ .Values.persistence.existingClaim | default (include "vaultwarden.fullname" .) }} + {{- else }} + emptyDir: {} + {{- end }} + - name: tmp + emptyDir: + medium: Memory + sizeLimit: 64Mi + {{- with .Values.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/vaultwarden/templates/ingress.yaml b/helm/vaultwarden/templates/ingress.yaml new file mode 100644 index 00000000..38a695fc --- /dev/null +++ b/helm/vaultwarden/templates/ingress.yaml @@ -0,0 +1,45 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "vaultwarden.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "vaultwarden.labels" . | nindent 4 }} + {{- with .Values.ingress.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "vaultwarden.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/vaultwarden/templates/pvc.yaml b/helm/vaultwarden/templates/pvc.yaml new file mode 100644 index 00000000..1e83b887 --- /dev/null +++ b/helm/vaultwarden/templates/pvc.yaml @@ -0,0 +1,33 @@ +{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) -}} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "vaultwarden.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "vaultwarden.labels" . | nindent 4 }} + {{- with .Values.persistence.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + annotations: + helm.sh/resource-policy: keep + {{- with .Values.persistence.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + accessModes: + {{- range .Values.persistence.accessModes }} + - {{ . }} + {{- end }} + {{- $sc := .Values.persistence.storageClassName }} + {{- if not (kindIs "invalid" $sc) }} + {{- if eq (toString $sc) "-" }} + storageClassName: "" + {{- else if $sc }} + storageClassName: {{ $sc | quote }} + {{- end }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size }} +{{- end }} diff --git a/helm/vaultwarden/templates/secret.yaml b/helm/vaultwarden/templates/secret.yaml new file mode 100644 index 00000000..8a2cb599 --- /dev/null +++ b/helm/vaultwarden/templates/secret.yaml @@ -0,0 +1,81 @@ +{{- if and .Values.vaultwarden.admin.enabled .Values.vaultwarden.admin.token (not .Values.vaultwarden.admin.existingSecret) -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "vaultwarden.fullname" . }}-admin + namespace: {{ .Release.Namespace }} + labels: + {{- include "vaultwarden.labels" . | nindent 4 }} +type: Opaque +stringData: + admin-token: {{ .Values.vaultwarden.admin.token | quote }} +{{- end }} +{{- if and .Values.vaultwarden.smtp.host .Values.vaultwarden.smtp.username (not .Values.vaultwarden.smtp.existingSecret) }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "vaultwarden.fullname" . }}-smtp + namespace: {{ .Release.Namespace }} + labels: + {{- include "vaultwarden.labels" . | nindent 4 }} +type: Opaque +stringData: + smtp-username: {{ .Values.vaultwarden.smtp.username | quote }} + smtp-password: {{ .Values.vaultwarden.smtp.password | quote }} +{{- end }} +{{- if and .Values.vaultwarden.sso.enabled .Values.vaultwarden.sso.clientId (not .Values.vaultwarden.sso.existingSecret) }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "vaultwarden.fullname" . }}-sso + namespace: {{ .Release.Namespace }} + labels: + {{- include "vaultwarden.labels" . | nindent 4 }} +type: Opaque +stringData: + sso-client-id: {{ .Values.vaultwarden.sso.clientId | quote }} + sso-client-secret: {{ .Values.vaultwarden.sso.clientSecret | quote }} +{{- end }} +{{- if and .Values.vaultwarden.push.enabled .Values.vaultwarden.push.installationId (not .Values.vaultwarden.push.existingSecret) }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "vaultwarden.fullname" . }}-push + namespace: {{ .Release.Namespace }} + labels: + {{- include "vaultwarden.labels" . | nindent 4 }} +type: Opaque +stringData: + push-installation-id: {{ .Values.vaultwarden.push.installationId | quote }} + push-installation-key: {{ .Values.vaultwarden.push.installationKey | quote }} +{{- end }} +{{- if and .Values.vaultwarden.yubico.enabled .Values.vaultwarden.yubico.clientId (not .Values.vaultwarden.yubico.existingSecret) }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "vaultwarden.fullname" . }}-yubico + namespace: {{ .Release.Namespace }} + labels: + {{- include "vaultwarden.labels" . | nindent 4 }} +type: Opaque +stringData: + yubico-client-id: {{ .Values.vaultwarden.yubico.clientId | quote }} + yubico-secret-key: {{ .Values.vaultwarden.yubico.secretKey | quote }} +{{- end }} +{{- if and (ne .Values.database.type "sqlite") .Values.database.url (not .Values.database.existingSecret) }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "vaultwarden.fullname" . }}-database + namespace: {{ .Release.Namespace }} + labels: + {{- include "vaultwarden.labels" . | nindent 4 }} +type: Opaque +stringData: + database-url: {{ .Values.database.url | quote }} +{{- end }} diff --git a/helm/vaultwarden/templates/service.yaml b/helm/vaultwarden/templates/service.yaml new file mode 100644 index 00000000..3574be8e --- /dev/null +++ b/helm/vaultwarden/templates/service.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "vaultwarden.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "vaultwarden.labels" . | nindent 4 }} + {{- with .Values.service.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + {{- if and (eq .Values.service.type "LoadBalancer") .Values.service.loadBalancerIP }} + loadBalancerIP: {{ .Values.service.loadBalancerIP }} + {{- end }} + {{- if and (or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer")) .Values.service.externalTrafficPolicy }} + externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }} + {{- end }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + {{- if and (eq .Values.service.type "NodePort") .Values.service.nodePort }} + nodePort: {{ .Values.service.nodePort }} + {{- end }} + selector: + {{- include "vaultwarden.selectorLabels" . | nindent 4 }} diff --git a/helm/vaultwarden/templates/serviceaccount.yaml b/helm/vaultwarden/templates/serviceaccount.yaml new file mode 100644 index 00000000..b82ed1ce --- /dev/null +++ b/helm/vaultwarden/templates/serviceaccount.yaml @@ -0,0 +1,14 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "vaultwarden.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "vaultwarden.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +{{- end }} diff --git a/helm/vaultwarden/templates/tests/test-connection.yaml b/helm/vaultwarden/templates/tests/test-connection.yaml new file mode 100644 index 00000000..0c4f9f71 --- /dev/null +++ b/helm/vaultwarden/templates/tests/test-connection.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "vaultwarden.fullname" . }}-test-connection" + namespace: {{ .Release.Namespace }} + labels: + {{- include "vaultwarden.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox:stable + command: ['wget'] + args: ['{{ include "vaultwarden.fullname" . }}:{{ .Values.service.port }}/alive', '-q', '-O', '-'] + restartPolicy: Never diff --git a/helm/vaultwarden/values.yaml b/helm/vaultwarden/values.yaml new file mode 100644 index 00000000..ac940128 --- /dev/null +++ b/helm/vaultwarden/values.yaml @@ -0,0 +1,347 @@ +# -- Number of replicas. Keep at 1 when using SQLite. +replicaCount: 1 + +image: + # -- Container image repository + repository: vaultwarden/server + # -- Image pull policy + pullPolicy: IfNotPresent + # -- Overrides the image tag (default: appVersion from Chart.yaml) + tag: "" + +# -- Image pull secrets +imagePullSecrets: [] +# -- Override the release name +nameOverride: "" +# -- Override the full release name +fullnameOverride: "" + +# ============================================================================= +# Vaultwarden configuration +# ============================================================================= +vaultwarden: + # -- (required) The domain URL for your vaultwarden instance (e.g. https://vault.example.com) + domain: "" + # -- Allow new user signups + signupsAllowed: false + # -- Rocket server port. Set to non-privileged port for non-root operation. + rocketPort: 8080 + # -- Enable websocket notifications + websocket: + enabled: true + + # -- Logging configuration + logging: + # -- Log level (trace, debug, info, warn, error, off) + level: "info" + + # -- Icon service configuration + icons: + # -- Icon service to use (internal, bitwarden, duckduckgo, google) + service: "internal" + + # -- Admin panel configuration + admin: + # -- Enable the admin panel (/admin) + enabled: false + # -- Admin token (argon2 hash recommended). Ignored if existingSecret is set. + token: "" + # -- Use an existing secret for the admin token + existingSecret: "" + # -- Key within the existing secret that holds the admin token + existingSecretKey: "admin-token" + + # -- SMTP email configuration + smtp: + # -- SMTP server hostname + host: "" + # -- Email address used as the sender + from: "" + # -- SMTP server port + port: 587 + # -- SMTP security mode (starttls, force_tls, off) + security: "starttls" + # -- SMTP username. Ignored if existingSecret is set. + username: "" + # -- SMTP password. Ignored if existingSecret is set. + password: "" + # -- Use an existing secret for SMTP credentials + existingSecret: "" + # -- Key in existing secret for SMTP username + existingSecretUsernameKey: "smtp-username" + # -- Key in existing secret for SMTP password + existingSecretPasswordKey: "smtp-password" + + # -- SSO/OpenID Connect configuration + sso: + # -- Enable SSO authentication + enabled: false + # -- Require SSO for all logins (disable password login) + only: false + # -- OpenID Connect authority URL + authority: "" + # -- OIDC client ID. Ignored if existingSecret is set. + clientId: "" + # -- OIDC client secret. Ignored if existingSecret is set. + clientSecret: "" + # -- Use an existing secret for SSO credentials + existingSecret: "" + # -- Key in existing secret for client ID + existingSecretClientIdKey: "sso-client-id" + # -- Key in existing secret for client secret + existingSecretClientSecretKey: "sso-client-secret" + + # -- Push notifications configuration (requires https://bitwarden.com/host keys) + push: + # -- Enable push notifications + enabled: false + # -- Installation ID from https://bitwarden.com/host. Ignored if existingSecret is set. + installationId: "" + # -- Installation key from https://bitwarden.com/host. Ignored if existingSecret is set. + installationKey: "" + # -- Use an existing secret for push notification credentials + existingSecret: "" + # -- Key in existing secret for installation ID + existingSecretInstallationIdKey: "push-installation-id" + # -- Key in existing secret for installation key + existingSecretInstallationKeyKey: "push-installation-key" + # -- Relay URI (default uses Bitwarden US server) + relayUri: "" + # -- Identity URI (default uses Bitwarden US server) + identityUri: "" + + # -- Yubico OTP configuration + yubico: + # -- Enable Yubico OTP support + enabled: false + # -- Yubico client ID. Ignored if existingSecret is set. + clientId: "" + # -- Yubico secret key. Ignored if existingSecret is set. + secretKey: "" + # -- Use an existing secret for Yubico credentials + existingSecret: "" + # -- Key in existing secret for client ID + existingSecretClientIdKey: "yubico-client-id" + # -- Key in existing secret for secret key + existingSecretSecretKeyKey: "yubico-secret-key" + +# ============================================================================= +# Database configuration +# ============================================================================= +database: + # -- Database backend: sqlite, postgresql, or mysql + type: "sqlite" + + # --- Option 1: Full connection URL --- + # -- Full database connection URL. + # Examples: + # postgresql://user:password@host:5432/vaultwarden + # mysql://user:password@host:3306/vaultwarden + url: "" + # -- Use an existing secret containing the full database URL + existingSecret: "" + # -- Key within the existing secret for the database URL + existingSecretKey: "database-url" + + # --- Option 2: Compose from parts (recommended for Postgres operators) --- + # When `host` is set, the chart composes DATABASE_URL from individual fields. + # Username and password are read from a Kubernetes secret via secretKeyRef. + # This is ideal for Zalando Postgres Operator, CloudNativePG, etc. + # -- Database hostname (e.g. my-cluster.postgres-namespace) + host: "" + # -- Database port + port: 5432 + # -- Database name + dbName: "vaultwarden" + # -- Secret containing database user credentials (must have username and password keys) + credentialsSecret: "" + # -- Key in credentialsSecret for the username + credentialsSecretUsernameKey: "username" + # -- Key in credentialsSecret for the password + credentialsSecretPasswordKey: "password" + + # --- Common settings --- + # -- Maximum number of database connections + maxConnections: 10 + # -- Enable WAL mode for SQLite (improves performance) + wal: true + +# ============================================================================= +# Kubernetes resources +# ============================================================================= +serviceAccount: + # -- Create a service account + create: true + # -- Annotations for the service account + annotations: {} + # -- Override the service account name + name: "" + # -- Automount the service account token + automountServiceAccountToken: false + +# -- Pod-level security context +podSecurityContext: + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + +# -- Container-level security context +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + readOnlyRootFilesystem: true + +service: + # -- Service type (ClusterIP, NodePort, LoadBalancer) + type: ClusterIP + # -- Service port + port: 8080 + # -- Node port (only used when type is NodePort) + nodePort: "" + # -- Load balancer IP (only used when type is LoadBalancer) + loadBalancerIP: "" + # -- External traffic policy (Local or Cluster, only used when type is NodePort or LoadBalancer) + externalTrafficPolicy: "" + # -- Additional service annotations (e.g. for external-dns, service meshes) + annotations: {} + # -- Additional service labels + labels: {} + +ingress: + # -- Enable ingress + enabled: false + # -- Ingress class name (e.g. nginx, traefik, haproxy) + className: "" + # -- Ingress annotations (e.g. cert-manager, auth, rate-limiting) + annotations: {} + # cert-manager.io/cluster-issuer: letsencrypt-production + # cert-manager.io/private-key-algorithm: ECDSA + # cert-manager.io/private-key-size: "384" + # cert-manager.io/private-key-rotation-policy: Always + # nginx.ingress.kubernetes.io/proxy-body-size: 128m + # -- Additional ingress labels + labels: {} + # -- Ingress hosts + hosts: + - host: vault.example.com + paths: + - path: / + pathType: Prefix + # -- Ingress TLS configuration + tls: [] + # - secretName: vault-tls + # hosts: + # - vault.example.com + +persistence: + # -- Enable persistent storage for /data + enabled: true + # -- Storage class name. + # Set to "-" to disable dynamic provisioning. + # Leave unset or null for the cluster default storage class. + # @default -- `nil` (cluster default) + storageClassName: + # -- Access modes + accessModes: + - ReadWriteOnce + # -- Storage size + size: 5Gi + # -- Use an existing PVC instead of creating one + existingClaim: "" + # -- Additional PVC annotations + annotations: {} + # -- Additional PVC labels + labels: {} + +# -- Resource requests and limits +resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 1Gi + +# -- Liveness probe configuration +livenessProbe: + httpGet: + path: /alive + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + +# -- Readiness probe configuration +readinessProbe: + httpGet: + path: /alive + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + +# -- Startup probe configuration (useful for slow-starting instances with large databases) +startupProbe: {} +# httpGet: +# path: /alive +# port: http +# failureThreshold: 30 +# periodSeconds: 5 + +# -- Deployment revision history limit +revisionHistoryLimit: 3 + +# -- Pod annotations +podAnnotations: {} + +# -- Pod labels +podLabels: {} + +# -- Priority class name for pod scheduling +priorityClassName: "" + +# -- Termination grace period in seconds +terminationGracePeriodSeconds: 30 + +# -- Node selector +nodeSelector: {} + +# -- Tolerations +tolerations: [] + +# -- Affinity rules +affinity: {} + +# -- Topology spread constraints for pod scheduling +topologySpreadConstraints: [] +# - maxSkew: 1 +# topologyKey: kubernetes.io/hostname +# whenUnsatisfiable: DoNotSchedule +# labelSelector: +# matchLabels: ... + +# -- Init containers +initContainers: [] + +# -- Additional environment variables +extraEnv: [] +# - name: EXAMPLE_VAR +# value: "example" +# - name: SECRET_VAR +# valueFrom: +# secretKeyRef: +# name: my-secret +# key: my-key + +# -- Additional volume mounts for the vaultwarden container +extraVolumeMounts: [] +# - name: custom-certs +# mountPath: /etc/ssl/custom +# readOnly: true + +# -- Additional volumes +extraVolumes: [] +# - name: custom-certs +# secret: +# secretName: custom-ca-certs From 834a194816abc4e16b8337795dbefe27642b9d50 Mon Sep 17 00:00:00 2001 From: Rohmilchkaese Date: Wed, 18 Feb 2026 17:59:53 +0100 Subject: [PATCH 2/2] feat(helm): add env, secretEnv maps for flexible env var configuration Add three layers for setting environment variables: - env: plain key-value map for any vaultwarden env var - secretEnv: shorthand for secretKeyRef without verbose YAML - extraEnv: raw Kubernetes env spec for complex cases (fieldRef, etc.) This lets users set any vaultwarden env var without requiring chart changes, while the structured values (vaultwarden.smtp.*, database.*, etc.) remain available for validation and existingSecret integration. --- helm/vaultwarden/README.md | 38 +++++++++++++++++++++- helm/vaultwarden/templates/deployment.yaml | 15 ++++++++- helm/vaultwarden/values.yaml | 34 +++++++++++++++---- 3 files changed, 78 insertions(+), 9 deletions(-) diff --git a/helm/vaultwarden/README.md b/helm/vaultwarden/README.md index 30da661a..2f1f6479 100644 --- a/helm/vaultwarden/README.md +++ b/helm/vaultwarden/README.md @@ -326,12 +326,48 @@ The chart runs vaultwarden as a non-root user (UID 1000) by default with a read- | `terminationGracePeriodSeconds` | Termination grace period | `30` | | `startupProbe` | Startup probe config (for slow starts) | `{}` | | `initContainers` | Init containers | `[]` | -| `extraEnv` | Additional environment variables | `[]` | | `extraVolumes` | Additional volumes | `[]` | | `extraVolumeMounts` | Additional volume mounts | `[]` | | `podAnnotations` | Pod annotations | `{}` | | `podLabels` | Additional pod labels | `{}` | +### Environment Variables + +The chart provides three layers for setting environment variables, from simplest to most flexible: + +**`env`** — plain key-value map for any vaultwarden env var: + +```yaml +env: + SIGNUPS_ALLOWED: "true" + INVITATION_ORG_NAME: "My Org" + SENDS_ALLOWED: "true" +``` + +**`secretEnv`** — shorthand for sourcing env vars from Kubernetes secrets: + +```yaml +secretEnv: + ADMIN_TOKEN: + secretName: my-admin-secret + secretKey: admin-token + DATABASE_URL: + secretName: my-db-secret + secretKey: database-url +``` + +**`extraEnv`** — raw Kubernetes env spec for complex cases (fieldRef, resourceFieldRef, etc.): + +```yaml +extraEnv: + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP +``` + +These layers are additive and render in order: structured values (from `vaultwarden.*`), then `env`, then `secretEnv`, then `extraEnv`. Later values override earlier ones for the same env var name. + ## Using Existing Secrets For production deployments, use `existingSecret` references instead of putting credentials in `values.yaml`. All sensitive values support `existingSecret`: diff --git a/helm/vaultwarden/templates/deployment.yaml b/helm/vaultwarden/templates/deployment.yaml index 724fdc6b..c14d7897 100644 --- a/helm/vaultwarden/templates/deployment.yaml +++ b/helm/vaultwarden/templates/deployment.yaml @@ -154,7 +154,20 @@ spec: name: {{ include "vaultwarden.yubicoSecretName" . }} key: {{ .Values.vaultwarden.yubico.existingSecretSecretKeyKey | default "yubico-secret-key" }} {{- end }} - {{- /* Extra env vars */}} + {{- /* Plain env vars from env map */}} + {{- range $name, $value := .Values.env }} + - name: {{ $name }} + value: {{ $value | quote }} + {{- end }} + {{- /* Secret env vars from secretEnv map */}} + {{- range $name, $ref := .Values.secretEnv }} + - name: {{ $name }} + valueFrom: + secretKeyRef: + name: {{ $ref.secretName }} + key: {{ $ref.secretKey }} + {{- end }} + {{- /* Raw extra env vars */}} {{- with .Values.extraEnv }} {{- toYaml . | nindent 12 }} {{- end }} diff --git a/helm/vaultwarden/values.yaml b/helm/vaultwarden/values.yaml index ac940128..60c247d5 100644 --- a/helm/vaultwarden/values.yaml +++ b/helm/vaultwarden/values.yaml @@ -324,15 +324,35 @@ topologySpreadConstraints: [] # -- Init containers initContainers: [] -# -- Additional environment variables +# -- Additional environment variables (plain key-value). +# Use this to set any vaultwarden env var not covered by the structured values above. +# These are added to the container env directly. +env: {} +# SIGNUPS_ALLOWED: "false" +# INVITATION_ORG_NAME: "My Org" +# SENDS_ALLOWED: "true" +# EMERGENCY_ACCESS_ALLOWED: "true" + +# -- Environment variables sourced from Kubernetes secrets (secretKeyRef shorthand). +# Each key is the env var name, value specifies the secret and key to read from. +secretEnv: {} +# ADMIN_TOKEN: +# secretName: my-admin-secret +# secretKey: admin-token +# DATABASE_URL: +# secretName: my-db-secret +# secretKey: database-url +# SMTP_PASSWORD: +# secretName: my-smtp-secret +# secretKey: password + +# -- Additional environment variables (raw Kubernetes env spec). +# Use this for complex env definitions like fieldRef, resourceFieldRef, etc. extraEnv: [] -# - name: EXAMPLE_VAR -# value: "example" -# - name: SECRET_VAR +# - name: POD_IP # valueFrom: -# secretKeyRef: -# name: my-secret -# key: my-key +# fieldRef: +# fieldPath: status.podIP # -- Additional volume mounts for the vaultwarden container extraVolumeMounts: []