Proxies & Password Pusher

Reverse proxy headers, trusted proxies, Cloudflare, and fixes for wrong URLs or HTTP 422 on login.

This article applies to: OSS Self-Hosted

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

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

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.

  1. Confirm X-Forwarded-Host, X-Forwarded-Proto, X-Forwarded-Port in the proxy config.
  2. Confirm the proxy IP is trusted (or use private Docker/LAN paths that are trusted by default).
  3. Fallback: PWP__OVERRIDE_BASE_URL.

Headers not being accepted

Symptoms: Headers are set in nginx but the app still uses the backend URL.

  1. Trusted proxies—add the proxy IP if needed.
  2. Cloudflare: enable PWP__CLOUDFLARE_PROXY.
  3. 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.

  1. Outbound HTTPS to Cloudflare’s endpoints must work from the container/host.
  2. Check logs for fetch timeouts or DNS failures.
  3. 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:

  1. Referrer-Policy: no-referrer (NPM / custom headers)
    Browsers may omit Referer on POSTs; Rails can respond with 422. Use Referrer-Policy "strict-origin-when-cross-origin" or remove the header. Example: GitHub comment.

  2. Forwarded headers
    Set X-Forwarded-Proto to match the browser (often https) and X-Forwarded-Host to the public hostname.

  3. Trusted proxies
    If the proxy is not in a default private range, set PWP__TRUSTED_PROXIES.

  4. Two-factor (TOTP)
    The OTP step uses 422 while 2FA is pending—confirm you see the OTP form, not a generic error page.

  5. Logs
    Run docker compose logs (or your orchestrator’s equivalent) on the app container and capture the request around the 422.


Best practices

  1. Prefer proxy headers over PWP__OVERRIDE_BASE_URL when you can.
  2. Add trusted proxy IPs for anything outside default private ranges.
  3. With Cloudflare, use PWP__CLOUDFLARE_PROXY=true when possible.
  4. Smoke-test URL generation after changes (create a push, open the link).
  5. Multiple instances: identical PWPUSH_MASTER_KEY and SECRET_KEY_BASE.

See also