Three independent decisions stand between a private site and the internet: which socket accepts the connection, which path may deliver it, and which HTTP clients the selected server answers. A server block makes the first decision only. Treat the other two as separate controls, or the site will look private while an old entrance remains open. Most VPN-only NGINX failures come from testing only the first layer.

The worked example uses a Linux OpenVPN gateway at 10.90.0.1 on tun0, clients in 10.90.0.0/24, preserved sources and TCP 8443 reserved for this entry. Adjust the values, keep the layers.

Start with the socket, not the hostname

Bind the HTTPS listener to the tunnel address and a dedicated port. A specific address is easier to reason about than a wildcard socket plus a name check, because the kernel will not hand the port to another local address.

server {
    listen 10.90.0.1:8443 ssl;
    server_name admin.corp.example;

    ssl_certificate     /etc/nginx/tls/admin-fullchain.pem;
    ssl_certificate_key /etc/nginx/tls/admin-key.pem;

    root /srv/private-site;

    allow 10.90.0.0/24;
    deny all;

    location / {
        try_files $uri $uri/ =404;
    }
}

The certificate must name the service host clients type, and the private DNS view must resolve that host to the bound address. A server_name only chooses a virtual host; it does not authenticate anyone. A missing DNS entry produces a certificate error that looks like a TLS fault. Check the name before replacing the certificate.

Read the effective configuration before adding the file. Includes and wildcard listeners can serve the same root elsewhere, and a default server may answer requests the new block never sees.

Close every other listener for that content

List the listen directives and compare the roots they serve. A block added for a different application can proxy to the same upstream, and an old default server can keep answering after the new one is in place. That output is the real inventory, not the file you just edited. Compare every root that serves the same content.

sudo nginx -T | grep -n "listen\|server_name\|root\|proxy_pass"

Check both address families. An IPv4 bind leaves an IPv6 socket untouched, and a new A record does not retire an old AAAA answer that still points at a public path.

Retire the old virtual host as a separate change. Removing the new block's public twin is usually the step that actually closes the entrance; the private listener only adds the intended one.

Scope the incoming path at the host

The listen address limits the socket, not the ingress interface. A packet routed to that address from somewhere else still arrives as local input, so the host firewall needs its own statement about where the connection may come from. That is why a bind alone rarely satisfies an audit.

With a default-deny incoming policy and no broader allowance for this service, a scoped rule names the tunnel, the source range, the destination and the port together:

sudo ufw allow in on tun0 proto tcp from 10.90.0.0/24 to 10.90.0.1 port 8443

Read the effective firewall order before applying it. An earlier broad accept can defeat the restriction, and an earlier terminal drop can stop this allowance from ever being reached. Do not flush unrelated rules or switch the firewall off to make the example fit.

That rule is host input, not a route to another LAN machine, and the gateway's outer OpenVPN listener remains a separate service. If the site was public before, plan how the old entry is retired and how existing sessions are handled.

Keep the HTTP list intact as locations change

NGINX evaluates allow and deny entries in order, and the server-level list above admits the declared pool before the final deny. The access module also records that a location defining any access entry uses its own list rather than the parent list. Order matters as much as the entries themselves.

That reset is the usual regression. A new upload, API or health location that adds one entry silently replaces the server-level restriction, and the site becomes broader at the path added most recently.

Review every location, alias and alternate block as part of the same policy. If IP checks are combined with NGINX authentication, understand satisfy: an any setting can admit a client through one mechanism while the other has not passed. Put the access list into the location review checklist.

An HTTP denial happens after the TCP connection reaches NGINX. It supplements the network restriction, and the application's own authorization still applies to each user.

Verify the live path and the failure case

Run the configuration parser, inspect the listening sockets, then make a fresh named request from an intended client. Confirm the certificate validates, the DNS name resolves to the bound address and the response comes from the expected root.

Test an excluded source and an alternate entrance next. A working allowed request says nothing about the paths you meant to close, and the excluded test is where a leftover listener or a broad firewall accept shows up.

Repeat the boot or VPN-recovery case. If NGINX starts before the tunnel address exists, the bind can fail even with a perfect file. Coordinate address readiness and keep a local recovery path before changing a shared web server.

Record the socket, port, source range and HTTP list that passed. The next VPN or DNS change can move any of them, and four named values are easier to compare than a working browser tab.

ተዛመድቲ ጽሑፋት