Express Trust Proxy Settings for Correct Client IP and Secure Cookies Behind a Reverse Proxy
Learn the minimal Express 4.x configuration needed to trust a terminating reverse proxy, set Secure cookies, and log the real client IP while avoiding common misconfigurations.
06 Aug 2025, 05:05 UTC

When Express 4.x sits behind a TLS-terminating reverse proxy, req.ip and req.protocol default to the proxy address and "http". That breaks rate limiting, logging, and Secure cookie handling. The minimal fix is to trust exactly one hop, keep middleware order, and use a four-argument error handler.
Requirements
The application must:
- Report the original client IP address in
req.ipfor logging and rate‑limiting decisions. - Set
SecureandHttpOnlyflags on cookies only when the original request was made over HTTPS, even though Express sees the connection as HTTP from the proxy. - Operate safely when deployed behind a trusted reverse proxy that terminates TLS and forwards standard
X-Forwarded-For,X-Forwarded-Proto, andX-Forwarded-Hostheaders.
Express 4.x does not inspect these headers by default; without configuration req.ip equals the proxy’s IP and req.protocol equals "http".
Minimal Suitable Design
The smallest set of Express constructs that satisfies the requirements is:
- Create the Express application.
- Configure trust for the first hop:
app.set('trust proxy', 1). - Add standard middleware in this order:
- Body parsers (
express.json(),express.urlencoded()). - Security helpers (e.g.,
helmet()if desired). - Application routes.
- Error‑handling middleware with four parameters (
function(err, req, res, next) { … }) placed after all routes.
- Body parsers (
- Start the server on a chosen port.
Example code (placeholders indicate values you must supply):
const express = require('express');
const app = express();
// Trust the immediate proxy (the first hop)
app.set('trust proxy', 1);
// Body parsing – must come before routes
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// Example route
app.get('/api/hello', (req, res) => {
// req.ip now reflects the original client IP
// req.protocol reflects the original scheme (http or https)
res.json({ message: 'hello', clientIp: req.ip, protocol: req.protocol });
});
// Error‑handling middleware – four args, placed after routes
app.use((err, req, res, next) => {
// In production avoid sending stack traces
const status = err.status || 500;
res.status(status).json({ error: err.message || 'Internal Server Error' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Listening on ${PORT}, trust proxy = ${app.get('trust proxy')}`);
});
No additional frameworks are required for this baseline.
Trust and Data Boundaries
The reverse proxy is the only entity allowed to connect directly to the Express process. Therefore:
- Headers such as
X-Forwarded-For,X-Forwarded-Proto, andX-Forwarded-Hostare considered untrusted input unless the connecting peer is explicitly trusted viatrust proxy. - Internal services that sit behind the Express app must never rely on those headers; they should treat the Express app as the source of truth for client IP and protocol.
- Network‑level isolation (firewall, security groups, or VPC) should enforce that only the proxy’s IP address can reach the Express listener on its port.
Operational Checks
To verify that the configuration behaves as expected, perform the following checks in a staging or production‑like environment:
- Log
req.ipandreq.protocolat the start of each request (or sample a fraction) and confirm they match the original client address and scheme when a request traverses the proxy. - Make an HTTPS request through the proxy and inspect the response’s
Set-Cookieheader; theSecureflag should be present only whenreq.protocol === 'https'. - Expose a lightweight health endpoint (e.g.,
GET /health) that returns200 OK; use it to ensure the process is reachable after a restart. - At startup, log
app.get('trust proxy')and compare the value to your deployment topology (number of trusted hops). If the app is running behind a proxy but the value isfalseorundefined, raise an alert.
These checks are observational; they do not guarantee correctness but help detect misconfiguration early.
Failure Modes
- Over‑trusting the proxy: Setting
trust proxytotrueor a numeric value larger than the actual hop count allows a malicious client to spoofX-Forwarded-ForandX-Forwarded-Proto. This can lead to incorrect rate‑limiting decisions and cookies being markedSecurewhen they were sent over plain HTTP. - Under‑trusting the proxy: Leaving the default (
false) or setting a value of0causes Express to ignore the proxy headers, soreq.ipreflects the proxy’s address andreq.protocolstays "http". Secure cookies will not be set, and logging will show the proxy IP instead of the client. - Missing four‑argument error handler: If error‑handling middleware is defined with fewer than four parameters or placed before routes, Express treats it as regular middleware. Uncaught errors will cause the request to hang, eventually timing out.
- Changing the proxy without updating trust settings: Removing the reverse proxy or inserting an additional layer (e.g., a sidecar) without adjusting
trust proxybreaks the IP and protocol logic, producing the same symptoms as over‑ or under‑trusting.
Conditions That Change the Design
Revisit the trust configuration when any of the following occurs:
- The deployment uses more than one trusted hop (e.g., a load balancer in front of a TLS‑terminating proxy). In that case set
app.set('trust proxy', <number of hops>)or provide an array/CIDR list that matches the trusted proxies. - You need to validate or sanitize the forwarded headers yourself (e.g., reject non‑numeric values in
X-Forwarded-For). This moves validation into application code rather than relying on Express’ built‑in trust. - The environment adopts mutual TLS (mTLS) or a service mesh that terminates TLS at the mesh sidecar; the trust boundary may shift, requiring a review of which headers are considered trustworthy.
- Upgrading to Express 5 (when stable) changes middleware signature expectations and error‑handling behavior; the four‑arg handler remains required, but its placement and error propagation semantics should be re‑examined.
Each trigger warrants a reassessment of the trust proxy value, middleware order, and any custom header validation you might add.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.