Forwarding encrypted bytes while never holding the certificate is what a TLS passthrough relay does, and the arrangement is both convenient and slightly confusing. Because nothing is decrypted, nothing HTTP-shaped exists to filter on, so the usual location block and its access rules have no purchase here. The decision available to NGINX is about the connection itself: which network peer knocked, and whether that peer should be relayed at all.

The decision belongs in stream, not HTTP

In the stream context NGINX works with TCP sessions. It sees the peer address of the connection and can apply ordered allow and deny rules to it before any bytes reach the backend. That is a complete admission decision and it requires no private key, no certificate and no HTTP parsing.

MechanismWhat it decidesWhere it lives
stream allow/denyWhether this TCP peer is relayedstream server block
ssl_preread mapWhich backend a named connection goes tostream, with the preread module
HTTP location ruleWhether an HTTP request is servedhttp server block

Those three rows never substitute for each other. Copying an HTTP access directive into whichever file mentions port 443 changes nothing, because the stream server never reads the http context.

Admit the connection before relaying it

Assume an existing installation with the stream and stream-access modules available, VPN clients keeping source addresses in 10.90.0.0/24, and a backend at 192.168.50.20:443 that already serves the intended HTTPS identity.

stream {
    server {
        listen 443;

        allow 10.90.0.0/24;
        deny all;

        proxy_pass 192.168.50.20:443;
    }
}

Read that as a new-listener fragment rather than a drop-in replacement for a working configuration. Review existing listeners and includes first, because a second listener on a port that is already bound fails at reload rather than at request time.

Notice what is absent: no listen ... ssl, no certificate directive. NGINX relays the encrypted stream, and the backend terminates TLS and presents the certificate the client validates. A client that fails the allowlist gets its TCP session rejected, not an HTTP 403 page, and any tool that expects a status code will report something unhelpful.

What the allowlist does and does not cover

If the relay already chooses backends by server name, ssl_preread and a map can keep doing that job, provided the module is available. The name is still supplied by the client, and nothing about presenting a name proves the client joined the VPN.

The example rule covers the whole stream listener. That makes it a poor fit for one entry point serving a mixture of public and VPN-only sites, where a per-name exception would be needed; separate listeners are usually clearer than a name-dependent policy nobody can read.

Two further gaps are worth naming. A prefix match says nothing about which interface the packet arrived on, and an HTTP rule somewhere else cannot constrain this listener. Both are reasons to keep host-level ingress policy in the picture rather than treating one allow line as the whole boundary. Write the boundary down as two controls that must agree, so a change to either one gets reviewed.

Whose address does NGINX actually see?

A load balancer, another proxy or source NAT can put an intermediary in front of NGINX, so the peer it observes is not the VPN client inner address. Inspect the real path before you choose a prefix, because an allowlist written for the wrong address either admits everyone behind the translator or nobody at all.

PROXY protocol and real-IP handling are legitimate answers, and they must be configured only for compatible, explicitly trusted senders. Both mechanisms believe what the sender says, which is precisely why the sender list has to be short. Trusting an identity header from any internet client does not make source information trustworthy again; it converts a correct refusal into a forged acceptance.

Test both outcomes before you reload

Validate the configuration, reload, then test twice: a fresh allowed session that must work, and a denied one that must not. Keep normal backend certificate verification in the allowed test, because a relay that connects but cannot validate the backend has fixed one problem and created another.

nginx -t
ss -tnp | grep ':443'

Check the backend route and the reply separately as well. Passing the allowlist only proves that NGINX accepted the session; it cannot make an unreachable backend answer, and it cannot repair a return path that was missing before you started.

Pennadoù liammet