Introduction
Vaultwarden is a lightweight, self-hosted alternative to Bitwarden that provides password management with minimal infrastructure overhead.
This guide demonstrates a production-ready deployment using:
- Rocky Linux 10 (container host)
- Podman (root-based container runtime)
- IIS 10 (reverse proxy + TLS termination)
- ARR + URL Rewrite
- SMTP integration
- Secure admin access model
- SELinux-compliant storage design
Unlike basic tutorials, this guide includes real operational constraints, failure modes, and production hardening practices.
Architecture Overview
Components
| Component | Role |
|---|---|
| Rocky Linux 10 | Hosts container runtime |
| Podman | Runs Vaultwarden container |
| IIS 10 | Reverse proxy + TLS termination |
| ARR + URL Rewrite | HTTP forwarding engine |
| SMTP server | Email delivery |
| SQLite | Vaultwarden database |
Network Design Example
| System | Example IP |
|---|---|
| IIS Reverse Proxy | 192.168.10.20 |
| Vaultwarden Host | 192.168.10.50 |
| Internal Network | 192.168.10.0/24 |
| Public Domain | vault.example.com |
Traffic Flow
Internet
↓ HTTPS (443)
IIS Reverse Proxy (192.168.10.20)
↓ HTTP (8000)
Vaultwarden Container (192.168.10.50)
Rocky Linux 10 Preparation
Install Podman
sudo dnf update -y
sudo dnf install podman -y
Verify:
podman --version
Vaultwarden Data Directory (CRITICAL SECTION)
Vaultwarden stores ALL persistent data in a single directory:
/vw-data
Create directory
mkdir /vw-data
chmod 700 /vw-data
What /vw-data contains
| File | Purpose |
|---|---|
| db.sqlite3 | Main database |
| rsa_key.pem | Encryption keypair |
| attachments/ | File uploads |
| sends/ | Secure file sharing |
| tmp/ | Temporary data |
⚠️ SELinux requirement (IMPORTANT)
On Rocky Linux 10, SELinux WILL block writes unless corrected.
Fix:
chcon -Rt container_file_t /vw-data
OR ensure Podman mount includes:
-v /vw-data:/data:Z
Why this matters
Without this:
- RSA key generation fails
- SQLite locks fail
- container exits on startup
Pull Vaultwarden Image
podman pull vaultwarden/server:latest
Environment Configuration
Create:
nano /vw-data/vaultwarden.env
Production Environment File
DOMAIN=https://vault.example.com
ADMIN_TOKEN=replace_with_strong_random_value
SIGNUPS_ALLOWED=false
IP_HEADER=X-Forwarded-For
SMTP_HOST=mail.example.com
SMTP_FROM=vault@example.com
SMTP_FROM_NAME=Vaultwarden
SMTP_SECURITY=force_tls
SMTP_PORT=465
SMTP_USERNAME=vault@example.com
SMTP_PASSWORD=your_password
Configuration Behavior (IMPORTANT)
Vaultwarden config precedence
- Environment variables (container runtime)
- config.json (if present)
- internal defaults
⚠️ Critical rule
Changes to
.envDO NOT apply to running containers.
You must recreate the container.
Starting Vaultwarden
podman run -d \
--name vaultwarden \
-e DOMAIN="https://vault.domain.tld" \
-v /vw-data:/data:Z \
--restart unless-stopped \
-p 127.0.0.1:8000:80 \
vaultwarden/server:latest
Container Lifecycle Reality
First run behavior
- Generates
rsa_key.pem - Initializes SQLite database
- Creates folder structure
Restart behavior
- Keeps existing data
- DOES NOT reload env changes
IIS Reverse Proxy Configuration
Required IIS Modules
- URL Rewrite
- Application Request Routing (ARR)
- WebSocket Protocol
- IP and Domain Restrictions
Enable ARR Proxy
IIS Manager → Server → Application Request Routing Cache
→ Server Proxy Settings → Enable Proxy ✔
Web.config (Production Ready)
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<!-- =========================
URL REWRITE RULES
========================== -->
<rewrite>
<rules>
<!-- BLOCK /admin except allowed subnets -->
<rule name="BlockAdmin" stopProcessing="true">
<match url="^admin/?(.*)" />
<conditions logicalGrouping="MatchAll">
<add input="{REMOTE_ADDR}"
pattern="^(172\.16\.(2[89]|3[01])\.[0-9]{1,3}|10\.20\.28\.[0-9]{1,3})$"
negate="true" />
</conditions>
<action type="CustomResponse"
statusCode="403"
statusReason="Forbidden"
statusDescription="Forbidden" />
</rule>
<!-- Reverse proxy to Vaultwarden -->
<rule name="VaultwardenReverseProxy" stopProcessing="true">
<match url="(.*)" />
<serverVariables>
<set name="HTTP_X_FORWARDED_PROTO" value="https" />
<set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
</serverVariables>
<action type="Rewrite"
url="http://192.168.10.50:8000/{R:1}" />
</rule>
</rules>
</rewrite>
<!-- =========================
CUSTOM ERROR PAGES (FIXED SAFELY)
========================== -->
<httpErrors errorMode="Custom" existingResponse="Replace">
<!-- REMOVE IIS DEFAULT 403 PAGE -->
<remove statusCode="403" />
<!-- CUSTOM 403 PAGE -->
<error statusCode="403"
path="/403.html"
responseMode="File" />
</httpErrors>
<!-- =========================
SECURITY HEADERS
========================== -->
<httpProtocol>
<customHeaders>
<add name="Strict-Transport-Security"
value="max-age=31536000; includeSubDomains; preload" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
WebSockets Requirement
Vaultwarden uses WebSockets for:
- Real-time sync
- Login session updates
- Device notifications
Without WebSockets:
- UI updates delay
- Mobile sync issues
SMTP Configuration Notes
Valid values only:
| Mode | Port | Security |
|---|---|---|
| STARTTLS | 587 | starttls |
| SMTPS | 465 | force_tls |
Invalid configuration example
SMTP_SECURITY=ssl ❌
This causes startup failure.
Admin Panel Security
Enable admin panel
ADMIN_TOKEN=strong_random_value
Access:
https://vault.example.com/admin
Security model
- Admin is NOT user-authenticated
- It is token-based only
- Must never be publicly exposed
Restricting Access (Production Model)
Recommended approach
✔ Restrict entire Vaultwarden to internal network:
172.16.28.0/22
Configured via IIS:
- IP Address and Domain Restrictions
- Deny all unspecified clients
- Allow internal subnet only
Backup Strategy
Backup command
tar czf vaultwarden-backup.tar.gz /vw-data
Restore procedure
systemctl stop podman-vaultwarden
tar xzf vaultwarden-backup.tar.gz -C /
systemctl start podman-vaultwarden
Common Failure Scenarios
1. Permission denied (rsa_key.pem)
Cause:
- SELinux blocking write
Fix:
chcon -Rt container_file_t /vw-data
2. SMTP_SECURITY crash
Cause:
- invalid value (e.g. “ssl”)
Fix:
- use
force_tlsorstarttls
3. ENV changes not applying
Cause:
- container not recreated
Fix:
podman rm -f vaultwarden
podman run ...
4. Admin panel disabled
Cause:
- missing ADMIN_TOKEN
Fix:
- set token + recreate container
Security Recommendations
- Disable public signups
- Restrict admin panel to internal network
- Enable HSTS
- Use HTTPS only at IIS
- Pin Vaultwarden version (avoid
latest) - Regular backups of
/vw-data
Final Thoughts
Vaultwarden combined with Podman and IIS provides a lightweight but production-capable password management system. However, its reliability depends heavily on:
- correct SELinux configuration
- proper container lifecycle management
- secure reverse proxy setup
- strict admin access control
When correctly configured, it is suitable for enterprise-grade internal deployments without requiring the complexity of the official Bitwarden stack.