Proxies & Password Pusher
Reverse proxy headers, trusted proxies, Cloudflare, and fixes for wrong URLs, HTTP 422 on login, or audit logs showing load balancer IPs.
Pro Self-Hosted? Use Behind a reverse proxy instead. Pro listens on HTTP port 80 (not 5100) and does not use OSS-only variables such as PWP__TRUSTED_PROXIES.
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 |
| Fix audit logs showing LB/proxy IPs | Audit log IPs |
| 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 |
| Audit log shows LB/private IP | Audit log IPs |
| 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 |
FORWARD_HEADERS |
Forward inbound X-Forwarded-* from an upstream proxy through Thruster to Rails |
On when TLS_DOMAIN is unset; off when TLS_DOMAIN is set |
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 · Audit log IPs · 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.
Audit logs show a private, load balancer, or proxy IP
Symptoms:
- The push audit log shows a private IP (
10.x,172.16–31.x,192.168.x) or your load balancer / reverse proxy IP - Container logs still show the real public client IP in
remote_addr - You may notice this after upgrading to v2.9.2 or later
Changed in v2.9.2: Audit logs now record the client IP via Rails request.remote_ip, which only trusts X-Forwarded-* from trusted proxies. Earlier versions stored the raw X-Forwarded-For header (often client, proxy), which viewers could spoof. That security fix is intentional; if you only see a load balancer IP after upgrading, the proxy headers are not reaching Rails correctly.
Why: The Docker image runs Thruster in front of Rails. With TLS_DOMAIN set, Thruster does not forward inbound X-Forwarded-* by default (so Rails may see only the load balancer). With TLS_DOMAIN unset, those headers are forwarded by default.
Start here
- Confirm you are behind a load balancer or reverse proxy (AWS ALB, Cloudflare, Nginx, NPM, Caddy, Traefik, etc.).
- Check whether
TLS_DOMAINis set in your Compose file or container environment. - Follow the matching branch below.
Not behind a proxy (container exposed directly with TLS_DOMAIN)? The audit IP should already be the client. Do not set FORWARD_HEADERS=1—untrusted clients could spoof X-Forwarded-Host / X-Forwarded-Proto.
Behind a proxy and TLS_DOMAIN is set
Quickest fix—enable header forwarding and restart:
environment:
FORWARD_HEADERS: '1'
FORWARD_HEADERS=1
Only enable this when a trusted upstream sets the headers. Limit network access to that proxy or load balancer.
Cleaner long-term setup: terminate TLS at the load balancer / proxy, unset TLS_DOMAIN, and point the upstream at HTTP port 5100. Thruster then forwards headers by default—no FORWARD_HEADERS override needed.
Behind a proxy and TLS_DOMAIN is unset
Header forwarding should already be on. Then:
- Confirm the proxy sets
X-Forwarded-For(see Option 1 for Nginx, NPM, Caddy, Traefik, and Apache). - If the audit IP is still the proxy/LB after that, add the proxy’s address as seen by the app to
PWP__TRUSTED_PROXIES. That is usually the private IP already showing in the audit log, or your proxy’s Docker/LAN address. Skip this when the proxy is already in a default private range—those are trusted automatically. - For Cloudflare, prefer
PWP__CLOUDFLARE_PROXY=true.
Verify
- Create or view a push and open its audit log.
- Confirm the IP is the public client address (one IP, not
user, proxy). - Optional: compare the container request log
remote_addrwith the audit IP—they should both reflect the client.
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 Set
X-Forwarded-Prototo match the browser (oftenhttps) andX-Forwarded-Hostto the public hostname. -
Trusted proxies If the proxy is not in a default private range, set
PWP__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 Run
docker 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