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.

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

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

  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.

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

  1. Confirm you are behind a load balancer or reverse proxy (AWS ALB, Cloudflare, Nginx, NPM, Caddy, Traefik, etc.).
  2. Check whether TLS_DOMAIN is set in your Compose file or container environment.
  3. 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:

  1. Confirm the proxy sets X-Forwarded-For (see Option 1 for Nginx, NPM, Caddy, Traefik, and Apache).
  2. 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.
  3. For Cloudflare, prefer PWP__CLOUDFLARE_PROXY=true.

Verify

  1. Create or view a push and open its audit log.
  2. Confirm the IP is the public client address (one IP, not user, proxy).
  3. Optional: compare the container request log remote_addr with the audit IP—they should both reflect the client.

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