Skip to content
Geek is the Way!
Menu
  • Forums
  • Sobre o blog
  • Contato
  • English
Menu

Deploying Vaultwarden into a Kubernetes with High Availability

Posted on June 24, 2026June 24, 2026 by Thiago Crepaldi

Last Updated on June 24, 2026 by Thiago Crepaldi

In an earlier post, we covered Deploying a Public Vaultwarden Instance on a Proxmox LXC Container Using HAProxy on pfSense. While that architecture served us well, running a critical service like a password manager on a single node introduces a single point of failure.

This guide represents the evolution of that setup. We are deploying Vaultwarden into a highly available (HA) Kubernetes cluster. By leveraging Longhorn for distributed storage, PostgreSQL for concurrent database connections, and running multiple replicas of the Vaultwarden application, we eliminate downtime during updates and protect against node failures.

If you still don’t have a Kubernetes cluster, I really encourage you to get into it. The post https://geekistheway.com/2026/06/22/building-an-enterprise-grade-kubernetes-k3s-stack-on-proxmox-ve/ can get you going, reusing your Proxmox VE installation.

📋 Prerequisites

  • A functional K3s/Kubernetes cluster.
  • Longhorn installed for distributed block storage.
  • Cert-Manager configured for automated Let’s Encrypt SSL management.
  • Ingress-Nginx deployed as your cluster’s ingress controller.
  • pfSense as firewall/router to expose your Vaultwarden service to the Internet
  • CloudFlare to manage the DNS of your mydomain.com domain

1. Establish a “Retain” Storage Policy

In Kubernetes, dynamically provisioned storage (like Longhorn volumes) is often tied to the lifecycle of the application. If you uninstall a Helm chart, Kubernetes might delete the underlying storage volumes by default, taking your data with it.

To protect your password vault, we will create a custom StorageClass with a Retain reclaim policy. This guarantees that even if the Vaultwarden deployment is completely deleted, the Longhorn volumes containing your database and attachments will safely remain on your disks until you manually destroy them.

Action: Create a file named storageclass-retain.yaml:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: longhorn-retain
provisioner: driver.longhorn.io
reclaimPolicy: Retain

Apply the configuration:

kubectl apply -f storageclass-retain.yaml

✅ Validation: Run kubectl get sc. You should see longhorn-retain listed with Retain in the ReclaimPolicy column.

2. Secure Credentials in Kubernetes Secrets

Helm charts often allow you to pass passwords and API keys directly in the values.yaml file. However, this is a poor security practice, as plaintext passwords can end up saved in your version control, Helm release history, and Kubernetes pod definitions.

Instead, we will pre-create Kubernetes Secrets. The Helm charts will reference these secrets, injecting them securely into the containers at runtime. (Note: We are purposefully leaving the ADMIN_TOKEN out of this step to keep the admin panel disabled by default for maximum security.)

To enable live Mobile Push Notifications, you will need an official Installation ID. Visit https://bitwarden.com/host/, enter an email address, and copy the resulting Installation ID and Key.

Run these commands in your terminal, replacing the placeholder values:

kubectl create secret generic vaultwarden-db-secret \
  --from-literal=pgusername="vaultwarden" \
  --from-literal=pgpassword="your-strong-admin-password" \
  --from-literal=postgres-password="your-strong-db-password"

Create the Vaultwarden Application secret:

kubectl create secret generic vaultwarden-app-secret \
  --from-literal=SMTP_USERNAME="emailer@example.com" \
  --from-literal=SMTP_PASSWORD="your-smtp-password" \
  --from-literal=YUBICO_SECRET="your-yubico-key" \
  --from-literal=PUSH_INSTALLATION_ID="your-push-id" \
  --from-literal=PUSH_INSTALLATION_KEY="your-push-key"

✅ Validation: Run kubectl get secrets. Both vaultwarden-db-secret and vaultwarden-app-secret should be present in the list.

3. Deploy PostgreSQL

SQLite is excellent for single-node deployments, but it locks the entire database file during write operations. In a High Availability setup with multiple Vaultwarden replicas trying to write data simultaneously, SQLite can corrupt or bottleneck. PostgreSQL is a robust relational database designed to handle high concurrency, making it the perfect backend for our distributed deployment.

Create a db-values.yaml file targeting the Bitnami PostgreSQL chart:

auth:
  database: "vaultwarden"
  username: "vaultwarden"
  existingSecret: "vaultwarden-db-secret"
primary:
  persistence:
    storageClass: "longhorn-retain"
    size: "5Gi"

Deploy the database:

helm install vaultwarden-db bitnami/postgresql -f db-values.yaml

✅ Validation: Run kubectl get pods. Wait until the vaultwarden-db-postgresql-0 pod reports a status of 1/1 Running.

4. Deploy Vaultwarden (Public Access)

Now we deploy the core Vaultwarden application using the guerzon/vaultwarden Helm chart. This configuration establishes our high availability by requesting two replicas. It also defines our Longhorn volumes utilizing the ReadWriteMany access mode, allowing both pods to read and write to the attachments folder simultaneously across different physical nodes.

This step configures the public-facing Ingress, which routes standard traffic to the root / path and automatically provisions our Let’s Encrypt SSL certificate.

Create your vw-values.yaml file:

image:
  repository: vaultwarden/server
  tag: latest

replicas: 2
domain: "https://vault.mydomain.com"
signupsAllowed: "false"
webVaultEnabled: "true"
sendsAllowed: "true"
invitationsAllowed: true
invitationOrgName: "MyDomain.com"
emergencyAccessAllowed: "true"
timeZone: "America/New_York"
resourceType: "Deployment"
orgEventsEnabled: "true"
extendedLogging: "true"
logTimestampFormat: "%Y-%m-%d %H:%M:%S.%3f"
logging:
  logLevel: "info"
  logFile: "/var/log/vaultwarden.log"

database:
  type: postgresql
  host: "vaultwarden-db-postgresql"
  port: "5432"
  dbName: "vaultwarden"
  existingSecret: "vaultwarden-db-secret"
  existingSecretUserKey: "pgusername"
  existingSecretPasswordKey: "pgpassword"

yubico:
  clientId: your-numeric-client-id-without-quotes
  existingSecret: "vaultwarden-app-secret"
  secretKey:
    existingSecretKey: "YUBICO_SECRET"

smtp:
  host: "smtp.gmail.com"
  from: "username@gmail.com"
  port: 587
  security: "starttls"
  existingSecret: "vaultwarden-app-secret"
  username:
    value: "username@gmail.com"
  password:
    existingSecretKey: "SMTP_PASSWORD"

pushNotifications:
  enabled: true
  existingSecret: "vaultwarden-app-secret"
  installationId:
    existingSecretKey: "PUSH_INSTALLATION_ID"
  installationKey:
    existingSecretKey: "PUSH_INSTALLATION_KEY"


# Storage Configuration (Must be ReadWriteMany for Deployment Replicas)
storage:
  data:
    name: "vaultwarden-data"
    size: "1Gi"
    class: "longhorn-retain"
    path: "/data"
    keepPvc: true
    accessMode: "ReadWriteMany"

  attachments:
    name: "vaultwarden-attachments"
    size: "20Gi"
    class: "longhorn-retain"
    path: "/data/attachments"
    keepPvc: true
    accessMode: "ReadWriteMany"

ingress:
  enabled: true
  class: "nginx"
  hostname: "vault.mydomain.com"
  path: "/"
  tls: true
  tlsSecret: "vaultwarden-tls"
  nginxIngressAnnotations: true
  additionalAnnotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    kubernetes.io/tls-acme: "true"

Deploy Vaultwarden:

helm install vaultwarden vaultwarden/vaultwarden -f vw-values.yaml

✅ Validation: Run kubectl get pods -l app.kubernetes.io/name=vaultwarden. You should see two pods, both running. Run kubectl get certificate vaultwarden-tls to ensure it reports Ready: True.

5. Data Migration from SQLite/LXC (Optional)

If you are migrating an existing instance, you cannot simply copy an SQLite database into a running multi-node cluster. We must backup your legacy LXC data, inject it into the new Longhorn volumes, and stream the old SQLite data into our new PostgreSQL database using pgloader.

Step A: Backup your old server

  1. SSH into your old LXC container/server: ssh user@<old_server_ip>
  2. Locate your Vaultwarden data directory (usually /var/lib/vaultwarden/data or/data or /opt/vaultwarden/data)
  3. Create a compressed tarball of the entire directory:
sudo tar -czvf /tmp/vaultwarden-backup.tar.gz -C /var/lib/vaultwarden/data .
  1. Exit the SSH session and securely copy the backup to your local machine (or your K3s control plane):
scp user@<old_server_ip>:/tmp/vaultwarden-backup.tar.gz ./

Step B: Migrate Data to Kubernetes

  1. Freeze the App: Prevent file conflicts by scaling down to zero: kubectl scale deployment vaultwarden --replicas=0
  2. Deploy the Migrator Pod: We need a temporary pod mounted to your exact Longhorn storage claims to extract the files. Create migrator-pod.yaml:
apiVersion: v1
kind: Pod
metadata:
  name: vw-migrator
spec:
  containers:
  - name: migrator
    image: ubuntu:latest
    command: ["sleep", "infinity"]
    volumeMounts:
    - name: data-vol
      mountPath: /vw-data
    - name: attach-vol
      mountPath: /vw-attachments
  volumes:
  - name: data-vol
    persistentVolumeClaim:
      claimName: vaultwarden-data 
  - name: attach-vol
    persistentVolumeClaim:
      claimName: vaultwarden-attachments

Apply it: kubectl apply -f migrator-pod.yaml

  1. Transfer Data: Copy your backup tarball into the pod.

kubectl cp ./vaultwarden-backup.tar.gz vw-migrator:/root/

  1. Fix Ownership and Stream DB: Execute into the pod to place the files and run pgloader. The chown 1000:1000 command is critical—Kubernetes runs the Vaultwarden container as an unprivileged user (UID 1000). If root owns these files, Vaultwarden will crash on startup.
kubectl exec -it vw-migrator -- bash

# Inside the pod:
apt-get update && apt-get install -y pgloader
cd /root
tar -xzvf vaultwarden-backup.tar.gz

# Move files to Longhorn mounts
cp -r attachments/* /vw-attachments/
cp -r sends/* /vw-attachments/
cp rsa_key* /vw-data/

# Fix Permissions for Vaultwarden User
chown -R 1000:1000 /vw-data /vw-attachments

# Migrate the Database
pgloader sqlite:///root/db.sqlite3 postgresql://vaultwarden:your-strong-db-password@vaultwarden-db-postgresql:5432/vaultwarden
exit
  1. Clean Up: Delete the migrator and scale your deployment back up:
kubectl delete pod vw-migrator kubectl scale deployment vaultwarden --replicas=2

    ✅ Validation: Log into your web vault. Your existing passwords, organizations, and file attachments should all be fully accessible.

    6. Managing the Admin Panel

    The Vaultwarden admin panel at /admin allows for powerful global configuration changes. How you expose this interface determines your security posture. Choose one of the following three paths based on your needs:

    Path 1: Disable the Admin Panel (Most Secure & Recommended)

    If you don’t actively need to change global settings, keeping the panel disabled is the safest approach. This is the default state if no ADMIN_TOKEN is provided. Action: Ensure the adminToken block is absent from your vw-values.yaml and do not add an admin key to your secrets. If you previously enabled it, remove the block and run:

    helm upgrade vaultwarden vaultwarden/vaultwarden -f vw-values.yaml

    Path 2: Enable Globally (Accessible from the Internet – High Risk)

    This makes the /admin page accessible from anywhere. It relies entirely on the strength of your Argon2 token.

    1. Add Token to Secret: Generate an Argon2 hash and patch your secret:
    kubectl edit secret vaultwarden-app-secret # Under data:, add ADMIN_TOKEN: <base64-encoded-argon2-hash>
    1. Update Helm Chart: Add this to vw-values.yaml:
    adminToken:
      existingSecret: "vaultwarden-app-secret"
      existingSecretKey: "ADMIN_TOKEN"
    1. Deploy: helm upgrade vaultwarden vaultwarden/vaultwarden -f vw-values.yaml

    Path 3: Enable Locally Only (Split Ingress – Best Balance)

    This keeps the admin panel enabled but restricts access strictly to your home network via an IP whitelist.

    Step 3A: Preserve Client IPs for NGINX By default, Kubernetes uses an externalTrafficPolicy: Cluster setting, obscuring the original client IP. To enforce an IP whitelist, NGINX must see your real IP. Run this command:

    kubectl patch svc ingress-nginx-controller -n ingress-nginx -p '{"spec":{"externalTrafficPolicy":"Local"}}'

    ⚠️ Side Effects & Reverting: Setting this to Local means traffic is only routed to NGINX pods on the specific node receiving the traffic, which can cause uneven load balancing in large clusters. To revert later, run the same command replacing Local with Cluster.

    Step 3B: Enable the Admin Token

    1. Add the base64-encoded ADMIN_TOKEN to your vaultwarden-app-secret via kubectl edit.
    2. Add the adminToken block to your vw-values.yaml (exactly as shown in Path 2).
    3. Run helm upgrade vaultwarden vaultwarden/vaultwarden -f vw-values.yaml.

    Step 3C: Deploy the Secure Admin Ingress Instead of routing /admin in the Helm chart, we deploy a standalone Ingress to apply our NGINX whitelist. Create admin-ingress.yaml:

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: vaultwarden-admin
      annotations:
        cert-manager.io/cluster-issuer: "letsencrypt-prod"
        kubernetes.io/tls-acme: "true"
        # Restrict access to local LAN subnets (adjust to match your home network)
        nginx.ingress.kubernetes.io/whitelist-source-range: "192.168.0.0/16,10.0.0.0/8"
    spec:
      ingressClassName: nginx
      tls:
      - hosts:
        - vault.mydomain.com
        secretName: vaultwarden-tls
      rules:
      - host: vault.mydomain.com
        http:
          paths:
          - path: /admin
            pathType: Prefix
            backend:
              service:
                name: vaultwarden
                port:
                  number: 80

    Apply the security layer: kubectl apply -f admin-ingress.yaml

    7. Configure pfSense to expose Vaultwarden to the Internet

    By default, pfSense drops all unsolicited inbound traffic. We need to explicitly tell it to take traffic hitting port 443 (HTTPS) on your public IP and forward it to your Kubernetes infra, more specifically to MetaILB load balancer.

    In pfSense, this is a two-step process: Translation (NAT) and Allowance (Firewall Rule). Fortunately, pfSense can do both at once if configured correctly.

    Step 2.1: Create the Port Forward (NAT)

    1. Log into your pfSense WebGUI.
    2. Navigate to Firewall -> NAT -> Port Forward.
    3. Click Add (to add a rule to the top or bottom of the list).
    4. Configure the rule exactly as follows:
      • Interface: WAN
      • Address Family: IPv4
      • Protocol: TCP
      • Destination: WAN Address
      • Destination Port Range: From HTTPS (443) to HTTPS (443)
      • Redirect Target IP: 192.168.1.201 (Replace with your specific MetalLB Ingress IP)
      • Redirect Target Port: HTTPS (443)
      • Description: K3s NGINX Ingress - HTTPS
      • Filter rule association: Select Add associated filter rule (This is crucial!).
    5. Click Save and then Apply Changes.

    Step 2.2: Verify the Firewall Rule

    Because pfSense evaluates NAT before Firewall rules, the destination IP in the firewall rule must be the internal, post-translated IP. Let’s verify pfSense generated this correctly.

    1. Navigate to Firewall -> Rules -> WAN.
    2. Look for the rule created by the NAT process (it will usually have a linked icon next to it).
    3. Ensure the rule looks exactly like this:
      • Action: Pass (Green checkmark)
      • Protocol: IPv4 TCP
      • Source: * (Any)
      • Port: *
      • Destination: 192.168.1.201 (Your MetalLB IP)
      • Port: 443 (HTTPS)
      • Gateway: *
    4. If this rule is missing, click Add and create it manually using the parameters above. Click Apply Changes.

    8. Configure Cloudflare DNS

    Now that the gate is open, we need to point the domain to your house.

    1. Log into your Cloudflare Dashboard and select your mydomain.com domain.
    2. Go to DNS -> Records.
    3. Click Add Record:
      • Type: A
      • Name: vault
      • IPv4 Address: Your Home WAN Public IP (You can find this by googling “What is my IP” from your home network).
      • Proxy Status:DNS Only (Grey Cloud).
        • Geek Note: We are using Cert-Manager with DNS-01 challenges, so the proxy status technically doesn’t impact certificate generation. However, setting it to “DNS Only” initially removes Cloudflare’s proxy from the equation, making it vastly easier to troubleshoot your pfSense routing. You can turn the proxy on (Orange Cloud) after everything is working!
    4. Click Save.

    9. Keeping Vaultwarden Updated

    Vaultwarden frequently releases updates to maintain API compatibility with official Bitwarden clients. Because you are running a Kubernetes Deployment, updates are handled via a Rolling Update strategy, ensuring zero downtime for your users.

    Check your current version: To see exactly which image tag your pods are currently running, use:

    kubectl describe pods -l app.kubernetes.io/name=vaultwarden | grep Image:

    Method A: Update the Helm Chart (Easiest)

    Usually, the Helm chart maintainers update the default container image tag in their repository. You can pull these latest definitions and apply them:

    helm repo update
    helm upgrade vaultwarden vaultwarden/vaultwarden -f vw-values.yaml
    

    Method B: Pin a Specific Container Version (Most Control)

    If you want to manually dictate which version of Vaultwarden runs, you can explicitly define the container tag in your configuration.

    1. Find the Latest Tag: Go to the Vaultwarden Docker Hub Tags page. Look for the latest Alpine-based version (e.g., 1.32.0-alpine).
    2. Update vw-values.yaml: Add the image block to the top of your file:
    image:
      repository: vaultwarden/server
      tag: "1.32.0-alpine" # Update this to the tag you found
    1. Apply the update:Bashhelm upgrade vaultwarden vaultwarden/vaultwarden -f vw-values.yaml

    ✅ Validation: Run kubectl rollout status deployment/vaultwarden. You will see Kubernetes carefully spin up the new version, wait for health checks to pass, and only then terminate the older pods.

    🎯 Conclusion

    By deploying Vaultwarden into a Kubernetes cluster, we have built a resilient, enterprise-grade password management infrastructure.

    We successfully:

    • Achieved High Availability through a multi-replica deployment, ensuring that software updates or node reboots occur with zero downtime.
    • Boosted concurrency by utilizing a robust PostgreSQL relational database capable of handling multiple pods writing simultaneously.
    • Decoupled our data from the application lifecycle using Longhorn Retain volumes, ensuring safe, persistent block storage.
    • Secured our deployment by moving sensitive API keys into base64-encoded Kubernetes Secrets rather than plaintext Helm configurations.
    • (If opted in) Hardened the perimeter using advanced NGINX ingress routing to restrict administrative access to trusted local networks, and
    • (if applicable) successfully migrated legacy SQLite data into our modern cluster.

    You now have a production-grade Vaultwarden instance running securely in your homelab!

    Share this:

    • Tweet

    Related

    Leave a ReplyCancel reply

    LIKED? SUPPORT IT :)

    Buy Me a Coffee


    Search


    Categories

    • Cooking (1)
    • Homelab (86)
      • APC UPS (6)
      • Kubernetes (2)
      • pfSense (42)
      • Plex (1)
      • Prometheus & Grafana (1)
      • Proxmox (23)
      • Shopping (1)
      • Supermicro (2)
      • Synology NAS (8)
      • Ubiquiti (6)
      • UDM-Pro (4)
    • Random (3)
    • Wordpress (1)

    Tags

    Agentless monitoring (3) AP9631 (3) Apache2 (3) APC UPS (6) apt-get software (2) Bind9 (3) certificates (5) CloudFlare (2) DDNS (5) debian (3) DNS (7) DSM (6) Dynamic DNS (4) Firewall (9) gmail (3) GPU (3) kubernetes (2) Let's Encrypt Certificates (7) monitoring (19) networking (21) PBS (3) pfsense (43) port forwarding (3) privacy (2) proxmox (18) proxmox backup server (3) proxmox virtual environment (17) pve (5) rev202207eng (76) security (29) SNMP (4) SNMPv1 (3) ssh (4) SSL (6) Synology (7) udm-pro (5) UDR (2) unifi (6) unifi controller (3) UPS (5) VLAN (4) vpn (9) wifi (4) Zabbix (18) Zabbix Agent2 (11)

    See also

    Privacy policy

    Sitemap

    ©2026 Geek is the Way! | Design by Superb