Proxies & Password Pusher
Reverse proxy headers, trusted proxies, Cloudflare, and fixes for wrong URLs or HTTP 422 on login.
Use this guide when Password Pusher sits behind a reverse proxy (Nginx, Apache, Caddy, Traefik, Cloudflare, etc.) for TLS, load balancing, or extra security. The goals are correct public URLs, trusted forwarded headers, and safe trusted-proxy settings.
Navigate this guide
| I want to… | Go to |
|---|---|
| Pick an approach for my setup | Quick Start |
Set X-Forwarded-* headers (recommended) |
Option 1: Proxy headers |
| Use Nginx Proxy Manager (Docker) | Nginx Proxy Manager |
| Force a base URL without headers | Option 2: Override base URL |
| Trust Cloudflare or a remote proxy IP | Trusted proxies · Cloudflare |
| Run multiple app servers behind a LB | Multiple backend instances |
| Fix wrong links, ignored headers, or 422 on login | Troubleshooting |
| Copy env var names | Environment variables reference |
Quick Start
| Scenario | Recommended approach |
|---|---|
| Single proxy on same server | Proxy headers |
| Remote proxy or load balancer | Proxy headers + trusted proxies |
| Nginx Proxy Manager (NPM) | NPM section · if you see 422 on login, read this |
| Cloudflare | PWP__CLOUDFLARE_PROXY |
| Complex proxy chains | PWP__OVERRIDE_BASE_URL |
| Multiple backend instances | Shared encryption keys |
Understanding the problem
Behind a proxy, generated links can show http://localhost:5100, wrong http/https, wrong host, or wrong port because the app only sees the internal connection to the proxy.
Fix: Forward X-Forwarded-Host, X-Forwarded-Proto, and (recommended) X-Forwarded-Port so Password Pusher can rebuild the public URL the browser used.
Configuration options
Option 1: Proxy headers (recommended)
Password Pusher uses standard X-Forwarded-* headers for the original client request. Configure your proxy to set them on requests to the app.
Required headers
| Header | Description |
|---|---|
X-Forwarded-Host |
Hostname the client used |
X-Forwarded-Proto |
http or https |
X-Forwarded-Port |
Original port (optional but recommended) |
Optional but useful
| Header | Description |
|---|---|
X-Forwarded-For |
Client IP (logging, throttling) |
X-Real-IP |
Alternative client IP |
Nginx
Show Nginx example
Add these headers to your location block:
location / {
proxy_pass http://pwpush:5100;
proxy_http_version 1.1;
# Required headers
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
# Optional but recommended
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
# Connection settings
proxy_set_header Connection "";
proxy_read_timeout 3600;
}
Nginx Proxy Manager
Show Nginx Proxy Manager configuration
Nginx Proxy Manager (NPM) is a UI on top of nginx, often used with Docker. You still need the same forwarded headers so the app sees your public hostname and HTTPS.
Advanced custom configuration (proxy host → Advanced tab) should include at least:
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
If TLS termination in NPM does not set $scheme to https as expected, force it:
proxy_set_header X-Forwarded-Proto https;
Response headers (custom location / snippets): Do not use Referrer-Policy "no-referrer" on Password Pusher routes—it can trigger HTTP 422 on sign-in. Prefer strict-origin-when-cross-origin or omit it. See HTTP 422 on login.
A full working example (community-tested) is in this GitHub comment.
Example — custom location / snippet headers
proxy_hide_header X-Powered-By;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Xss-Protection "1; mode=block" always;
Apache
Show Apache example
For Apache with mod_proxy, add these headers in your VirtualHost or .htaccess:
ProxyPreserveHost On
ProxyPass / http://localhost:5100/
ProxyPassReverse / http://localhost:5100/
# Required headers
RequestHeader set X-Forwarded-Host "%{HTTP_HOST}e"
RequestHeader set X-Forwarded-Proto "%{REQUEST_SCHEME}e"
RequestHeader set X-Forwarded-Port "%{SERVER_PORT}e"
RequestHeader set X-Forwarded-For "%{REMOTE_ADDR}e"
Caddy
Show Caddy example
Caddy usually forwards proxy headers; you can set them explicitly:
pwpush.example.com {
reverse_proxy localhost:5100 {
header_up X-Forwarded-Host {host}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-Port {port}
header_up X-Real-IP {remote}
}
}
Traefik
Show Traefik example
Traefik adds forward headers automatically. Example labels:
services:
pwpush:
labels:
- "traefik.http.services.pwpush.loadbalancer.server.port=5100"
- "traefik.http.routers.pwpush.rule=Host(`pwpush.example.com`)"
- "traefik.http.routers.pwpush.entrypoints=websecure"
- "traefik.http.routers.pwpush.tls.certresolver=letsencrypt"
Traefik forwards X-Forwarded-* headers to the backend.
Option 2: Override base URL
If you cannot forward headers or need a fixed public URL, set PWP__OVERRIDE_BASE_URL (no trailing slash).
When to use
- The proxy cannot send reliable
X-Forwarded-*headers - You want one explicit URL regardless of headers
- Headers are lost in a complex chain
Note: When set, this value overrides proxy headers for generated URLs.
Configuration
# Docker Compose
environment:
PWP__OVERRIDE_BASE_URL: 'https://pwpush.example.com'
export PWP__OVERRIDE_BASE_URL='https://pwpush.example.com'
| Format | Valid? |
|---|---|
https://pwpush.example.com |
Yes |
https://pwpush.example.com:8443 |
Yes (non-standard port) |
https://pwpush.example.com/ |
No (trailing slash) |
Security: Trusted proxies
Rails only trusts X-Forwarded-* from trusted clients. Password Pusher defaults include common private ranges (RFC 1918, loopback, link-local, RFC 6598, IPv6 localhost), including Docker (172.16.0.0/12), so many LAN/Docker proxies work without extra config.
When you must add IPs
Configure PWP__TRUSTED_PROXIES (or settings.yml) when the proxy uses a public or non-default address—for example:
- Remote proxy on another machine (not in private ranges as seen by the app)
- Cloud / CDN / LB in front (see Cloudflare too)
- Any case where headers seem ignored despite correct nginx config
Risk: If trust is wrong, clients could spoof forwarded headers. Combine with network controls.
Configuration
Environment variable (recommended)
environment:
PWP__TRUSTED_PROXIES: '1.2.3.4,2.3.4.5'
PWP__TRUSTED_PROXIES='1.2.3.4'
PWP__TRUSTED_PROXIES='1.2.3.4,2.3.4.5,3.4.5.6'
settings.yml
trusted_proxies:
- '1.2.3.4'
- '2.3.4.5'
Finding proxy IP addresses
| Proxy type | How to find IPs |
|---|---|
| Cloudflare, CloudFront, etc. | Provider docs for IP ranges |
| Self-hosted | Server or container IP as seen by the app |
| Docker | Container IP on the bridge network |
| Cloud load balancers | Provider-published ranges |
Cloudflare integration
Password Pusher can fetch Cloudflare’s current IP ranges and add them as trusted proxies.
Automatic configuration (recommended)
environment:
PWP__CLOUDFLARE_PROXY: 'true'
export PWP__CLOUDFLARE_PROXY='true'
On boot, the app pulls Cloudflare’s IPv4/IPv6 ranges and trusts them. Startup may pause briefly (on the order of seconds) while fetching.
Manual configuration
Add ranges to PWP__TRUSTED_PROXIES or settings.yml. Prefer PWP__CLOUDFLARE_PROXY=true so ranges stay current.
Environment variables reference
| Variable | Description | Default |
|---|---|---|
PWP__OVERRIDE_BASE_URL |
Force base URL for generated links | None (uses headers) |
PWP__TRUSTED_PROXIES |
Comma-separated trusted proxy IPs | Private ranges (see Trusted proxies) |
PWP__CLOUDFLARE_PROXY |
Trust Cloudflare IPs automatically | false |
PWPUSH_MASTER_KEY |
DB encryption key (must match all instances) | Default key |
SECRET_KEY_BASE |
Session / Rails secret (must match all instances) | Random per deploy if unset |
Critical: Multiple backend instances
Warning: If you run more than one Password Pusher app process behind a load balancer, every instance must use the same keys.
| Variable | Role |
|---|---|
PWPUSH_MASTER_KEY |
Encrypts push payload in the database |
SECRET_KEY_BASE |
Sessions and Rails crypto |
If keys differ: decryption failures, random logouts, intermittent errors depending on which server answers.
Details: Application encryption
Troubleshooting
Quick links: Wrong URLs · Headers ignored · Cloudflare fetch fails · HTTP 422 on login
Generated URLs are incorrect
Symptoms: Links show http://localhost:5100, HTTP instead of HTTPS, or wrong host/port.
- Confirm
X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-Portin the proxy config. - Confirm the proxy IP is trusted (or use private Docker/LAN paths that are trusted by default).
- Fallback:
PWP__OVERRIDE_BASE_URL.
Headers not being accepted
Symptoms: Headers are set in nginx but the app still uses the backend URL.
- Trusted proxies—add the proxy IP if needed.
- Cloudflare: enable
PWP__CLOUDFLARE_PROXY. - Check logs to confirm headers reach the app; verify spelling/case.
Cloudflare IPs not loading
Symptoms: Cloudflare mode on but ranges not trusted; slow boot.
- Outbound HTTPS to Cloudflare’s endpoints must work from the container/host.
- Check logs for fetch timeouts or DNS failures.
- Fall back to manual IP lists in
PWP__TRUSTED_PROXIES.
HTTP 422 (Unprocessable Entity) on login
Symptoms: Sign-in returns 422, often after an upgrade, commonly with Nginx Proxy Manager or custom response headers.
Check in order:
-
Referrer-Policy: no-referrer(NPM / custom headers)
Browsers may omitRefereron POSTs; Rails can respond with 422. UseReferrer-Policy "strict-origin-when-cross-origin"or remove the header. Example: GitHub comment. -
Forwarded headers
SetX-Forwarded-Prototo match the browser (oftenhttps) andX-Forwarded-Hostto the public hostname. -
Trusted proxies
If the proxy is not in a default private range, setPWP__TRUSTED_PROXIES. -
Two-factor (TOTP)
The OTP step uses 422 while 2FA is pending—confirm you see the OTP form, not a generic error page. -
Logs
Rundocker compose logs(or your orchestrator’s equivalent) on the app container and capture the request around the 422.
Best practices
- Prefer proxy headers over
PWP__OVERRIDE_BASE_URLwhen you can. - Add trusted proxy IPs for anything outside default private ranges.
- With Cloudflare, use
PWP__CLOUDFLARE_PROXY=truewhen possible. - Smoke-test URL generation after changes (create a push, open the link).
- Multiple instances: identical
PWPUSH_MASTER_KEYandSECRET_KEY_BASE.
See also
- Installation — includes a link here for NPM / reverse proxies
- Application encryption
- Configuration strategies
- Public gateway