Every line in the access log shows the same loopback address, while the page itself loads and the certificate checks out. Nothing is broken in the sense of being unreachable, which is why this one survives for months. What broke is attribution: the web backend records the address of the hop that forwarded the connection, and every downstream decision that trusted the log inherited the same wrong answer.

The checks below run from the wire inward. Stop at the first one that explains the value you see, because each later step assumes the earlier ones are already true. None of them requires guessing at the front-end configuration.

Follow the two connections, not the log line

Port sharing multiplexes two protocols on one TCP listener. OpenVPN inspects the first bytes of an incoming connection, keeps it when it looks like its own protocol, and hands anything else to a second destination. That destination is normally a web server on the same host, so the web server's accepted socket has the OpenVPN process as its peer.

ss -ltnp 'sport = :443'
ss -tnp 'dport = :8080'

The first command shows who owns the public listener. The second shows the connections arriving at the backend port, and their peer address is the one the access log will print. If that peer is loopback, the log is accurate and the information was lost a layer earlier. Renaming the log field or enabling a client-IP module will not recover an address the connection never carried.

This is the check that decides everything else. If the backend peer is the browser's address, port sharing is not your problem and the log format is.

Transport forwarding is not header injection

The forwarder moves bytes. It does not parse HTTP, so it cannot insert a header into a request it never reads as a request, and it cannot add anything to a TLS stream it is passing through untouched. A module expecting a proxy header will keep waiting for one that no component is producing.

Two mechanisms can carry the original address, and both require agreement on both ends. The PROXY protocol prepends a small header to the connection, and the backend must be configured to expect it; a backend that does not expect it will treat those bytes as the start of the request and fail in a confusing way. Trusted proxy headers such as X-Forwarded-For work when an HTTP-aware proxy sets them and the backend is told which peers are allowed to assert them.

Neither is automatic, and neither is safe by default. A backend that believes any incoming forwarded header lets a client choose its own address, which converts a logging inaccuracy into an access-control bypass. Run the demonstration once on a staging host before you trust any header on a production path.

curl -sS -H 'X-Forwarded-For: 127.0.0.1' https://site.example.com/health
grep -F '127.0.0.1' /var/log/nginx/access.log | tail -n 3

The optional source-mapping directory

Port sharing has an optional directory argument that records a mapping between the original source address and the forwarding connection, on platforms where the feature is implemented. The web side is expected to consult that mapping; it is not a header, and it is not injected into the request.

That design is worth knowing about because it explains why some deployments report real addresses and others do not, with apparently identical front-end configuration. The difference is on the receiving side, and it usually lives in a module or patch that most packaged web servers do not enable by default. Read your web server documentation for the exact hook it offers, and confirm the module is loaded rather than assumed.

If you go this route, treat it as an integration with two halves: the mapping written by the forwarder and the lookup performed by the backend. Verify both, and confirm the directory survives a restart and does not grow without bound on a busy listener.

Decide which component vouches for the address

Before changing configuration, name the component that is allowed to assert a client address and the components that are allowed to believe it. A single answer written down prevents the usual drift, where three different layers each trust a different source.

Three designs cover most needs. Keep the public listener and the web listener separate, so the web server sees real peers and no attribution problem exists. Put an HTTP-aware reverse proxy in front, terminate or parse the request there, and let it set a forwarded header for a backend that trusts only that proxy. Or keep port sharing and accept that logs show the hop, with access rules written against application identity instead of an address.

What does not work is treating a forwarded loopback address as evidence that the request originated on this machine. That inference is exactly backwards: the address is present precisely because a local component made the connection. A rule written that way admits anyone who can reach the listener.

Test the rule, not the page

A page that loads proves the forwarding path works and says nothing about attribution. Test the consumer of the address: the access rule, the rate limit, the audit entry, the geo lookup.

curl -sS -o /dev/null -w '%{http_code}\n' https://site.example.com/
tail -n 5 /var/log/nginx/access.log
grep -c '' /var/log/nginx/access.log

Compare an authorized external request against both the front-end and backend records, and confirm that a rule which should deny still denies and one that should admit still admits. If the log line is correct but the rule still behaves as though everyone were local, the rule reads a different field than the one you fixed.

Finish by writing the attribution chain into the deployment notes: which listener, which forwarder, which header or mapping, and which backend is configured to trust it. The next person to read a log line will need all four, and the absence of that note is how the same ticket reopens a year later. Keep it next to the configuration rather than in a chat thread.

Skyldar greinar