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.
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