Skip to main content
Protecting Vaultwarden Behind Caddy with Cloudflare Proxy
  1. Tutorials/

Protecting Vaultwarden Behind Caddy with Cloudflare Proxy

Author
Mario
Security engineer by day, homelab tinkerer by night. Building self-hosted infrastructure and documenting the journey.
Homelab Security - This article is part of a series.
Part : This Article

Overview
#

Your password vault is arguably the most sensitive service in your homelab. Exposing Vaultwarden to the internet requires layered protection. This tutorial shows how to add Cloudflare Proxy (WAF, DDoS protection, bot management) in front of Vaultwarden while preserving real client IP logging.

What you’ll achieve:

1
2
3
4
5
6
7
8
9
        Client (real IP)
Cloudflare Edge (WAF, DDoS, Bot protection)
               ↓ CF-Connecting-IP header
 Your Firewall (geo-blocking, threat intel)
 Caddy (extracts real IP, TLS termination)
               ↓ X-Real-IP header
Vaultwarden (rate limiting, 2FA, logs real IP)

Prerequisites
#

  • Vaultwarden already running behind Caddy reverse proxy
  • Domain managed by Cloudflare (DNS)
  • Caddy with valid TLS certificates (Let’s Encrypt/ACME)
  • Basic understanding of reverse proxies

The Problem
#

When you enable Cloudflare Proxy (orange cloud), traffic flows through Cloudflare’s edge servers before reaching your origin. This provides excellent protection, but introduces two challenges:

  1. SSL handshake fails if Cloudflare tries HTTP to your origin
  2. Real client IPs are hidden — your logs show Cloudflare’s edge IP instead

Step 1: Enable Cloudflare Proxy
#

In your Cloudflare dashboard:

  1. Go to DNS → Find your Vaultwarden subdomain
  2. Toggle the cloud icon from gray (DNS only) to orange (Proxied)

At this point, you’ll likely see ERR_SSL_PROTOCOL_ERROR. Don’t panic — Step 2 fixes this.

Step 2: Set SSL/TLS Mode to Full
#

This is the most common gotcha. Cloudflare’s default “Flexible” mode tries to connect to your origin via HTTP port 80, but Caddy redirects HTTP → HTTPS, causing a protocol mismatch.

  1. Go to SSL/TLSOverview
  2. Change encryption mode to Full or Full (strict)
ModeBehaviorUse When
FlexibleCF → Origin via HTTP❌ Breaks with HTTPS origins
FullCF → Origin via HTTPS (any cert)✅ Self-signed certs
Full (strict)CF → Origin via HTTPS (valid cert)✅ Best with Let’s Encrypt

Since Caddy gets real certificates from Let’s Encrypt, use Full (strict) for maximum security.

Step 3: Configure Caddy to Trust Cloudflare IPs
#

Add Cloudflare’s IP ranges to your Caddyfile global config so Caddy knows to look for real client IPs in headers:

1
2
3
4
5
{
    servers {
        trusted_proxies static 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32
    }
}

Get current ranges from: https://www.cloudflare.com/ips/

Step 4: Forward Real Client IP to Vaultwarden
#

Here’s the second major gotcha. The typical Caddy pattern uses {remote_host}:

1
2
3
4
# WRONG for Cloudflare-proxied traffic
reverse_proxy <YOUR_BACKEND>:80 {
    header_up X-Real-IP {remote_host}  # Shows Cloudflare's IP!
}

When traffic comes through Cloudflare, {remote_host} is Cloudflare’s edge server IP (e.g., 162.158.x.x), not your actual client.

The fix: Use Cloudflare’s header directly:

1
2
3
4
# CORRECT for Cloudflare-proxied traffic
reverse_proxy <YOUR_BACKEND>:80 {
    header_up X-Real-IP {header.CF-Connecting-IP}
}

Cloudflare always sends the real client IP in CF-Connecting-IP.

Step 5: Configure Vaultwarden to Read the Header
#

Add this environment variable to your Vaultwarden configuration:

1
2
environment:
  - IP_HEADER=X-Real-IP

Now Vaultwarden will log the real client IP from the header Caddy forwarded.

Step 6: Enable Vaultwarden Security Settings
#

While you’re hardening, add these security settings:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
environment:
  # Disable public registration
  - SIGNUPS_ALLOWED=false
  - INVITATIONS_ALLOWED=true

  # Rate limiting (brute force protection)
  - LOGIN_RATELIMIT_MAX_BURST=5
  - LOGIN_RATELIMIT_SECONDS=60

  # Admin panel (leave empty to disable)
  - ADMIN_TOKEN=

  # IP header for logging
  - IP_HEADER=X-Real-IP

Verification
#

Test 1: Access Works
#

1
2
curl -I https://vault.<YOUR_DOMAIN>
# Should return HTTP/2 200

Test 2: Real IP Logging
#

  1. Access Vaultwarden from your phone on cellular (not WiFi)
  2. Check Vaultwarden logs:
    1
    
    podman logs vaultwarden | grep -i login
  3. The logged IP should be your phone’s cellular IP, not a Cloudflare IP (104.x.x.x, 162.158.x.x, 172.64.x.x)

Troubleshooting
#

SymptomCauseFix
ERR_SSL_PROTOCOL_ERRORSSL mode is FlexibleChange to Full or Full (strict)
Logs show 162.158.x.xUsing {remote_host}Change to {header.CF-Connecting-IP}
520/521/522 errorsOrigin unreachableCheck firewall allows Cloudflare IPs
525 SSL handshake failedCert issue at originVerify Caddy has valid certs

What I Learned
#

Gotcha 1: SSL Mode Matters
#

Cloudflare’s “Flexible” mode is a trap for homelabs. It assumes your origin only speaks HTTP, which made sense in 2010. With modern reverse proxies that auto-provision HTTPS, always use Full or Full (strict).

Gotcha 2: Caddy Placeholders Are Tricky
#

The difference between {remote_host} and {header.CF-Connecting-IP} isn’t obvious. The naming suggests remote_host is the remote client, but it’s actually the immediate connection — which is Cloudflare when proxied.

Gotcha 3: Mixed Proxy Configurations
#

If some domains use Cloudflare Proxy (orange cloud) and others don’t (gray cloud), you need different header configurations:

  • Orange cloud domains: {header.CF-Connecting-IP}
  • Gray cloud / internal domains: {remote_host}

Architecture Diagram
#

Vaultwarden security stack — traffic flows through Cloudflare, firewall, Caddy, then Vaultwarden with IP headers preserved end-to-end

Final Security Stack
#

LayerProtectionStatus
CloudflareWAF, DDoS, Bot management
FirewallGeo-blocking, threat intel EDLs
Vaultwarden2FA (TOTP/WebAuthn)
VaultwardenLogin rate limiting
VaultwardenPublic signups disabled
VaultwardenReal IP logging

Why Not Cloudflare Zero Trust?
#

You might wonder why we didn’t use Cloudflare Access (Zero Trust) instead. The answer: mobile apps.

Bitwarden/Vaultwarden mobile apps need to sync in the background. Cloudflare Access requires interactive authentication through a browser, which breaks app sync. Use Zero Trust for browser-only services, not password managers.

Homelab Security - This article is part of a series.
Part : This Article