Self-Hosting Vaultwarden on Kubernetes: A Complete Production-Ready Guide with Security Hardening

Complete step-by-step guide to deploying and securing Vaultwarden password manager on Kubernetes with enterprise-grade security, including pod security contexts, network policies, secret management, and monitoring

A comprehensive guide to deploying and securing your own password manager on Kubernetes


Table of Contents

  1. Introduction
  2. What is Vaultwarden?
  3. Why Self-Host Your Password Manager?
  4. Prerequisites
  5. Architecture Overview
  6. Step 1: Database Setup
  7. Step 2: Secret Management
  8. Step 3: Kubernetes Deployment
  9. Step 4: Security Hardening
  10. Step 5: Network Policies
  11. Step 6: Monitoring & Maintenance
  12. Testing Your Deployment
  13. Conclusion

Introduction

In an era where data breaches hit record highs with over 2,300 incidents in 2024 alone, and 81% of these breaches are linked to weak or reused passwords, password management has never been more critical. While cloud-based password managers are convenient, they come with inherent risks: you’re trusting a third party with the keys to your digital kingdom.

This is where self-hosting comes in. By deploying your own password manager, you gain complete control over your sensitive data, eliminate third-party trust requirements, and can implement security measures tailored to your specific needs.

In this comprehensive guide, I’ll walk you through deploying Vaultwardenβ€”a lightweight, unofficial Bitwarden-compatible serverβ€”on Kubernetes with production-grade security hardening. This isn’t just a basic deployment; we’ll implement pod security contexts, network policies, rate limiting, comprehensive secret management, and monitoring to create a truly production-ready system.


What is Vaultwarden?

Vaultwarden (formerly known as bitwarden_rs) is an unofficial, open-source reimplementation of the Bitwarden server API written in Rust. It’s designed to be lightweight, resource-efficient, and easy to self-host while maintaining full compatibility with all official Bitwarden clients.

Key Features

Vaultwarden vs. Official Bitwarden

FeatureVaultwardenOfficial Bitwarden
Resource Usage~50MB RAM, SQLite/PostgreSQL2GB+ RAM, MSSQL required
Premium FeaturesFreePaid ($10-40/year)
Setup ComplexityLowHigh (multiple services)
SupportCommunityOfficial
AuditCommunity-drivenThird-party audited
Client Compatibility100%100% (official)

Why Self-Host Your Password Manager?

1. Complete Data Control

With a self-hosted password manager, your data resides entirely on your own servers. You’re not trusting a third party, regardless of their security claims. This is particularly important for businesses handling sensitive client data or individuals with strict privacy requirements.

2. Enhanced Security Posture

Self-hosting allows you to:

  • Implement your own security models and policies
  • Place your password manager behind custom proxies and firewalls
  • Control exactly who has access and from where
  • Audit all access attempts and changes
  • Implement custom authentication mechanisms

As noted by security experts, businesses can implement security measures that commercial password managers may not provide.

3. No Vendor Lock-In

Cloud services can change pricing, terms, or even shut down. Self-hosting means you’re never held hostage by a vendor’s business decisions.

4. Compliance & Regulatory Requirements

Many industries (healthcare, finance, government) have strict data residency and handling requirements. Self-hosting ensures you meet these obligations.

5. Cost Savings

For teams, Bitwarden premium can cost $40/year per user. A self-hosted Vaultwarden instance can serve unlimited users at just the cost of your infrastructure.

The Trade-offs

Self-hosting isn’t without responsibilities:

  • You must handle backups, updates, and security patches
  • You’re responsible for availability and disaster recovery
  • You need technical expertise to maintain the system
  • You won’t have official vendor support

However, with proper setup (which this guide provides), these responsibilities become manageable routine maintenance tasks.


Prerequisites

Before we begin, ensure you have:

Infrastructure Requirements

  • Kubernetes Cluster: v1.24+ (this guide uses a standard K8s cluster)
  • kubectl: Configured and authenticated to your cluster
  • Helmfile: For simplified Helm chart management
  • PostgreSQL Database: Running in your cluster or externally accessible
  • Ingress Controller: NGINX Ingress Controller installed
  • Cert-Manager: For automatic TLS certificate management
  • DNS: A domain name with DNS configured (e.g., vault.yourdomain.com)

Secret Management

  • HashiCorp Vault: For secure secret storage
  • External Secrets Operator: To sync secrets from Vault to Kubernetes

Knowledge Requirements

  • Basic Kubernetes concepts (pods, services, deployments)
  • Understanding of Helm charts
  • Familiarity with YAML configuration
  • Basic command-line proficiency

Estimated Time

  • Initial setup: 1-2 hours
  • Security hardening: 30-60 minutes
  • Testing and validation: 30 minutes

Architecture Overview

Our production-ready Vaultwarden deployment consists of several components:

 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
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Internet                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      β”‚
                      β”‚ HTTPS (TLS 1.2+)
                      β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              NGINX Ingress Controller                        β”‚
β”‚  - TLS Termination                                          β”‚
β”‚  - Security Headers (CSP, HSTS, X-Frame-Options)            β”‚
β”‚  - Rate Limiting (10 req/sec)                               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      β”‚
                      β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚    Vaultwarden Pod          β”‚
        β”‚  - Non-root user (UID 1000) β”‚
        β”‚  - Dropped capabilities     β”‚
        β”‚  - Rate limiting            β”‚
        β”‚  - Extended logging         β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚                   β”‚
         β–Ό                   β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  PostgreSQL DB   β”‚   β”‚  External SMTP   β”‚
β”‚  - Encrypted     β”‚   β”‚  - Email         β”‚
β”‚    connection    β”‚   β”‚    notifications β”‚
β”‚  - Connection    β”‚   β”‚  - 2FA codes     β”‚
β”‚    pooling       β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β–²
         β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Vault (Secrets)  β”‚
β”‚  ↓                β”‚
β”‚  External Secrets β”‚
β”‚  Operator         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Component Breakdown

  1. Ingress Layer: Handles TLS termination, security headers, and initial rate limiting
  2. Vaultwarden Application: Runs as non-root with minimal privileges
  3. PostgreSQL Database: Persistent storage for encrypted vault data
  4. Vault + External Secrets: Secure secret management pipeline
  5. Network Policies: Restrict pod-to-pod communication

Step 1: Database Setup

Vaultwarden requires a database to store encrypted vault data. While it supports SQLite, PostgreSQL is strongly recommended for production due to better concurrency, backup capabilities, and reliability.

1.1 Create Database and User

Connect to your PostgreSQL instance:

1
2
3
4
5
# If PostgreSQL is running in Kubernetes
kubectl exec -it postgres-app-0 -n postgres -- psql -U postgres

# Or connect from your local machine
psql -h your-postgres-host -U postgres

Create the database and dedicated user:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
-- Create a dedicated user for vaultwarden
CREATE USER vaultwarden WITH ENCRYPTED PASSWORD 'REPLACE_WITH_STRONG_PASSWORD';

-- Create the database owned by vaultwarden user
CREATE DATABASE vaultwarden WITH OWNER vaultwarden;

-- Grant all privileges on the database
GRANT ALL PRIVILEGES ON DATABASE vaultwarden TO vaultwarden;

-- Connect to the vaultwarden database
\c vaultwarden postgres

-- Grant schema privileges (important for table creation)
GRANT ALL ON SCHEMA public TO vaultwarden;

-- Grant default privileges for future objects
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO vaultwarden;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO vaultwarden;

1.2 Verify Database Setup

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
-- Verify user was created
\du vaultwarden

-- Verify database exists and has correct owner
\l vaultwarden

-- Expected output:
--   Name       | Owner       | Encoding | Collate | Ctype | Access privileges
-- -------------+-------------+----------+---------+-------+-------------------
--  vaultwarden | vaultwarden | UTF8     | ...     | ...   | =Tc/vaultwarden

1.3 Construct Database URL

Your database connection string will look like:

1
postgresql://vaultwarden:<PASSWORD>@<HOST>:<PORT>/vaultwarden

For example:

Security Note: Never commit this URL to version control. We’ll store it securely in Vault in the next step.


Step 2: Secret Management

Proper secret management is critical for security. We’ll use HashiCorp Vault as our source of truth, with External Secrets Operator syncing secrets to Kubernetes.

2.1 Generate Required Secrets

First, generate all necessary secrets:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Generate admin token (used to access admin panel)
# This should be a long, random string
ADMIN_TOKEN=$(openssl rand -base64 48)
echo "Admin Token: $ADMIN_TOKEN"
# Save this in a secure location!

# Generate database password (if you haven't already)
DB_PASSWORD=$(openssl rand -base64 32)

# You'll also need SMTP credentials from your email provider
# For Gmail, use an App Password: https://myaccount.google.com/apppasswords

2.2 Store Secrets in Vault

Store all credentials in HashiCorp Vault:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Ensure you're authenticated to Vault
vault login

# Store all vaultwarden credentials in a single secret
vault kv put kv/infra/vaultwarden-credentials \
    db-url='postgresql://vaultwarden:[email protected]:5432/vaultwarden' \
    admin-token="$ADMIN_TOKEN" \
    smtp-username='[email protected]' \
    smtp-password='your-smtp-app-password'

# Verify the secret was stored
vault kv get kv/infra/vaultwarden-credentials

2.3 Configure External Secrets Operator

The External Secrets Operator will automatically sync these secrets to Kubernetes:

1
2
3
4
5
6
7
# This will be part of our Helm values (shown in next step)
externalSecrets:
  - name: vaultwarden-credentials
    refreshInterval: 5m  # Sync every 5 minutes
    secretStoreRefName: vault-backend
    targetName: vaultwarden-credentials
    dataKey: infra/vaultwarden-credentials

This creates a Kubernetes Secret named vaultwarden-credentials in the vaultwarden namespace, automatically synced from Vault.


Step 3: Kubernetes Deployment

Now we’ll deploy Vaultwarden using Helmfile and a custom Helm chart.

3.1 Directory Structure

Create the following directory structure:

1
2
3
4
5
k8s/releases/vaultwarden/
β”œβ”€β”€ helmfile.yaml
β”œβ”€β”€ values.yml
β”œβ”€β”€ network-policy.yaml (we'll create this in Step 5)
└── README.md

3.2 Helmfile Configuration

Create helmfile.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
helmDefaults:
  createNamespace: true
  timeout: 300
  wait: false

repositories:
  - name: citizix
    url: https://etowett.github.io/helm-charts

releases:
  - name: vaultwarden
    namespace: vaultwarden
    chart: citizix/app
    version: "1.3.1"
    values:
      - ./values.yml

3.3 Helm Values Configuration

Create values.yml with production-ready configuration:

  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
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
replicaCount: 1

image:
  repository: vaultwarden/server
  pullPolicy: IfNotPresent
  tag: "1.35.2-alpine"

serviceAccount:
  create: true

# ============================================
# SECURITY CONTEXT - Run as non-root user
# ============================================
podSecurityContext:
  runAsNonRoot: true
  runAsUser: 1000
  runAsGroup: 1000
  fsGroup: 1000
  seccompProfile:
    type: RuntimeDefault

securityContext:
  allowPrivilegeEscalation: false
  capabilities:
    drop:
    - ALL
  readOnlyRootFilesystem: false  # Vaultwarden needs /data write access
  runAsNonRoot: true
  runAsUser: 1000

# ============================================
# SERVICE CONFIGURATION
# ============================================
service:
  type: ClusterIP
  name: http
  port: 80
  targetPort: 8080  # Non-root port

# ============================================
# HEALTH CHECKS
# ============================================
livenessProbe:
  path: /alive
  port: http
  initialDelaySeconds: 30
  periodSeconds: 30
  timeoutSeconds: 10
  failureThreshold: 3

readinessProbe:
  path: /alive
  port: http
  initialDelaySeconds: 30
  periodSeconds: 30
  timeoutSeconds: 10
  failureThreshold: 3

# ============================================
# RESOURCE LIMITS
# ============================================
resources:
  limits:
    cpu: 1000m
    memory: 1024Mi
  requests:
    cpu: 100m
    memory: 128Mi

# ============================================
# INGRESS WITH SECURITY HARDENING
# ============================================
ingress:
  enabled: true
  className: nginx
  annotations:
    # TLS Certificate
    cert-manager.io/cluster-issuer: letsencrypt-prod-issuer

    # Force HTTPS
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    nginx.ingress.kubernetes.io/ssl-protocols: "TLSv1.2 TLSv1.3"
    nginx.ingress.kubernetes.io/ssl-ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384"

    # Security Headers
    nginx.ingress.kubernetes.io/configuration-snippet: |
      more_set_headers "X-Frame-Options: DENY";
      more_set_headers "X-Content-Type-Options: nosniff";
      more_set_headers "X-XSS-Protection: 1; mode=block";
      more_set_headers "Referrer-Policy: strict-origin-when-cross-origin";
      more_set_headers "Permissions-Policy: geolocation=(), microphone=(), camera=()";
      more_set_headers "Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';";

    # Rate Limiting
    nginx.ingress.kubernetes.io/limit-rps: "10"
    nginx.ingress.kubernetes.io/limit-burst-multiplier: "2"

    # Timeouts and Body Size
    nginx.ingress.kubernetes.io/proxy-body-size: "100m"
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "60"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "60"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "60"

  hosts:
    - host: vault.yourdomain.com  # CHANGE THIS
      paths:
        - path: /
          pathType: ImplementationSpecific
  tls:
    - secretName: vaultwarden-tls
      hosts:
        - vault.yourdomain.com  # CHANGE THIS

# ============================================
# EXTERNAL SECRETS CONFIGURATION
# ============================================
externalSecrets:
  - name: vaultwarden-credentials
    refreshInterval: 5m
    secretStoreRefName: vault-backend
    targetName: vaultwarden-credentials
    dataKey: infra/vaultwarden-credentials

# ============================================
# VAULTWARDEN ENVIRONMENT VARIABLES
# ============================================
env:
  # Domain Configuration
  DOMAIN: "https://vault.yourdomain.com"  # CHANGE THIS

  # Signup Controls - Disable public signups
  SIGNUPS_ALLOWED: "false"
  INVITATIONS_ALLOWED: "true"
  INVITATION_ORG_NAME: "Your Organization"
  SIGNUPS_DOMAINS_WHITELIST: "yourdomain.com"  # CHANGE THIS

  # Organization Features
  ORG_EVENTS_ENABLED: "true"
  ORG_GROUPS_ENABLED: "true"

  # Password Security
  SHOW_PASSWORD_HINT: "false"
  PASSWORD_HINTS_ALLOWED: "false"
  PASSWORD_ITERATIONS: "600000"  # 6x more secure than default

  # Login Rate Limiting - Prevent brute force
  LOGIN_RATELIMIT_SECONDS: "60"
  LOGIN_RATELIMIT_MAX_BURST: "10"
  ADMIN_RATELIMIT_SECONDS: "300"
  ADMIN_RATELIMIT_MAX_BURST: "3"

  # Session Security
  EXTENDED_LOGGING: "true"
  LOG_LEVEL: "info"
  REQUIRE_DEVICE_EMAIL: "true"  # Email verification for new devices

  # Two-Factor Authentication
  DISABLE_2FA_REMEMBER: "false"

  # Emergency Access
  EMERGENCY_ACCESS_ALLOWED: "true"

  # File Upload Limits
  ATTACHMENT_LIMIT: "52428800"  # 50MB
  SEND_ATTACHMENT_SIZE_LIMIT: "104857600"  # 100MB
  TRASH_AUTO_DELETE_DAYS: "30"

  # Icon Service Security - Prevent SSRF attacks
  DISABLE_ICON_DOWNLOAD: "false"
  ICON_CACHE_TTL: "2592000"
  ICON_DOWNLOAD_TIMEOUT: "10"
  ICON_BLACKLIST_NON_GLOBAL_IPS: "true"

  # Database Settings
  DB_CONNECTION_RETRIES: "15"
  DATABASE_MAX_CONNS: "10"

  # Web Server Configuration
  ROCKET_PORT: "8080"
  ROCKET_WORKERS: "10"

  # SMTP Configuration - Change these
  SMTP_HOST: "smtp.gmail.com"
  SMTP_FROM: "[email protected]"
  SMTP_FROM_NAME: "Your Organization Vault"
  SMTP_TIMEOUT: "15"
  SMTP_SECURITY: "starttls"
  SMTP_PORT: "587"
  SMTP_DEBUG: "false"
  SMTP_ACCEPT_INVALID_CERTS: "false"
  SMTP_ACCEPT_INVALID_HOSTNAMES: "false"

# ============================================
# SECRET ENVIRONMENT VARIABLES
# ============================================
secretEnv:
  DATABASE_URL:
    name: vaultwarden-credentials
    key: db-url
  ADMIN_TOKEN:
    name: vaultwarden-credentials
    key: admin-token
  SMTP_USERNAME:
    name: vaultwarden-credentials
    key: smtp-username
  SMTP_PASSWORD:
    name: vaultwarden-credentials
    key: smtp-password

3.4 Deploy Vaultwarden

Deploy the application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Navigate to the vaultwarden directory
cd k8s/releases/vaultwarden

# Deploy using helmfile
helmfile apply

# Watch the deployment
kubectl rollout status deployment/vaultwarden-app -n vaultwarden

# Check pod status
kubectl get pods -n vaultwarden

# View logs
kubectl logs -f -n vaultwarden -l app.kubernetes.io/name=vaultwarden

3.5 Verify Deployment

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Check if the pod is running
kubectl get pods -n vaultwarden

# Expected output:
# NAME                               READY   STATUS    RESTARTS   AGE
# vaultwarden-app-xxxxxxxxxx-xxxxx   1/1     Running   0          2m

# Check ingress
kubectl get ingress -n vaultwarden

# Test basic connectivity
curl -I https://vault.yourdomain.com

# Expected: HTTP/2 200 with security headers

Step 4: Security Hardening

Now let’s implement additional security hardening beyond the basic deployment.

4.1 Hash the Admin Token

For additional security, hash your admin token using Argon2:

1
2
3
4
5
6
7
8
# Get the pod name
POD_NAME=$(kubectl get pod -n vaultwarden -l app.kubernetes.io/name=vaultwarden -o jsonpath='{.items[0].metadata.name}')

# Run the hash command
kubectl exec -it $POD_NAME -n vaultwarden -- /vaultwarden hash

# Enter your admin token when prompted
# Copy the resulting hash (starts with $argon2id$)

Update Vault with the hashed token:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Update the admin token in Vault
vault kv patch kv/infra/vaultwarden-credentials \
    admin-token='$argon2id$v=19$m=65540,t=3,p=4$...'  # Your hash here

# Force External Secrets to sync
kubectl annotate externalsecret vaultwarden-credentials -n vaultwarden \
    force-sync=$(date +%s) --overwrite

# Restart vaultwarden to pick up the new token
kubectl rollout restart deployment/vaultwarden-app -n vaultwarden

Important: When accessing the admin panel, you’ll still use the original plain text token as the password, not the hash!

4.2 Enable Admin Panel IP Whitelisting (Optional)

For maximum security, restrict admin panel access to specific IP addresses:

Add this to your ingress annotations in values.yml:

1
2
3
4
5
6
nginx.ingress.kubernetes.io/server-snippet: |
  location /admin {
    allow 203.0.113.0/24;  # Your office IP range
    allow 198.51.100.50;   # Your VPN IP
    deny all;
  }

4.3 Configure Regular Backups

Set up automated database backups:

 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
# Create a CronJob for daily backups
cat <<EOF | kubectl apply -f -
apiVersion: batch/v1
kind: CronJob
metadata:
  name: vaultwarden-backup
  namespace: postgres
spec:
  schedule: "0 2 * * *"  # Daily at 2 AM
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: backup
            image: postgres:15-alpine
            command:
            - /bin/sh
            - -c
            - |
              pg_dump -h postgres-app -U postgres vaultwarden | \
              gzip > /backups/vaultwarden-\$(date +\%Y\%m\%d-\%H\%M\%S).sql.gz
              # Upload to S3 or other backup location
              # aws s3 cp /backups/vaultwarden-*.sql.gz s3://your-bucket/
            volumeMounts:
            - name: backups
              mountPath: /backups
            env:
            - name: PGPASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-credentials
                  key: password
          volumes:
          - name: backups
            persistentVolumeClaim:
              claimName: postgres-backups
          restartPolicy: OnFailure
EOF

Step 5: Network Policies

Implement Kubernetes NetworkPolicies to restrict pod-to-pod communication using the principle of least privilege.

5.1 Label Namespaces

First, ensure your namespaces have the correct labels:

1
2
3
4
5
6
7
8
# Label the ingress namespace
kubectl label namespace ingress-nginx name=ingress-nginx --overwrite

# Label the postgres namespace
kubectl label namespace postgres name=postgres --overwrite

# If using monitoring
kubectl label namespace monitoring name=monitoring --overwrite

5.2 Create Network Policy

Create network-policy.yaml:

 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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: vaultwarden-netpol
  namespace: vaultwarden
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: vaultwarden
  policyTypes:
  - Ingress
  - Egress

  # INGRESS RULES - Who can connect TO vaultwarden
  ingress:
  # Allow traffic from ingress controller
  - from:
    - namespaceSelector:
        matchLabels:
          name: ingress-nginx
    ports:
    - protocol: TCP
      port: 8080

  # Allow traffic from monitoring (optional)
  - from:
    - namespaceSelector:
        matchLabels:
          name: monitoring
    ports:
    - protocol: TCP
      port: 8080

  # EGRESS RULES - Where vaultwarden can connect TO
  egress:
  # Allow DNS resolution
  - to:
    - namespaceSelector: {}
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

  # Allow PostgreSQL database access
  - to:
    - namespaceSelector:
        matchLabels:
          name: postgres
    ports:
    - protocol: TCP
      port: 5432

  # Allow SMTP for email notifications
  - to:
    - namespaceSelector: {}
    ports:
    - protocol: TCP
      port: 587   # STARTTLS
    - protocol: TCP
      port: 465   # SMTPS

  # Allow HTTPS for icon downloads
  - to:
    - namespaceSelector: {}
    ports:
    - protocol: TCP
      port: 443
    - protocol: TCP
      port: 80

5.3 Apply Network Policy

1
2
3
4
5
6
7
8
9
# Apply the network policy
kubectl apply -f network-policy.yaml

# Verify it was created
kubectl get networkpolicy -n vaultwarden

# Test that vaultwarden still works
curl -I https://vault.yourdomain.com
# Should still return 200 OK

5.4 Verify Network Restrictions

Test that the network policy is working:

1
2
3
4
5
6
7
# This should FAIL (vaultwarden can't access other services)
kubectl run test-pod --rm -i --tty --image=busybox -- \
  wget -O- http://vaultwarden-app.vaultwarden.svc.cluster.local

# This should SUCCEED (ingress can access vaultwarden)
kubectl run test-pod -n ingress-nginx --rm -i --tty --image=busybox -- \
  wget -O- http://vaultwarden-app.vaultwarden.svc.cluster.local:8080/alive

Step 6: Monitoring & Maintenance

6.1 Set Up Logging

Monitor your vaultwarden logs for security events:

1
2
3
4
5
6
7
8
9
# View live logs
kubectl logs -f -n vaultwarden -l app.kubernetes.io/name=vaultwarden

# Check for failed login attempts
kubectl logs -n vaultwarden -l app.kubernetes.io/name=vaultwarden --since=24h | \
  grep -iE "error|failed|unauthorized|rate.?limit"

# Save logs to file for analysis
kubectl logs -n vaultwarden -l app.kubernetes.io/name=vaultwarden --since=7d > vaultwarden-audit.log

6.2 Set Up Prometheus Monitoring (Optional)

If using Prometheus Operator, add ServiceMonitor to your values.yml:

1
2
3
4
5
6
serviceMonitor:
  enabled: true
  interval: 30s
  scrapeTimeout: 10s
  labels:
    release: prometheus

Create alert rules:

 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
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: vaultwarden-alerts
  namespace: vaultwarden
spec:
  groups:
  - name: vaultwarden
    interval: 30s
    rules:
    - alert: VaultwardenDown
      expr: up{job="vaultwarden"} == 0
      for: 5m
      labels:
        severity: critical
      annotations:
        summary: "Vaultwarden is down"

    - alert: VaultwardenHighMemory
      expr: container_memory_usage_bytes{pod=~"vaultwarden.*"} / container_spec_memory_limit_bytes{pod=~"vaultwarden.*"} > 0.9
      for: 10m
      labels:
        severity: warning
      annotations:
        summary: "Vaultwarden high memory usage"

6.3 Regular Maintenance Tasks

Create a maintenance schedule:

Weekly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Review security logs
kubectl logs -n vaultwarden -l app.kubernetes.io/name=vaultwarden --since=7d | \
  grep -iE "error|failed|unauthorized" > weekly-audit.log

# Check resource usage
kubectl top pods -n vaultwarden

# Verify backups completed
kubectl get cronjob -n postgres
kubectl get jobs -n postgres

Monthly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Update vaultwarden to latest version
# Edit values.yml and change image tag
# image:
#   tag: "1.36.0-alpine"  # Update to latest

# Apply update
helmfile apply

# Monitor rollout
kubectl rollout status deployment/vaultwarden-app -n vaultwarden

# Test functionality after update
curl -I https://vault.yourdomain.com

Quarterly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Rotate admin token
NEW_TOKEN=$(openssl rand -base64 48)
vault kv patch kv/infra/vaultwarden-credentials admin-token="$NEW_TOKEN"
kubectl rollout restart deployment/vaultwarden-app -n vaultwarden

# Security scan
trivy image vaultwarden/server:1.35.2-alpine

# Test disaster recovery
# (Restore backup to test environment)

Testing Your Deployment

7.1 Functional Tests

Test basic functionality:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 1. Test web access
curl -I https://vault.yourdomain.com
# Expected: HTTP/2 200 with security headers

# 2. Test admin panel access
curl -I https://vault.yourdomain.com/admin
# Expected: HTTP/2 200 (will show login page)

# 3. Test security headers
curl -I https://vault.yourdomain.com | grep -E "X-Frame|X-Content|CSP"
# Expected: See security headers

# 4. Test rate limiting
for i in {1..15}; do
  curl -w "%{http_code}\n" -o /dev/null -s https://vault.yourdomain.com/api/accounts
done
# Expected: Should see 429 (Too Many Requests) after ~10 requests

7.2 Client Setup

Now set up Bitwarden clients to connect to your Vaultwarden instance:

Desktop/Mobile:

  1. Before logging in, tap the settings gear icon
  2. Enter your server URL: https://vault.yourdomain.com
  3. Create an account or login

Browser Extension:

  1. Install the Bitwarden browser extension
  2. Click the extension icon β†’ Settings β†’ Gear icon
  3. Set server URL: https://vault.yourdomain.com
  4. Login or create account

7.3 Admin Panel Configuration

Access the admin panel at https://vault.yourdomain.com/admin:

  1. Enter your admin token (the original plain text version)
  2. Review and configure:
    • User management
    • Organization settings
    • Email settings (test email delivery)
    • 2FA enforcement policies

Important: Enable 2FA requirement for all users:

  • Users β†’ Select user β†’ Edit β†’ Require 2FA

7.4 Create Test Vault Items

  1. Login to the web vault
  2. Create a test login item
  3. Verify it syncs to your other devices
  4. Test autofill in browser
  5. Test the Send feature (secure sharing)

Troubleshooting Common Issues

Pod Won’t Start

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Check pod status
kubectl describe pod -n vaultwarden -l app.kubernetes.io/name=vaultwarden

# Check logs
kubectl logs -n vaultwarden -l app.kubernetes.io/name=vaultwarden --previous

# Common issues:
# - Database connection failed: Check DATABASE_URL secret
# - Permission denied: Check pod security context
# - Admin token invalid: Verify ADMIN_TOKEN in secret

Can’t Access Admin Panel

1
2
3
4
5
6
7
# Verify admin token
vault kv get -field=admin-token kv/infra/vaultwarden-credentials

# Check if secret synced to Kubernetes
kubectl get secret vaultwarden-credentials -n vaultwarden -o yaml

# Verify you're using the PLAIN TEXT token, not the hash

Email Not Sending

1
2
3
4
5
6
7
8
9
# Check SMTP logs
kubectl logs -n vaultwarden -l app.kubernetes.io/name=vaultwarden | grep -i smtp

# Test SMTP connectivity from pod
kubectl exec -it vaultwarden-app-xxxx-xxx -n vaultwarden -- sh
nc -vz smtp.gmail.com 587

# For Gmail: Ensure you're using an App Password, not your regular password
# Generate at: https://myaccount.google.com/apppasswords

Rate Limiting Too Aggressive

If legitimate users are getting rate limited:

1
2
3
4
5
6
# Edit values.yml and increase limits:
LOGIN_RATELIMIT_MAX_BURST: "20"  # Increased from 10
ADMIN_RATELIMIT_MAX_BURST: "5"   # Increased from 3

# Redeploy
helmfile apply

Network Policy Blocking Traffic

1
2
3
4
5
6
# Temporarily remove to test
kubectl delete networkpolicy vaultwarden-netpol -n vaultwarden

# If that fixes it, adjust the policy
# Check namespace labels:
kubectl get namespace --show-labels

Security Best Practices Summary

Here’s a checklist of security measures we’ve implemented:

  • βœ… Pod Security: Non-root user, dropped capabilities, seccomp profile
  • βœ… Network Segmentation: NetworkPolicy restricting traffic
  • βœ… Secret Management: Vault + External Secrets Operator
  • βœ… TLS Encryption: TLS 1.2+ with strong ciphers
  • βœ… Security Headers: CSP, HSTS, X-Frame-Options, etc.
  • βœ… Rate Limiting: Both at ingress and application level
  • βœ… Password Security: 600,000 iterations (6x default)
  • βœ… Admin Token: Hashed with Argon2
  • βœ… SSRF Protection: Icon service secured
  • βœ… Audit Logging: Extended logging enabled
  • βœ… Device Verification: Email required for new devices
  • βœ… Regular Backups: Automated daily backups
  • βœ… Resource Limits: Proper CPU/memory constraints
  • βœ… Health Checks: Liveness and readiness probes

Conclusion

Congratulations! You now have a production-ready, security-hardened Vaultwarden deployment running on Kubernetes. This setup provides:

  • Complete data ownership: Your passwords live entirely on your infrastructure
  • Enterprise-grade security: Multiple layers of defense
  • High availability: Kubernetes handles pod restarts and health monitoring
  • Scalability: Easy to scale or migrate as your needs grow
  • Cost efficiency: Free alternative to premium password managers

Key Takeaways

  1. Self-hosting gives you control, but with it comes responsibility for maintenance, backups, and security
  2. Security is layered: We implemented defense in depth with pod security, network policies, rate limiting, and more
  3. Secret management matters: Using Vault + External Secrets keeps sensitive data secure
  4. Monitoring is critical: Regular log reviews and automated backups prevent disasters
  5. Keep it updated: Regular updates and security scans are essential

Next Steps

  1. Share with your team: Set up accounts and enforce 2FA
  2. Import existing passwords: Use Bitwarden’s import tool to migrate from other password managers
  3. Set up monitoring: Implement Prometheus alerts for proactive monitoring
  4. Test disaster recovery: Practice restoring from backup in a test environment
  5. Stay updated: Subscribe to Vaultwarden releases for security updates

Resources


About This Guide

This guide is based on a real production deployment running on Kubernetes, serving a team of developers and handling thousands of vault items. The configuration has been battle-tested and refined over months of operation.

If you found this guide helpful, consider sharing it with others who might benefit from self-hosting their password manager. Questions or improvements? Feel free to reach out!


Sources


Published: January 2026 Last Updated: January 18, 2026

comments powered by Disqus
Citizix Ltd
Built with Hugo
Theme Stack designed by Jimmy