Lab 04: The Endpoint That Answered To Nobody
A PrivateLink interface endpoint reports available, DNS resolves to it, the load balancer behind it has a healthy target, and the application security group allows all egress. Every connection still times out, and nothing in the consumer configuration looks wrong.
- Debugging time
- ~30 min
- Reading time
- 14 min
- Reported by
- Integrations
- Tier
- Specialty
Application cannot reach the vendor API through its new PrivateLink endpoint
Reported by Integrations
- Environment
- staging
- Region
- us-east-1
- Consumer VPC
- 10.20.0.0/16
- Endpoint type
- Interface (PrivateLink)
- Change ref
- CHG-2410 — replace public egress with PrivateLink
We moved the vendor API integration off public internet egress and onto PrivateLink, so the traffic never leaves AWS. The endpoint came up clean. The application cannot connect.
What we have confirmed:
- The VPC endpoint state is available, not
pendingorpendingAcceptance. - The endpoint DNS name resolves from the application host, to an address inside our own subnet.
- The application's security group allows all egress. We widened it to be certain.
- There are no custom network ACLs on the subnet — it is the VPC default, allow all.
- The route table is untouched. The endpoint address is in our own VPC CIDR, so it is a local route.
- On the provider side the load balancer target is healthy.
Every layer we know how to check reports healthy, and curl hangs until it times out. We are
starting to think PrivateLink is broken, which we accept is unlikely.
What you are working with
Two VPCs. The provider hosts a service behind an internal Network Load Balancer, exposed as a VPC endpoint service. The consumer reaches it through an interface endpoint. There is no peering, no Transit Gateway, and no internet path between them — PrivateLink is the only connection.
Both subnets are public only so Session Manager can reach the hosts; the failing request never uses that path. The backend security group permits 8080 from all of 10.0.0.0/8 and the application's own security group permits all egress, so neither of the two security groups written in the Terraform is restricting this flow. Note what the endpoint's ENI is drawn as: an interface in the consumer's subnet, which means it has security groups of its own.
- EC2
Application host
10.20.1.30 in the consumer VPC
- FILTER
Application security group — egress
sg-app — allow all outbound
- RTB
Consumer VPC route table
10.20.0.0/16 → local — the endpoint ENI is in this subnet
- VPCE
Interface endpoint ENI
vpce-… — state: available
- GW
Endpoint service → NLB
healthy target in the provider VPC
- DEST
Service backend
10.10.1.20:8080 — listening
Everything the team checked is on this diagram and all of it passes. The hop they did not think to inspect is the endpoint itself.
Scope and constraints
- In scope: the interface endpoint and what governs traffic reaching it.
- Out of scope: the endpoint service, the NLB, its target group health, the backend service, DNS, routing, and network ACLs. All are correct — the ticket's checks were accurate.
- The consumer's application security group is not the problem. It permits all egress and widening it further will not help. Read that sentence carefully; it is true and it is also a hint.
- You get a shell on both hosts. The backend is reachable directly from within the provider VPC if you want to confirm the service works.
Deploy the broken state
Download the file below into an empty directory and apply it. Expect five to seven minutes — the NLB and the endpoint service both take a while to become available.
terraform init
terraform apply
# Shell on the consumer application host
terraform output -raw consumer_session_command
# The URL the application uses
terraform output -raw endpoint_urlFull source: main.tf. It builds both VPCs, the
internal NLB with a health-checked target group, the endpoint service with the consumer registered
as an allowed principal, the interface endpoint, and one instance per side.
Two implementation notes so they do not read as bugs. private_dns_enabled is false because
private DNS on a custom endpoint service requires domain ownership verification — consumers use
the generated endpoint DNS name, which is the normal pattern for third-party services.
The target group also sets preserve_client_ip = "false", and it is worth being precise about why,
because the obvious reading is wrong. It is not what stops the backend seeing a consumer address.
AWS documents that client IP preservation has no effect on inbound traffic from
PrivateLink:
the source address of PrivateLink traffic is always the private address of the Network Load
Balancer, whichever way the attribute is set. A consumer's private IP never crosses the PrivateLink
boundary, so the backend could not see one to begin with.
Disabling it is therefore a no-op for this lab's data path. What it does buy you in a real deployment is documented on the same page: with preservation off, an NLB supports roughly 55,000 simultaneous connections per unique target, and exceeding that produces port allocation errors. It is set here for symmetry with how you would build the provider side for real, not because the lab depends on it.
Get the target:
terraform output -raw endpoint_url
# http://vpce-0abc123-def456.vpce-svc-0789xyz.us-east-1.vpce.amazonaws.com:8080/Open a shell on the consumer application host:
aws ssm start-session --region us-east-1 --target <app instance id>Confirm the failure
URL=$(terraform output -raw endpoint_url) # or paste it in
curl -sS -m 10 "$URL"
# curl: (28) Connection timed out after 10002 millisecondsA timeout. By now that should immediately tell you the packet is being discarded somewhere rather than actively refused.
Confirm DNS is not the problem
ENDPOINT_HOST=vpce-0abc123-def456.vpce-svc-0789xyz.us-east-1.vpce.amazonaws.com
dig +short "$ENDPOINT_HOST"
# 10.20.1.87That address is inside the consumer subnet — it is the endpoint's own network interface. DNS is doing exactly the right thing, and the destination is one hop away on the local subnet.
Connect straight to the address to take DNS out of the picture entirely:
nc -vz -w 8 10.20.1.87 8080
# Ncat: Connection timed out.Confirm the service itself works
Worth eliminating so you are not chasing a broken backend. Open a shell on the provider host and hit the service locally, then through the NLB:
# On the provider backend host
curl -sS -m 5 http://localhost:8080/
# vn-lab-04 provider backend okAnd confirm the target group agrees:
aws elbv2 describe-target-health \
--target-group-arn "$(terraform output -raw target_group_arn)" \
--query 'TargetHealthDescriptions[].TargetHealth.State' --output text
# healthyThe service is up and the load balancer knows it.
Look at the endpoint itself
The team verified the endpoint's state. Look at its configuration:
aws ec2 describe-vpc-endpoints \
--vpc-endpoint-ids "$(terraform output -raw vpc_endpoint_id)" \
--query 'VpcEndpoints[0].{State:State,Type:VpcEndpointType,Groups:Groups,NetworkInterfaces:NetworkInterfaceIds}' \
--output jsonRead the Groups field. Then compare it against the application's own security group:
echo "app sg: $(terraform output -raw app_security_group_id)"
echo "consumer default: $(terraform output -raw consumer_default_security_group_id)"If those differ, work out what the group attached to the endpoint actually permits:
aws ec2 describe-security-groups \
--group-ids "$(terraform output -raw consumer_default_security_group_id)" \
--query 'SecurityGroups[0].IpPermissions' --output jsonSign in to read the debrief
This debrief is free — it just needs an account. One covers every free lab, on any device, and there is no card and no trial.
Solve it yourself first if you can. The brief, the reproduction steps, and the lab’s Terraform are all open and need no account.