An admin page answers from a phone on mobile data and times out from a laptop joined to the VPN. Nothing about the tunnel is broken, because the request never enters it. The hostname resolves to the gateway's public address, and that address is reachable only from outside the tunnel it terminates. Fixing this means giving the service a private entry rather than moving the outer endpoint around.

The page loads from outside and not through the tunnel

Two different requests are being compared. From mobile data, the browser reaches the public address directly. From inside the tunnel, the same name resolves to the same public address, and the packet now has to leave through the tunnel to reach an address on the tunnel's own far side. Same name, same address, two very different positions.

A destination route picks an address, not a port. Nothing in the routing table knows that port 443 is for administration and port 1194 is the transport, so both are treated as the same destination.

That is why the page sometimes works and sometimes does not, depending on which name and which network the client used. It is a path problem rather than an authentication problem. Both clients authenticate perfectly well.

Keep the outer endpoint reachable

Full-tunnel profiles need an exception route for the VPN endpoint, and OpenVPN's redirect-gateway option documents exactly that: the remote endpoint keeps a route through the pre-existing gateway so the tunnel does not route into itself. Remove that exception and the encrypted transport follows the tunnel, which ends the session in a loop. That exception is why the endpoint address stays reachable from inside the tunnel.

The consequence is that the public address stays reachable from the client, which is also why a browser request to that address can follow the same exception and leave without entering the tunnel. The exception is doing its job, and the name is doing the damage.

Read the route decision rather than the interface list before changing anything. The commands below print the decision rather than the inventory. Run them from the client that failed.

ip -4 route get 203.0.113.77
ip -4 rule show
ip -4 route show table all

If the lookup shows the endpoint route through the physical gateway, the behaviour is correct and the admin page is simply being sent down the wrong path. That is the point where name resolution becomes the fix.

Give the service a private name

The service needs a name that resolves to the gateway's address inside the tunnel, while keeping the certificate and hostname semantics the application expects. A private zone record for the administration name is the usual answer, and it leaves the transport name untouched.

Do not repoint the transport name to the inner address, because the next tunnel establishment would then try to reach an address that only exists inside a tunnel. That mistake produces a VPN that works exactly once.

Check both address families as well, since an AAAA record left behind can send the browser somewhere the private plan never covered.

dig +short admin.hq.internal
dig +short -t AAAA admin.hq.internal
curl -sS -o /dev/null -w '%{http_code} %{remote_ip}\n' https://admin.hq.internal/

Match the policy to where the process runs

A native process listening on the gateway is reached through the local-input path, so the input chain and the socket binding decide whether the request is accepted. Forwarding rules and IP forwarding do not make a local listener answer. Permit the tunnel sources in the input policy and confirm the process is bound to an address the tunnel can reach.

A container published on a host port follows a different path, because the packet is destination-translated and forwarded. That request meets the host forwarding policy and Docker's own rules, which is why a working native listener can coexist with a container that refuses the same port.

Startup order matters in both cases, because a bind to an address that appears only after the tunnel comes up fails silently at boot.

Check private access and a denied public path

Make a fresh named request from an authorized client with the application's own identity checks intact. Then try the same service from outside the tunnel and confirm it does not answer. A private name is a convenience rather than a restriction, and a listener still bound to the public address is still exposed. That last check is the one most reviews skip.

Read the bindings and the filter state rather than the hostname users were given. Exposure is a property of the socket rather than of the name. That distinction survives every DNS change.

ss -lntp | grep ':443'
nft list ruleset | grep -n 443
curl -sS -m 5 -o /dev/null -w '%{http_code}\n' https://203.0.113.77/

When the inner request returns the page and the outer request times out, the service has one deliberate private path and the transport stays reachable. When the outer request still answers, the listener or a published container entry needs narrowing before anyone describes the service as internal-only.

פֿאַרבונדענע אַרטיקלען