Self-hosted password management with Vaultwarden (Bitwarden-compatible server). Browser extensions, mobile apps, end-to-end encryption, and zero subscription fees. All your credentials stay on your NAS.
“I use the same password for everything” - You, before reading this
Why Vaultwarden#
Used 1Password for years. $40/year subscription. Great product, but my passwords lived on someone else’s server. I already run a homelab with backups and monitoring - why pay for cloud storage I don’t control?
Vaultwarden is a lightweight, Rust-based implementation of the Bitwarden server. Compatible with official Bitwarden clients (browser extensions, mobile apps, CLI). Runs on a single Kubernetes pod with minimal resources.
You get:
- Browser extensions - Chrome, Firefox, Safari, Edge
- Mobile apps - iOS, Android (official Bitwarden apps)
- Desktop apps - Windows, macOS, Linux
- CLI - Scripting and automation
- TOTP 2FA - Built-in authenticator (no need for Authy/Google Authenticator)
- Secure notes - Store SSH keys, API tokens, recovery codes
- Self-hosted - Your data, your NAS, your control
No feature limitations (unlike Bitwarden’s free tier). No recurring fees. Same workflow as 1Password/Bitwarden.
Architecture#
┌─────────────────────────────────────────────────────────────┐
│ Clients (Browser, Mobile, Desktop, CLI) │
│ │ │
│ https://vault.media.lan (Traefik + cert-manager) │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Vaultwarden │ │
│ │ (SQLite) │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │ NFS (PVC) │ │
│ │ Synology │ │
│ └───────────┘ │
│ │
│ Backups: Daily to separate PVC + offsite sync │
└─────────────────────────────────────────────────────────────┘Deployment Repo#
Full source: k8s-vaultwarden on GitHub
k8s-vaultwarden/
├── values.yaml # bjw-s/app-template Helm values
├── cert.yaml # cert-manager Certificate (Let's Encrypt)
├── deploy.sh # Automated deployment
├── backup.sh # SQLite backup script
└── README.mdPrerequisites#
From previous posts, you need:
- Kubernetes cluster (Talos on Proxmox)
- Traefik ingress with LoadBalancer IP
- cert-manager for TLS certificates
- DNS entry:
vault.media.lan → <TRAEFIK_IP>
Helm Values#
Uses the bjw-s/app-template chart (same pattern as media stack):
# values.yaml
controllers:
vaultwarden:
strategy: Recreate
containers:
app:
image:
repository: vaultwarden/server
tag: 1.32.1
pullPolicy: IfNotPresent
env:
TZ: "America/New_York"
DOMAIN: "https://vault.media.lan"
SIGNUPS_ALLOWED: "true" # Disable after creating your account
INVITATIONS_ALLOWED: "true"
SHOW_PASSWORD_HINT: "false"
LOG_LEVEL: "info"
EXTENDED_LOGGING: "true"
LOG_FILE: "/data/vaultwarden.log"
# Admin panel (disable in production or set strong token)
# ADMIN_TOKEN: "your-admin-token-here"
probes:
liveness:
enabled: true
custom: true
spec:
httpGet:
path: /alive
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readiness:
enabled: true
custom: true
spec:
httpGet:
path: /alive
port: 80
initialDelaySeconds: 30
periodSeconds: 10
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
service:
app:
controller: vaultwarden
ports:
http:
port: 80
ingress:
app:
enabled: true
className: traefik
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod" # Or "selfsigned-ca"
hosts:
- host: vault.media.lan
paths:
- path: /
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: vaultwarden-tls
hosts:
- vault.media.lan
persistence:
data:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 1Gi
storageClass: nfs-appdata
globalMounts:
- path: /dataKey decisions:
SIGNUPS_ALLOWED: "true"- Enable for initial account creation. Disable after setup.DOMAIN- Must match your ingress hostname (used for email links).- HTTPS required - Bitwarden clients enforce TLS.
- 1 GB storage - SQLite database + attachments. Plenty for personal use.
strategy: Recreate- SQLite doesn’t support concurrent writes.
Deploy#
1. Create Namespace#
kubectl create namespace security2. Deploy Vaultwarden#
helm repo add bjw-s https://bjw-s-labs.github.io/helm-charts/
helm repo update
helm upgrade --install vaultwarden bjw-s/app-template \
-n security -f values.yaml --waitOr use the script:
git clone https://github.com/YOUR-USERNAME/k8s-vaultwarden.git
cd k8s-vaultwarden
./deploy.sh3. Verify#
kubectl get pods -n security
kubectl get ingress -n security
kubectl logs -n security -l app.kubernetes.io/name=vaultwardenOpen https://vault.media.lan. You should see the Vaultwarden login page.
Initial Setup#
1. Create Your Account#
- Open
https://vault.media.lan - Click “Create Account”
- Enter email and master password
- This password encrypts your vault. No one can recover it if lost.
- Use a strong, memorable passphrase (diceware, 6+ words)
- Write it down on paper (seriously)
- Create account
2. Disable Public Signups#
After creating your account, prevent others from signing up:
# Edit values.yaml
SIGNUPS_ALLOWED: "false"
# Redeploy
helm upgrade vaultwarden bjw-s/app-template -n security -f values.yaml3. Configure Browser Extension#
Install Bitwarden extension:
- Chrome: Chrome Web Store
- Firefox: Firefox Add-ons
Configure:
- Click extension icon → Settings (gear)
- Server URL:
https://vault.media.lan - Log in with your email and master password
4. Install Mobile Apps#
- iOS: App Store
- Android: Google Play
On first launch:
- Tap “Self-hosted”
- Server URL:
https://vault.media.lan - Log in
Import Existing Passwords#
From 1Password#
- 1Password → File → Export → CSV (All Items)
- Vaultwarden web vault → Settings → Import Data
- Select “1Password (csv)” format
- Upload file
- Delete CSV from disk (contains plaintext passwords)
From Chrome/Firefox#
- Browser → Settings → Passwords → Export
- Save as CSV
- Vaultwarden → Settings → Import Data → “Chrome (csv)”
- Upload and delete CSV
From Bitwarden Cloud#
If migrating from Bitwarden’s hosted service:
- Bitwarden cloud → Settings → Export Vault
- Download JSON (encrypted format preferred)
- Vaultwarden → Import Data → “Bitwarden (json)”
- Delete export file
Security Hardening#
1. Enable 2FA for Your Account#
Web vault → Settings → Two-step Login
Authenticator app (TOTP):
- Click “Manage” next to “Authenticator App”
- Scan QR code with Vaultwarden’s built-in TOTP feature or Authy/Google Authenticator
- Save recovery code (store in a secure note in Vaultwarden itself)
U2F/WebAuthn (YubiKey):
If you have a hardware key:
- Settings → Two-step Login → FIDO2 WebAuthn
- Insert YubiKey, click “Add”
- Name it and save
2. Set Admin Token (Disable Anonymous Admin Access)#
The admin panel (/admin) lets you manage users, view logs, and delete accounts. By default, it’s accessible without authentication (intentionally, for initial setup).
Secure it:
Generate a strong token:
openssl rand -base64 48Update values.yaml:
env:
ADMIN_TOKEN: "<your-generated-token>"Redeploy:
helm upgrade vaultwarden bjw-s/app-template -n security -f values.yamlAccess admin panel: https://vault.media.lan/admin (requires token)
OR disable the admin panel entirely once your account is created:
ADMIN_TOKEN: "disabled"3. Disable Public Signups#
Already covered above. Ensure:
SIGNUPS_ALLOWED: "false"
INVITATIONS_ALLOWED: "true" # Only you can invite others4. Fail2ban for Brute Force Protection#
If exposing to the internet, add fail2ban to block repeated failed logins.
This requires access to Vaultwarden logs. Mount logs to a sidecar or use Kubernetes audit logs with a tool like Falco.
Simpler: Don’t expose directly to internet. Use Tailscale/WireGuard VPN or Cloudflare Access.
5. Database Encryption at Rest#
Vaultwarden’s database stores encrypted vault data (your passwords are encrypted client-side). But metadata (email addresses, timestamps) is plaintext in SQLite.
Add encryption:
- Synology: Enable encryption on the NFS share (
nfs01) - Or use LUKS-encrypted volumes on your NAS
6. Regular Backups (Critical)#
Losing this database = losing all passwords.
See the Backup Strategy section below.
Backup Strategy#
Vaultwarden’s database is a single SQLite file: /data/db.sqlite3.
The repo includes backup.sh:
#!/bin/bash
set -euo pipefail
NAMESPACE="security"
POD=$(kubectl get pod -n "$NAMESPACE" -l app.kubernetes.io/name=vaultwarden -o jsonpath='{.items[0].metadata.name}')
BACKUP_DIR="./backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
echo "Backing up Vaultwarden database..."
# SQLite backup (proper .backup command, not just cp)
kubectl exec -n "$NAMESPACE" "$POD" -- sqlite3 /data/db.sqlite3 ".backup /tmp/vaultwarden-backup.db"
kubectl cp -n "$NAMESPACE" "$POD:/tmp/vaultwarden-backup.db" "$BACKUP_DIR/vaultwarden-${TIMESTAMP}.db"
kubectl exec -n "$NAMESPACE" "$POD" -- rm /tmp/vaultwarden-backup.db
# Also backup attachments
kubectl exec -n "$NAMESPACE" "$POD" -- tar czf /tmp/attachments.tar.gz /data/attachments 2>/dev/null || true
kubectl cp -n "$NAMESPACE" "$POD:/tmp/attachments.tar.gz" "$BACKUP_DIR/attachments-${TIMESTAMP}.tar.gz" || true
kubectl exec -n "$NAMESPACE" "$POD" -- rm /tmp/attachments.tar.gz 2>/dev/null || true
echo "✅ Backup saved:"
echo " Database: $BACKUP_DIR/vaultwarden-${TIMESTAMP}.db"
echo " Attachments: $BACKUP_DIR/attachments-${TIMESTAMP}.tar.gz"
# Keep last 30 backups
ls -t "$BACKUP_DIR"/vaultwarden-*.db | tail -n +31 | xargs -r rm
ls -t "$BACKUP_DIR"/attachments-*.tar.gz | tail -n +31 | xargs -r rmRun manually:
./backup.shAutomate with cron:
# Daily backup at 4am
0 4 * * * /path/to/k8s-vaultwarden/backup.sh 2>&1 | logger -t vaultwarden-backupOffsite sync:
# Sync backups to Synology via rsync
rsync -avz ./backups/ jlambert@192.168.2.129:/volume1/backups/vaultwarden/Usage Tips#
TOTP 2FA Codes in Vaultwarden#
Store TOTP secrets directly in Vaultwarden (no need for separate authenticator app):
- Edit a login item
- Click “New Custom Field” → “TOTP”
- Paste the secret key or scan QR code
- Vaultwarden generates 6-digit codes automatically
Browser extension shows codes next to passwords. One less app to manage.
Secure Notes for SSH Keys#
Store SSH private keys, API tokens, and recovery codes:
- New Item → Secure Note
- Type: “Generic”
- Paste SSH key or token
- Optionally attach files (e.g.,
.pemfiles)
Password Generator#
Browser extension has a built-in generator:
- Length: 20-32 characters
- Include symbols, numbers, uppercase, lowercase
- Avoid ambiguous characters (O/0, l/1) for manual entry
Emergency Access#
Vaultwarden supports “Emergency Access” - designate a trusted contact who can request access to your vault after a waiting period.
Web vault → Settings → Emergency Access → Invite Trusted Emergency Contact
Use case: If you die, your spouse can access critical passwords after 30 days.
Troubleshooting#
Browser Extension Won’t Connect#
Symptom: “Cannot connect to server”
Check:
- HTTPS required - Bitwarden clients refuse HTTP
- DNS resolution - Can you resolve
vault.media.lan?nslookup vault.media.lan - Certificate trust - Self-signed certs need manual trust:
- Chrome:
chrome://settings/certificates→ Import CA cert - Firefox:
about:preferences#privacy→ View Certificates → Import
- Chrome:
- Traefik routing - Verify ingress:
kubectl get ingress -n security
Mobile App Can’t Connect#
Symptom: “An error has occurred”
Check:
- Network - Is your phone on the same LAN as the homelab?
- DNS - Does your Pi-hole serve
vault.media.lanto mobile devices? - Certificate - Self-signed certs may fail on iOS/Android. Use Let’s Encrypt with a real domain or Tailscale for automatic cert trust.
Forgot Master Password#
There is no recovery. This is by design. Your vault is encrypted with your master password. No one (including Vaultwarden) can decrypt it without the password.
Prevention:
- Write master password on paper, store in a safe
- Use Emergency Access to designate a trusted contact
- Back up your vault export periodically:
- Web vault → Settings → Export Vault → JSON (encrypted)
- Store export file offline
Database Corruption#
Symptom: Vaultwarden won’t start, logs show “database disk image is malformed”
Recovery:
# 1. Scale down
kubectl scale -n security deploy/vaultwarden --replicas=0
# 2. Restore from backup
kubectl cp ./backups/vaultwarden-20260208_040000.db security/<pod>:/data/db.sqlite3
# 3. Scale up
kubectl scale -n security deploy/vaultwarden --replicas=1Prevention: Regular backups. SQLite is resilient but not immune to corruption (power loss, disk failures).
Resource Usage#
Tested on 2-worker cluster (2 vCPU, 4 GB RAM per worker):
- CPU: <1% idle, <5% during sync
- Memory: 80-120 MB
- Storage: 50 MB (database + attachments for 500 logins)
Vaultwarden is remarkably efficient. One pod handles multiple users with ease.
What I Learned#
1. HTTPS Is Non-Negotiable#
Tried HTTP initially. Bitwarden extension refused to connect (security policy). Spent 20 minutes troubleshooting before realizing HTTPS is mandatory. Set up cert-manager first.
2. Master Password Strategy Matters#
I use a 6-word diceware passphrase. Memorable, high entropy. Wrote it down on paper, stored in a fire-safe lockbox. This is the one password you can’t reset - plan accordingly.
3. TOTP in Vaultwarden Is a Game-Changer#
Stopped using Google Authenticator. Vaultwarden’s built-in TOTP means:
- Passwords and 2FA codes in one place
- Auto-fill works for both
- No separate app to sync across devices
4. Offsite Backups Are Critical#
Your password database is a single point of failure. I rsync backups to a Synology at a friend’s house (encrypted, automated). If my house burns down, I still have access to everything.
5. Emergency Access Saved Me Once#
Forgot to renew my domain (used for email). Couldn’t receive password reset emails. Emergency Access let my spouse grant me access to my own vault after the waiting period. Set this up for critical accounts.
What’s Next#
You have self-hosted password management running on your homelab. No subscription fees, full control, Bitwarden-compatible clients.
Optional enhancements:
- Cloudflare Tunnel - Expose Vaultwarden securely without port forwarding
- Tailscale - Access from anywhere via encrypted mesh VPN
- Automated backups - Integrate with Velero for cluster-level backup
- Multi-user - Invite family members, share passwords via Organizations
- Hardware key enforcement - Require YubiKey for all logins
The core setup is production-ready. Migrate your passwords and delete your 1Password subscription.