Hardens the IP allowlist in require_auth against the two bypasses filed in #3. 1. Bypass via missing X-Forwarded-For (no IP to check, allowlist skipped). 2. Spoofing via attacker-controlled X-Forwarded-For (header trusted unconditionally). Resolves both by deriving the client IP from the socket peer (ConnectInfo<SocketAddr>) and only honoring X-Forwarded-For when the immediate peer is in a new security.trusted_proxies allowlist (default empty = strict). Fails closed with 403 forbidden_ip when a non-empty allowlist is configured and the client IP cannot be determined. Empty ip_whitelist continues to mean allow all (preserved for dev installs). 27 pm-auth tests pass (12 new resolver + 8 new middleware + 7 existing). Spec: tasks/ip-allowlist-spec.md.
5.2 KiB
Reverse Proxy Deployment Runbook
Audience: Operators deploying pm-web behind a reverse proxy (nginx,
HAProxy, Cloudflare, AWS ALB, etc.).
Related:
docs/security-review.md§1.3 (IP Whitelist Enforcement)tasks/ip-allowlist-spec.md§7 (Risk Analysis)- Issue #3
TL;DR
If you front pm-web with a reverse proxy, you MUST add the proxy's
IP address (or CIDR) to security.trusted_proxies in
/etc/patch-manager/config.toml. If you do not, the IP allowlist will
evaluate against the proxy's IP (not the real client) and will return
403 forbidden_ip for legitimate traffic.
Why
Starting with the IP-allowlist hardening in issue #3, pm-web no longer
trusts X-Forwarded-For by default. The default behavior is strict:
- The server reads the socket peer IP from
ConnectInfo<SocketAddr>. - The server checks that IP against
security.ip_whitelist. X-Forwarded-Foris ignored unless the socket peer is insecurity.trusted_proxies.
When you put a reverse proxy in front, every connection's socket peer IP
is the proxy's address. Without trusted_proxies set, the proxy's IP is
checked against your allowlist — and unless your allowlist happens to
include the proxy (which would defeat the purpose of the allowlist),
the request is denied.
How to Fix
-
Identify the egress IP of your reverse proxy (the IP
pm-websees as the immediate TCP peer). This is typically:- nginx: the IP nginx binds to internally, or the host's IP if nginx
runs on the same host as
pm-web(port forward). - Cloudflare: see Cloudflare IP ranges.
- AWS ALB / NLB: the ALB/NLB's private IP from the VPC.
- HAProxy: the bind address.
- nginx: the IP nginx binds to internally, or the host's IP if nginx
runs on the same host as
-
Add the IP (or CIDR for multiple hops) to
trusted_proxiesin/etc/patch-manager/config.toml:[security] ip_whitelist = ["10.0.0.0/8"] # example: corporate clients trusted_proxies = ["172.16.5.10/32"] # example: reverse proxy egress -
Restart
pm-webfor the config to take effect. Thetrusted_proxiesfield is read at startup; runtime updates are supported viaAuthConfig::update_trusted_proxiesbut not yet exposed through a settings endpoint. -
Verify by tailing the logs and confirming that requests with
X-Forwarded-For: <allowed-client-ip>succeed (status 200/401, NOT 403) when the request comes through the proxy.
Multi-hop Proxy Chains
If you have multiple proxies in front of pm-web (e.g., Cloudflare →
nginx → pm-web), add each hop you control to trusted_proxies:
trusted_proxies = [
"172.16.5.10/32", # nginx egress (immediate peer)
"10.0.0.0/8", # internal network (in case nginx runs on a different host)
]
The resolver picks the leftmost entry of X-Forwarded-For when the
immediate peer is in trusted_proxies. With two trusted hops, the
resolver will pick the leftmost untrusted IP (the real client).
Reverse Proxy Headers (recommended)
In addition to the trusted_proxies config, configure your reverse
proxy to:
- Append to
X-Forwarded-For(not replace) so the chain is preserved through multiple hops. - Set
X-Real-IP(optional, informational; pm-web currently usesX-Forwarded-For). - Forward the original
Hostheader so SAML/OIDC redirects work correctly. - Do not strip the
Authorizationheader.
nginx example
location /api/ {
proxy_pass http://127.0.0.1:12443;
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;
}
The proxy_add_x_forwarded_for directive appends, which is what you want.
Troubleshooting
All requests return 403 forbidden_ip
- Check that
trusted_proxiesis set and contains the proxy's IP. - Check that the proxy's IP is correct (run
ss -tnpon the pm-web host to see the actual peer address). - Check
tracinglogs forreason = "unresolvable_client_ip"— this means theConnectInfo<SocketAddr>extension is missing (the listener wasn't built withinto_make_service_with_connect_info).
XFF is being ignored
- Check that the immediate peer's IP is in
trusted_proxies. If the immediate peer is NOT intrusted_proxies, XFF is ignored (correct behavior). - Check the XFF format: pm-web parses the leftmost entry, trimmed of whitespace. A malformed leftmost entry falls back to the socket peer.
Multiple IPs in XFF and only the last hop is trusted
- If you have one trusted proxy and one untrusted, the resolver will only use XFF when the immediate peer (the trusted one) is in the list. The XFF is parsed leftmost-first, so the real client IP (leftmost untrusted hop) is used.
- If neither hop is in
trusted_proxies, XFF is ignored and the socket peer IP (the immediate proxy) is used. Add the immediate proxy totrusted_proxiesto fix.
See Also
config/config.example.toml— inline documentation ontrusted_proxies.tasks/ip-allowlist-spec.md§3 (Design Decisions) for the rationale.crates/pm-auth/src/rbac.rs— the resolver implementation.