Behind Proxies

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Express 5.3 (npm express 5.3.0, published 2026-10-09) on Node.js 18 or later, the minimum for Express 5, and it states where Express 4 (latest 4.22.3) behaves differently. Everything was checked against the official documentation and the package repositories on 2026-10-29. In production an Express application almost never faces the Internet directly: a reverse proxy such as Nginx, a cloud load balancer, a CDN or a Kubernetes ingress terminates TLS, spreads the load and forwards plain HTTP to Node.js. From the application’s point of view every request then comes from the proxy, over HTTP, addressed to an internal host name. The trust proxy setting tells Express when it may believe the X-Forwarded-* headers that the proxy adds to recover the real client address, protocol and host. Get it wrong in one direction and logs, rate limits and secure cookies break; get it wrong in the other and any client can forge its own address. The examples use ESM and the node: prefix for built-in modules, belong to the Bookshelf API that grows across this section, and contain placeholders only, never real secrets. The settings table is in Application & Settings, the request properties in Request, and the wider hardening checklist in Security Best Practices.

Why Express needs to be told

Express derives several request properties from the TCP connection:

  • req.ip is the remote address of the socket.

  • req.protocol is https when the socket is encrypted and http otherwise.

  • req.hostname comes from the Host header.

Behind a proxy all three describe the proxy, not the client. The socket address is the proxy’s internal address, the socket is plain HTTP because the proxy terminated TLS, and the Host header may have been rewritten to the name of the internal upstream. The proxy usually passes the original values along in headers, but a header is only as trustworthy as the machine that wrote it. An HTTP header sent by a browser, a script or an attacker looks exactly like one added by your Nginx. Express therefore ignores the forwarding headers by default and uses them only for connections that come from a proxy you declared as trusted.

flowchart TD A["Request arrives on the socket"] --> B{"Is the socket peer a trusted proxy?"} B -->|"no (default: trust proxy is false)"| C["Use the socket as it is: req.ip is the peer, req.protocol from the TLS state, req.hostname from Host"] B -->|"yes"| D["Read X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host"] D --> E["proxy-addr walks X-Forwarded-For from the right and stops at the first address that is not trusted"] E --> F["req.ip is that address, req.ips lists the chain, req.protocol and req.hostname use the forwarded values"]

The forwarding headers

No standard obliges a proxy to send anything, but three de facto headers are used by Nginx, HAProxy, Apache, Envoy, the cloud load balancers and ingress controllers. Express reads exactly these:

Header Meaning Used by Express for

X-Forwarded-For

A comma-separated list of addresses. Each proxy appends the address of the peer it received the request from, so the list reads client first, the most recent proxy last. The last proxy’s own address is not in the list: it is the socket address of the connection that reached Express.

req.ip and req.ips

X-Forwarded-Proto

The protocol the client used with the first proxy, http or https.

req.protocol and therefore req.secure

X-Forwarded-Host

The Host header the client sent to the first proxy.

req.host, req.hostname and req.subdomains

Three more details avoid common confusion:

  • Node.js joins repeated header lines with a comma, so a request carrying two X-Forwarded-For lines is read as one list, in the order the lines were received.

  • When X-Forwarded-Proto or X-Forwarded-Host contains several comma-separated values (several proxies each appended one), Express uses the first, the one closest to the client. A single leading value can still have been written by the client when the first proxy does not overwrite it.

  • X-Forwarded-Port, X-Forwarded-Prefix and the standard Forwarded header (RFC 7239) are not interpreted by Express. proxy-addr reads only X-Forwarded-For. If a proxy emits Forwarded only, translate it at the proxy (Nginx and HAProxy can do so) or read it in your own middleware.

A request through a proxy chain

The figure follows one request through a CDN and an Nginx reverse proxy to Express, with the headers each hop adds and the result of three different trust proxy values.

A client request passing a CDN, an Nginx reverse proxy and Express, with the X-Forwarded headers added by each hop and the resulting req.ip for trust proxy values of false, 1 and 2

proxy-addr builds the address list from the socket address followed by the X-Forwarded-For entries read from right to left, so index 0 is always the machine that opened the connection to Express, index 1 the one before it, and so on. Trust is then evaluated hop by hop from the application outwards. The first untrusted address ends the walk and is reported as the client. When every address is trusted, the leftmost entry of the header wins, which is exactly why trusting too much is dangerous.

The trust proxy setting

trust proxy is an application setting changed with app.set(), app.enable() or app.disable() before the first request. Express compiles the value once, with proxy-addr, into a function (address, index) => boolean and stores it as the internal setting trust proxy fn. The default is false: nothing is trusted. The setting accepts four kinds of value:

Value Meaning When to use

false (default)

No proxy is trusted. The forwarding headers are ignored.

The application is reachable directly, with no proxy in front.

true

Every hop is trusted: the client is the leftmost entry of X-Forwarded-For, and X-Forwarded-Proto and X-Forwarded-Host are always believed.

Almost never in production. Only when it is certain that the application cannot be reached except through a proxy that overwrites the header, and even then a hop count is more precise.

A number n

Trust the n hops closest to the application. With 1, only the machine that opened the connection to Express is trusted, and req.ip is the address it reported in X-Forwarded-For. With 2 the client is the entry two hops away. 0 trusts nothing, like false.

A fixed, known number of proxies, for example one Nginx, or a CDN plus a load balancer. The number must match the real chain and must be updated when a proxy is added.

An IP address, a subnet, a list of them, or a preset name

A string with comma-separated entries, or an array of strings. Each entry is an IPv4 or IPv6 address, a CIDR subnet (10.0.0.0/8), an address with a netmask (10.0.0.0/255.0.0.0) or one of the presets listed below. An address is trusted when it matches any entry.

The proxies have known, stable addresses or live in a known private network. This is the most robust choice.

A function (address, index) => boolean

Called for each hop with the address and its distance from the application (0 is the socket peer). The result decides whether the hop is trusted.

Dynamic or large address lists, for instance the published ranges of a CDN, or when a decision needs more than a prefix match.

The preset names are shortcuts for ranges that private networks use:

Name Addresses

loopback

127.0.0.1/8 and ::1/128

linklocal

169.254.0.0/16 and fe80::/10

uniquelocal

10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and fc00::/7

An IPv4 address that the socket reports as an IPv4-mapped IPv6 address (::ffff:10.0.0.5) is matched against IPv4 entries as well, so a single 10.0.0.0/8 entry covers both representations. An unparsable value throws a TypeError at the moment the setting is applied, so a typo fails at start-up rather than silently trusting nothing.

// trust-proxy-values.mjs
import { BlockList } from 'node:net';
import express from 'express';

const app = express();

// 1. Boolean. Trusts everything; avoid in production.
// app.set('trust proxy', true);

// 2. Hop count. Exactly one reverse proxy sits in front of the app.
// app.set('trust proxy', 1);

// 3. Presets, addresses and subnets, as a comma-separated string or an array.
// app.set('trust proxy', 'loopback, linklocal, uniquelocal');
// app.set('trust proxy', ['loopback', '10.0.1.0/24', 'fd00::/8']);

// 4. A function: here the proxies are the members of a block list, such as the ranges of a CDN.
const proxies = new BlockList();
proxies.addSubnet('10.0.0.0', 8, 'ipv4');
proxies.addAddress('203.0.113.10', 'ipv4');

app.set('trust proxy', (address, index) => {
  const ipv4 = address.startsWith('::ffff:') ? address.slice('::ffff:'.length) : address;
  return proxies.check(ipv4, 'ipv4');
});

// The compiled function is available for diagnostics.
console.log('trust proxy =', app.get('trust proxy'));
console.log('10.1.2.3 is trusted:', app.get('trust proxy fn')('10.1.2.3', 0));

app.get('/whoami', (req, res) => {
  res.json({ ip: req.ip, ips: req.ips, protocol: req.protocol, hostname: req.hostname });
});

app.listen(3000, (error) => {
  if (error) {
    throw error;
  }
});

Notes on the example. Only the last app.set() is active, the others are alternatives shown commented out. The call app.enable('trust proxy') is the same as app.set('trust proxy', true), which is why it should read as a warning when it appears in a code review. The function receives the address and the hop index, and proxy-addr calls it first with the socket peer (index 0), then with each earlier entry of X-Forwarded-For. A function that returns true for every hop behaves like true. The trusted ranges of a public CDN are published by the vendor and change, so such a function should load them from configuration instead of hard-coding them.

Choosing a value

The goal is that req.ip is the address of the first machine that you do not control. Three questions decide the value:

  1. How many proxies stand between the Internet and the application, in every deployment, including the developer laptop with no proxy at all?

  2. Can a client reach Express without passing through them (a public port, a second ingress, another service in the cluster calling the pod directly)?

  3. Do all of them overwrite or append to X-Forwarded-For, or does the first one pass the client’s value through?

Practical rules:

  • When the proxies sit in a private network and nothing else in that network is untrusted, 'uniquelocal' (plus 'loopback' for a sidecar) is simple and survives changes of the number of hops. It is wrong when the network also hosts untrusted workloads that can send requests straight to the application.

  • A hop count is exact for a stable chain, and wrong the moment the chain changes. A count that is too small makes req.ip the address of a proxy, so one rate-limit bucket covers every user. A count that is too large makes req.ip an attacker-controlled entry.

  • A list of exact addresses or subnets is the most defensive form, because a request that arrives from any other address is treated as direct and its headers are ignored.

  • Use an environment variable so that development, tests and production can differ:

// config-trust-proxy.mjs
import express from 'express';

/** Turns the TRUST_PROXY environment value into something app.set() understands. */
export function parseTrustProxy(value) {
  if (value === undefined || value === '' || value === 'false') {
    return false;
  }
  if (value === 'true') {
    return true;
  }
  if (/^\d+$/.test(value)) {
    return Number(value);
  }
  return value; // addresses, subnets or presets, separated by commas
}

const app = express();
app.set('trust proxy', parseTrustProxy(process.env.TRUST_PROXY));

app.listen(3000, (error) => {
  if (error) {
    throw error;
  }
});

Run it with TRUST_PROXY=1 node config-trust-proxy.mjs behind a single proxy, TRUST_PROXY=uniquelocal node ... inside a private network, or without the variable on a laptop.

What changes in the request

The setting affects these properties. All of them behave the same in Express 4 and Express 5 unless stated.

Property Without trust With trust

req.ip

The socket’s remote address.

The first address from the right, in the chain described above, that is not trusted. If every hop is trusted, the leftmost X-Forwarded-For entry.

req.ips

An empty array.

The client and the intermediate proxies, client first. It does not include the socket address of the machine that connected to Express. req.ips[0] is the same as req.ip when trust stops inside the list.

req.protocol

https for an encrypted socket, http otherwise.

The first value of X-Forwarded-Proto when the socket peer is trusted (the header is trimmed), or the socket-derived value when the header is missing.

req.secure

true only for an encrypted socket.

true when req.protocol is https.

req.host

The Host header, including the port. In Express 5 the port is no longer removed; Express 4 had a deprecated version that did.

The first value of X-Forwarded-Host when the peer is trusted, otherwise the Host header.

req.hostname

The host without the port (an IPv6 literal keeps its brackets).

The first X-Forwarded-Host value without the port.

req.subdomains

Computed from req.hostname and the subdomain offset setting.

The same computation over the forwarded host name.

Two points are easy to miss. First, only the headers of the connection peer are gated by the trust function: for X-Forwarded-Proto and X-Forwarded-Host, Express asks whether the socket peer (index 0) is trusted and does not distinguish between hops. A hop count of 1 and a count of 5 therefore read the same protocol and host, and differ in req.ip. Second, req.secure and req.protocol follow the first value of the header: a proxy that appends instead of overwriting an inbound X-Forwarded-Proto lets a client choose the protocol Express sees. Make the first proxy overwrite it.

// whoami.mjs
import express from 'express';

const app = express();
app.set('trust proxy', process.env.TRUST_PROXY ?? false);

app.get('/whoami', (req, res) => {
  res.json({
    ip: req.ip,
    ips: req.ips,
    protocol: req.protocol,
    secure: req.secure,
    host: req.host,
    hostname: req.hostname,
    subdomains: req.subdomains,
    socketAddress: req.socket.remoteAddress,
  });
});

app.listen(3000, (error) => {
  if (error) {
    throw error;
  }
});

Try it locally, pretending to be a proxy with curl:

# Terminal 1, no trust: the headers are ignored and req.ip is the loopback address.
node whoami.mjs
# Terminal 2:
curl -s -H 'X-Forwarded-For: 203.0.113.7' -H 'X-Forwarded-Proto: https' localhost:3000/whoami

# Stop the server, then restart it trusting the loopback peer, which now acts as the proxy.
TRUST_PROXY=loopback node whoami.mjs
# Terminal 2: the header is now believed.
curl -s -H 'X-Forwarded-For: 203.0.113.7' -H 'X-Forwarded-Proto: https' localhost:3000/whoami
{
  "ip": "203.0.113.7",
  "ips": ["203.0.113.7"],
  "protocol": "https",
  "secure": true,
  "host": "localhost:3000",
  "hostname": "localhost",
  "subdomains": [],
  "socketAddress": "::1"
}

The first request reports the loopback address (::1 or ::ffff:127.0.0.1), an empty ips and http. This is also the quickest check of a deployed setup: expose a diagnostic route on an internal port, send a request through the full proxy chain and compare ip and ips with what is expected.

Trusting too much: spoofing and rate-limit bypass

X-Forwarded-For is a request header, and a client can send any value in it. A proxy appends its observation to the right of whatever the client wrote, which is why proxy-addr reads from the right and stops at the first untrusted address. Consider a single Nginx that appends to the header, and a hostile client at 203.0.113.7 that sends X-Forwarded-For: 1.2.3.4. Nginx forwards 1.2.3.4, 203.0.113.7 (it appended the real client address to the forged one), so the list seen by Express is the Nginx address, 203.0.113.7, 1.2.3.4:

Setting req.ip Result

false

The proxy’s address

The forgery is ignored, but every user shares one address.

1

203.0.113.7

Correct, because the single trusted hop reported what it really saw.

2

1.2.3.4

The application believes a second proxy that does not exist, so the forged entry becomes the client.

true

1.2.3.4

Every hop is trusted and the leftmost entry is taken: the client chooses its own address.

The consequences of a forgeable req.ip are practical:

  • A rate limiter keyed on req.ip can be bypassed by sending a different header value in every request, and an attacker can lock out a victim by making the limiter see the victim’s address.

  • IP allow lists and deny lists protecting an admin route can be defeated.

  • Audit and access logs record false addresses, which weakens incident response.

  • Geolocation and fraud scoring become unreliable.

express-rate-limit validates this configuration at runtime and logs an error when X-Forwarded-For is present while trust proxy is false, and when trust proxy is true, for these reasons. Treat those messages as configuration bugs, not noise. Direct access is the other half of the problem: when the application port is reachable without the proxy, a client sends the forged header straight to Express, and any setting that trusts the client’s own address (true, or a count larger than the real chain) believes it. Bind to a private interface, restrict the security group or network policy so that only the proxy can connect, and prefer an address list so that stray direct connections are treated as untrusted.

The hardening at the proxy is as important as the setting in Express. The first proxy (the one facing the Internet) should discard what the client sent in X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host and write its own values; inner proxies should append.

The 2026 proxy-addr fix

Express delegates all address parsing and trust evaluation to the jshttp proxy-addr package. Its release 2.0.8 fixes a vulnerability published as CVE-2026-90711 (GitHub advisory GHSA-jqcg-44mw-7w3h), and Express 5.3.0 raised its dependency range to ^2.0.8. Applications that depend on trust proxy for security decisions should:

  • Update to a current Express release (5.3.x or 4.22.x or later) and confirm the resolved version with npm ls proxy-addr; a lockfile can keep an older copy even after Express is updated.

  • Read the advisory text for the exact conditions, since this page does not repeat the details.

  • Re-test the trust proxy configuration with spoofed headers after upgrading, using the diagnostic route above or an automated test.

npm ls proxy-addr
npm audit
npm update proxy-addr

Secure cookies and HTTPS redirects

The most frequent symptom of a missing trust proxy is a session that never sticks. express-session refuses to send a cookie marked secure: true over a connection that Express considers insecure. Behind a TLS-terminating proxy without trust proxy, req.protocol is http, the Set-Cookie header is never produced and users cannot log in, although nothing is logged. The fix is to trust the proxy so that req.secure becomes true. The same property drives a redirect from HTTP to HTTPS done in the application instead of at the proxy:

// secure-session.mjs
import express from 'express';
import session from 'express-session';

const app = express();

// One reverse proxy terminates TLS in front of the application.
app.set('trust proxy', 1);

app.use((req, res, next) => {
  if (process.env.NODE_ENV === 'production' && !req.secure) {
    return res.redirect(308, `https://${req.hostname}${req.originalUrl}`);
  }
  next();
});

app.use(
  session({
    name: 'bookshelf.sid',
    secret: process.env.SESSION_SECRET, // read from the environment; never commit a value
    resave: false,
    saveUninitialized: false,
    cookie: { secure: true, httpOnly: true, sameSite: 'lax', maxAge: 60 * 60 * 1000 },
  }),
);

app.get('/', (req, res) => {
  req.session.views = (req.session.views ?? 0) + 1;
  res.json({ views: req.session.views, secure: req.secure });
});

app.listen(3000, (error) => {
  if (error) {
    throw error;
  }
});

Run it with node --env-file=.env secure-session.mjs, where .env defines SESSION_SECRET with a value generated by the command node -p crypto.randomBytes(32).toString('hex'). express-session also has its own proxy option for deciding whether to trust X-Forwarded-Proto; when it is left undefined the middleware follows the application’s trust proxy setting, which is the behaviour to keep. A redirect built from req.hostname rather than the Host header avoids copying a port, but the host name still comes from the client, so a production deployment should redirect to a configured canonical origin. The full session setup is in Cookies & Sessions.

Typical setups

The values below are starting points. The behaviour of a proxy depends on its configuration and version, so verify the real chain with the diagnostic route.

Setup Typical trust proxy Remarks

Nginx on the same host

'loopback' or 1

Nginx must set the headers (see the example below). The socket peer is 127.0.0.1 or ::1.

Nginx in a container network, or a Docker Compose service

'uniquelocal' or the exact subnet of the network

The peer is the bridge-network address of the Nginx container.

Cloud load balancer (AWS ALB, Google Cloud HTTP(S) load balancer, Azure Application Gateway)

1

The balancer appends the client address to X-Forwarded-For and sets X-Forwarded-Proto. Check the vendor’s page for the exact header behaviour; some balancers add their own address as well, so two hops can appear.

CDN in front of a load balancer in front of Express

2

The count must include every proxy that appends to X-Forwarded-For. Many CDNs also send a vendor-specific client IP header; that value is not read by Express.

Heroku

1

The Heroku router sits in front of the dyno and appends the client address. The Heroku free dynos that older tutorials used no longer exist, but paid dynos still need the setting.

Kubernetes ingress controller

1, 'uniquelocal' or the pod network range

Whether the controller forwards or replaces an inbound X-Forwarded-For is a controller option. A cloud load balancer or NodePort service in front of the ingress changes the number of hops and can hide the client address with source NAT. Verify with the diagnostic route.

Platform-as-a-service (Cloud Run, Render, Fly.io, Railway)

1, unless the vendor documents otherwise

The platform proxy terminates TLS. Read the vendor’s headers page.

Local development, no proxy

false

Do not copy the production value into development.

A minimal Nginx server block in front of Express, which overwrites what the client sent:

server {
    listen 443 ssl;
    http2 on;
    server_name books.example.com;

    ssl_certificate     /etc/ssl/books.example.com/fullchain.pem;
    ssl_certificate_key /etc/ssl/books.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;

        # The first proxy overwrites what the client sent.
        proxy_set_header X-Forwarded-For   $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_set_header Host              $host;
    }
}

Because $remote_addr replaces the header, this Nginx is the first proxy and trust proxy should be 1 (or 'loopback'). Behind another proxy that already added a legitimate chain, use $proxy_add_x_forwarded_for instead, which appends $remote_addr to the incoming value. The proxy_set_header directive replaces any header the client sent with the same name, so it is the place where a spoofed X-Forwarded-Proto is discarded. The reverse is also worth checking: Nginx does not pass headers whose value is empty, and a directive missing from a nested location can silently reset the inherited ones, because proxy_set_header is inherited only when the block defines none of its own.

Testing the configuration

A request test proves that spoofed headers are rejected. Use supertest and node:test against the exported application without listening (see Testing):

// trust-proxy.test.mjs
import assert from 'node:assert/strict';
import { test } from 'node:test';
import express from 'express';
import request from 'supertest';

function createApp(trustProxy) {
  const app = express();
  app.set('trust proxy', trustProxy);
  app.get('/ip', (req, res) => {
    res.json({ ip: req.ip, ips: req.ips, protocol: req.protocol });
  });
  return app;
}

test('ignores forwarding headers when no proxy is trusted', async () => {
  const response = await request(createApp(false))
    .get('/ip')
    .set('X-Forwarded-For', '1.2.3.4')
    .set('X-Forwarded-Proto', 'https');

  assert.notEqual(response.body.ip, '1.2.3.4');
  assert.deepEqual(response.body.ips, []);
  assert.equal(response.body.protocol, 'http');
});

test('one trusted hop reads the rightmost forwarded address only', async () => {
  const response = await request(createApp('loopback'))
    .get('/ip')
    .set('X-Forwarded-For', '1.2.3.4, 203.0.113.7');

  assert.equal(response.body.ip, '203.0.113.7');
});

test('trusting everything lets the client choose its address', async () => {
  const response = await request(createApp(true)).get('/ip').set('X-Forwarded-For', '1.2.3.4, 203.0.113.7');

  assert.equal(response.body.ip, '1.2.3.4'); // the reason not to use true
});

Source: ladjs/supertest

Install with npm install --save-dev supertest and run node --test. The second test shows the key property: with a trusted loopback peer, 1.2.3.4 stays an unverified claim, and the address that the (simulated) proxy observed wins.

What changed recently

Version Change relevant to proxies

Express 5.0.0 (2024-09)

req.host no longer removes the port; req.hostname is unchanged. The trust proxy semantics, req.ip, req.ips, req.protocol and req.secure are unchanged from Express 4.

proxy-addr 2.0.7

Reads the peer from req.socket instead of the deprecated req.connection (through forwarded 0.2.0).

proxy-addr 2.0.8 and Express 5.3.0 (2026-10-09)

Fixes CVE-2026-90711; Express 5.3.0 requires proxy-addr ^2.0.8. Check npm ls proxy-addr on both major lines.

Express 4.22.x

Maintenance and security releases. Behaviour of the settings is the same as in earlier 4.x releases. The command npm view express dist-tags shows the current 4.x release.

Where the books are outdated

The four books consulted for this section target Express 4.0 to 4.18 and Node.js 0.10 to 20. The deployment chapters that put a session-based application on a platform proxy are the ones affected:

Older practice Where it appears Current behaviour

Sessions with a secure cookie deployed on Heroku, with no trust proxy.

Deployment chapters of the books

The cookie is never set, because the application sees http. Add app.set('trust proxy', 1) and use a hop count or a list instead of true.

app.enable('trust proxy') as a blanket fix for logging or HTTPS problems.

Community deployment tutorials

Trusts every hop and lets clients forge req.ip. Use a count or an address list that matches the real chain.

Reading req.connection.remoteAddress or the raw x-forwarded-for header directly.

Older middleware and tutorials

Use req.ip and req.ips, which apply the trust rules; req.connection is deprecated in Node.js in favour of req.socket.

Heroku free dynos, mLab and other hosts as the deployment target.

All four books

These services no longer exist in that form. The setups above apply to current platforms, and the chain length must be re-measured for each one.

No mention of rate limiting or logging of the client address.

All four books

Rate limiting, logging and allow lists all depend on a correct req.ip; see Security Best Practices.

Common pitfalls

Symptom Usual cause Fix

Every user has the same IP in logs and the rate limiter.

trust proxy is false, or the hop count is smaller than the chain.

Set the count or list that matches the proxies and test through the real chain.

Users can choose their own IP.

trust proxy is true, the count is larger than the chain, or the application port is reachable directly.

Use a list or exact count, close direct access, and make the first proxy overwrite X-Forwarded-For.

Login works locally but the session cookie is missing in production.

cookie.secure is true and the proxy is not trusted, so req.secure is false.

Set trust proxy, and confirm that the proxy sends X-Forwarded-Proto.

Infinite redirect loop from an HTTP to HTTPS middleware.

The application sees http for every request because the proxy is not trusted or does not send X-Forwarded-Proto.

Trust the proxy and forward the header; or do the redirect at the proxy.

req.hostname is the internal service name.

The proxy does not forward Host or X-Forwarded-Host, or the proxy is not trusted.

Send X-Forwarded-Host (or pass Host through) and trust the proxy.

req.ip shows ::ffff:10.0.0.5.

The socket is IPv6 dual-stack and reports an IPv4-mapped address.

Normalise it when storing or comparing addresses (remove the ::ffff: prefix) or compare with node:net.

The value is right on a laptop and wrong in the cluster.

The number of hops differs between environments.

Read the setting from an environment variable and verify each environment with a diagnostic route.

req.ip is the load balancer’s address behind a Kubernetes service.

Source NAT replaced the client address before it reached the ingress.

Preserve the client address at the load balancer or service level, or use the proxy’s headers; then verify the chain.

A custom function trusts too much.

It ignores the index or matches a broad prefix.

Return true only for exact addresses or subnets you control and test it with forged chains.