Skip to content
Professional8 min read

Client IP Preservation: What Your Target Actually Sees as the Source

Whether a target sees the real client address depends on the target type and the protocol, not on one setting. Getting it wrong produces working traffic with the wrong source IP, which breaks security groups, rate limiting, and audit logs without breaking connectivity.

Published Aug 19, 2026

Every question about ingress eventually becomes the same question: what does the target see in the source field? Security groups are evaluated against it. Rate limiters key on it. Audit logs record it. Geo-restriction decides on it. And a load balancer is a device whose entire job involves standing between the client and the target, so the answer is never simply "the client".

The failure mode is not an outage. Traffic flows, health checks pass, and the source address is wrong — so the rules you wrote against it silently stop meaning what you intended.

The default depends on two things, not one

For a Network Load Balancer, client IP preservation is controlled by the preserve_client_ip.enabled target group attribute, but whether you can set it at all — and what it defaults to — depends on the target type and the protocol.

Target groupProtocolDefaultChangeable
instanceAnyEnabledOnly for TCP and TLS
ipUDP, TCP_UDP, QUIC, TCP_QUICEnabledNo
ipTCP, TLSDisabledYes

The CDK documentation states the same rule more compactly than the service docs do, and it is worth memorizing in this form: the default is false if the target group type is IP address and the protocol is TCP or TLS, otherwise true.

So the common case — an instance target group on TCP — preserves the client IP by default, and the other common case — an ip target group on TCP, which is what you get with Fargate or with IP targets behind PrivateLink — does not.

Address family can erase the client IP on its own

Preservation only survives when the client and the target are the same address family. A dual-stack load balancer translating between families substitutes its own address:

TargetIPv4 clientIPv6 client
instance (IPv4)Client IPv4Load balancer IPv4
ip (IPv4)Client IPv4Load balancer IPv4
ip (IPv6)Load balancer IPv6Client IPv6

This is a quiet one. Enabling dual-stack on an existing load balancer is usually framed as additive, and for connectivity it is. For anything keyed on source address, IPv6 clients reaching IPv4 targets arrive looking like the load balancer — so a rule that worked for every client last week now fails for the subset that happened to prefer IPv6.

Preservation decides what your target's security group must allow

This is the coupling people miss, and it is why toggling the attribute can take an application down.

If the load balancer has a security group, reference that security group from the target's rules. If the load balancer has no security group, what you must allow depends on preservation:

PreservationTarget security group must allow
EnabledTraffic from approved client addresses
DisabledTraffic from the load balancer's private IPs
EitherHealth checks from the load balancer's private IPs

So a target group whose security group allows the load balancer's private addresses works fine with preservation off, and starts dropping client traffic the moment you turn preservation on — because the packets now arrive from the client, which that rule never permitted. The health checks keep passing throughout, because they still originate from the load balancer. A target that is "healthy" and refusing every real request is the signature of exactly this.

Four places preservation silently does not apply

Preservation requires the traffic to reach the target directly. Four topologies break that, and none of them produce an error:

  • Through a transit gateway — not supported.
  • Through a Gateway Load Balancer endpoint inspecting traffic between the load balancer and the target — not supported.
  • To a target outside the load balancer's VPC, unless it is a peered VPC in the same Region.
  • Across address families, per the table above.

The first two matter most because they are what a centralized inspection design looks like. If you insert an inspection layer between a Network Load Balancer and its targets, you lose client IP preservation as a side effect of the topology change, and nothing in the inspection rollout will mention it.

Behind an interface endpoint the answer is unconditional. When a consumer sends traffic to a service through an interface endpoint, the source addresses the application receives are the private IP addresses of the load balancer nodes, never the consumer's.

  1. EC2

    Consumer instance

    its own private address, in the consumer VPC

  2. VPCE

    Interface endpoint

    endpoint network interface in the consumer VPC

  3. GW

    Network Load Balancer

    rewrites the source to its own node address

  4. DEST

    Provider target

    sees the load balancer, not the consumer

Proxy protocol v2 is the only way to recover the consumer's address here, and it also carries the interface endpoint ID.

Enabling proxy protocol on the load balancer gets you both the consumer address and the endpoint ID from the header, which is the only way a provider can attribute traffic to a specific consumer.

One operational detail if a service is fronted by more than one load balancer: an endpoint service can be associated with multiple Network Load Balancers, and each endpoint network interface binds to one, chosen at random from those in its Availability Zone on the first connection, then reused for every subsequent connection from that interface. AWS recommends identical listener and target group configuration across all of them, because which one a given consumer lands on is not yours to decide.

Proxy protocol v2 is the way out, and it breaks health checks first

Setting proxy_protocol_v2.enabled on the target group makes the load balancer prepend a PPv2 header carrying the original connection details. Two things about it cause more incidents than the feature itself:

The header is also prepended to health check connections, and on those it carries no client information. So your target has to parse a header that is sometimes informative and sometimes empty.

A target that cannot parse the header fails its health checks — commonly returning HTTP 400: Bad request — which means enabling proxy protocol on a target group whose application does not understand it takes the whole target group out of service. The symptom is not "we can't see client IPs", it is "the service is down", and the change that caused it was intended to improve observability.

Enable it on the application first, confirm it tolerates the header, then enable it on the target group.

An Application Load Balancer does it with a header instead

Because an ALB terminates HTTP, it can pass the client address in a request header rather than the packet. X-Forwarded-For carries it, and the routing.http.xff_header_processing.mode attribute controls what happens to an inbound one:

ModeBehaviour
appendDefault. Adds the last hop's client address to the existing header
preservePasses the header through unchanged
removeStrips the header before forwarding

The left-most address in the list is where the request originated, followed by each proxy in the chain.

AWS's own warning on this page deserves repeating, because X-Forwarded-For is a header and headers are client-controlled: entries "can only be considered trustworthy if added by systems that are properly secured within the network." With the default append mode, a client that sends its own X-Forwarded-For gets its value kept, with the real address appended after it. If your application reads the first entry to identify the client — which is the conventional rule — it reads whatever the client claimed. Either use remove so the load balancer writes the only entry, or count from the right by the number of proxies you actually operate.

What to check

  • Which default is each target group getting. describe-target-group-attributes and read preserve_client_ip.enabled against the target type and protocol, rather than assuming.
  • Whether target security groups reference the client or the load balancer, and whether that matches the preservation setting. This is the pair that breaks.
  • Whether anything sits between the load balancer and the targets. A transit gateway or a Gateway Load Balancer endpoint in that path means preservation is off regardless of the attribute.
  • What the application logs as the source. The authoritative answer is what the target recorded, not what the configuration implies.

Next

Target health and zonal distribution covers the other half of ingress: which Availability Zones receive traffic at all, and why a load balancer with no healthy targets anywhere behaves identically to one that is completely healthy.