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

ComponentRole
Rocky Linux 10Hosts container runtime
PodmanRuns Vaultwarden container
IIS 10Reverse proxy + TLS termination
ARR + URL RewriteHTTP forwarding engine
SMTP serverEmail delivery
SQLiteVaultwarden database

Network Design Example

SystemExample IP
IIS Reverse Proxy192.168.10.20
Vaultwarden Host192.168.10.50
Internal Network192.168.10.0/24
Public Domainvault.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

FilePurpose
db.sqlite3Main database
rsa_key.pemEncryption 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

  1. Environment variables (container runtime)
  2. config.json (if present)
  3. internal defaults

⚠️ Critical rule

Changes to .env DO 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:

ModePortSecurity
STARTTLS587starttls
SMTPS465force_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_tls or starttls

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.

Last Update: June 23, 2026

Tagged in:

, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,