Project configuration

Real visitor IP

Every request to a public HTTP project goes through Zenifra first, which handles HTTPS, DDoS protection, and routing to your application. Because of that, the connection address your application receives is an internal platform address, not the address of whoever made the request. The visitor's real IP arrives in an HTTP header.

Where the visitor IP is

SourceWhat it containsUse it?
X-Real-IP headerThe visitor IP, set by ZenifraYes
X-Forwarded-For headerA list of addresses. The last one is the visitor IP; earlier ones may have been sent by the visitorOnly the last item
Connection address (req.socket.remoteAddress, REMOTE_ADDR)An internal platform address, the same for every visitorNo

Zenifra always overwrites X-Real-IP. Even if the visitor sends this header with another value, your application receives the IP the request actually came from.

Do not use the first X-Forwarded-For item

Visitors can send their own X-Forwarded-For. Zenifra keeps what they sent and appends the real IP at the end:

X-Forwarded-For: 1.2.3.4, 203.0.113.10
X-Real-IP: 203.0.113.10

In this example, 1.2.3.4 was made up by the visitor and 203.0.113.10 is the real IP. Code that takes the first item of the list can be fooled with a header, which breaks rate limits, blocks, and audit logs. Prefer X-Real-IP.

Examples by language

The header does not exist in local development, so the examples fall back to the connection address.

Node.js with Express

// Trust one proxy hop: Zenifra's
app.set('trust proxy', 1)

app.get('/', (req, res) => {
  res.send(`Your IP: ${req.ip}`)
})

With trust proxy set to 1, req.ip uses the last X-Forwarded-For item, which is the real IP. Do not set trust proxy to true: in that case Express uses the first item, which the visitor can make up.

Node.js without a framework

const ip = req.headers['x-real-ip'] ?? req.socket.remoteAddress

Python

# Flask
ip = request.headers.get('X-Real-IP', request.remote_addr)

# FastAPI
ip = request.headers.get('x-real-ip', request.client.host)

# Django
ip = request.META.get('HTTP_X_REAL_IP', request.META.get('REMOTE_ADDR'))

PHP

$ip = $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'];

Go

ip := r.Header.Get("X-Real-IP")
if ip == "" {
	ip, _, _ = net.SplitHostPort(r.RemoteAddr)
}

Original HTTPS and port

The application receives the request over an internal connection, but Zenifra tells it how the visitor connected:

X-Forwarded-Proto: https
X-Forwarded-Port: 443

Like X-Real-IP, these values are set by Zenifra and cannot be changed by the visitor. When the proxy is trusted, frameworks recognize the HTTPS access and generate absolute URLs with https:// and Secure cookies:

  • Express: with app.set('trust proxy', 1), req.protocol is 'https' and req.secure is true.
  • Django: SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') makes request.is_secure() return True.

Best practices

  • Store the IP as text. Accept IPv4 and IPv6, up to 45 characters.
  • Use the IP for rate limits, logs, and fraud checks, always from X-Real-IP.
  • Do not force HTTPS in the application. Zenifra already redirects http:// to https:// before the request arrives. Options such as SECURE_SSL_REDIRECT in Django, force_ssl in Rails, or redirect middlewares can create a loop.
  • To block or allow IPs, prefer the console's IP access control (from the Premium Plus plan). It uses the real visitor IP and stops the request before it reaches your application.
  • An IP is not an identity. Many people can share the same IP, such as in companies and mobile networks. Keep login, tokens, or API keys on sensitive routes.

Next steps

FAQ

Why does my application show the same IP for every visitor?

It is reading the connection address, which is internal to the platform. Switch to the X-Real-IP header, as in the examples above.

Can a visitor fake X-Real-IP?

No. Zenifra replaces the value sent by the visitor with the IP the request came from.

Does it work with a custom domain?

Yes. The header arrives the same way on the Zenifra subdomain and on custom domains.

Last updated on

On this page