Keeping the VPN client's address all the way to the application makes every log line and every allowlist entry mean the same thing. Rewriting it at the cloud edge makes the routing table simpler and the logs less useful. Neither choice is wrong on its own, and the argument that decides it is rarely about performance. Three conditions settle it: whether the application must know which human connected, whether the cloud fabric will accept the source you intend to send, and who is willing to own the return route afterwards.

Three conditions decide it

Start with the application's requirement, not with the network diagram. If authorization or audit depends on the individual client address, then preserving it is a functional requirement and translation is a regression. If the application only needs to know "traffic arrived through the remote access path", a translated address is sufficient and much cheaper to operate.

The second condition is mechanical. A virtual network does not automatically trust a packet that claims an address the sending interface was not assigned. Most cloud fabrics enforce that, in different ways and under different names, and a design that ignores it will look correct in a diagram and drop silently in production.

The third is organizational. Preserving sources means the application subnet's route table has to point back at the gateway for the client prefix, and every team that adds a subnet afterwards has to keep that route consistent. That is a small cost once, and a recurring one across dozens of accounts. Decide who pays it before you promise the behaviour.

A route is not permission to spoof

Adding a return route tells your subnet where replies should go. It does not tell the fabric that the instance forwarding those packets is allowed to emit them with a foreign source. On AWS the relevant control is source/destination checking, and it is visible per instance:

aws ec2 describe-instances --instance-ids i-0a1b2c3d4e5f60718 \
  --query "Reservations[].Instances[].SourceDestCheck"
aws ec2 modify-instance-attribute --instance-id i-0a1b2c3d4e5f60718 \
  --no-source-dest-check

Turn that off for the instance whose documented job is forwarding, and for nothing else. Every application VM that keeps the check enabled is one fewer machine that can be used to emit traffic with a borrowed source. If your platform is not AWS, look for the equivalent anti-spoofing or allowed-address setting rather than assuming the behaviour carries over.

A quick packet capture settles the question faster than reading the console. If replies leave and the client never sees them, the fabric is discarding the spoofed source rather than the application failing.

What translation buys, and what it costs

Rewriting the source at the gateway collapses the return path problem. The application sees one address it already knows how to reach, and the client prefix never has to appear in a cloud route table. On Linux the rule is one line, and it is worth writing the narrowest version you can:

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 172.20.8.0/24 -o ens5 -j SNAT --to-source 10.40.3.10

Note what the application now records: 10.40.3.10 for every user, on every connection, forever. Any address allowlist built on that value now grants access to the entire remote access population, and any incident review has to go back to the gateway to answer "who was this".

The fix is not to abandon translation, it is to stop pretending the address is an identity. Carry the client identity in an authenticated field, log it at the gateway with the translated tuple, and let the application join on that. Without the join key, translation converts a network problem into a permanent forensic one.

The relay is a different interface

A reverse proxy in front of the application sidesteps the routed-path question entirely. The proxy opens its own backend connection, so the client prefix never needs to be routable inside the virtual network, and the application can receive identity in a header the proxy sets.

That header is only trustworthy when the backend accepts it from the proxy and from nothing else. In nginx terms that means naming the trusted peer explicitly instead of believing whatever arrives:

set_real_ip_from 10.40.3.10;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

Any client that can reach the backend directly can forge the same header, so the backend must be unreachable except through the proxy. That is a firewall fact, not a configuration opinion, and it is the assumption people skip when they copy the snippet.

The relay also changes what you are buying. You get an application-level identity and a natural place to log it; you give up generality, because each new protocol needs its own proxy support. For HTTP services that trade is usually good, and for anything that is not HTTP it is usually not available at all.

Write down what each boundary sees

Whatever you choose, the design review should end with one table that names the source address and the provable identity at each hop. Without it, the first incident produces three teams each describing a different client.

BoundarySource the next hop seesWhat is actually proven
Client to gateway172.20.8.14A certificate or credential was presented
Gateway to app subnet, preserved172.20.8.14The same session, if the return route exists
Gateway to app subnet, translated10.40.3.10Only that traffic came through the gateway
Proxy to backend10.40.3.10 plus a headerThe proxy asserted an identity it authenticated

Then choose. Preserve when the application's authorization genuinely depends on the client address and you can own the route. Translate when it does not and you can carry identity out of band, and use the AWS forwarding-instance guidance for the platform-specific forwarding settings. Reach for a relay when the service speaks HTTP and you want identity to stop being an address at all.

Artaigilean co-cheangailte