CoursesAWS security engineeringPrivate endpoints & east-west TLS

Private endpoints & east-west TLS

PrivateLink, endpoint policies, mTLS internally.

Advanced30 min · lesson 12 of 15

A backend service sitting in a private subnet calls S3, Amazon's file storage, to read one file. Nothing about that sounds dangerous. Yet by default that call leaves your network, crosses the public internet, and comes back. That round trip is where a surprising amount of AWS data quietly gets exposed. The fix is simple to say: stop walking outside. Bring the service into your own building and keep every hop indoors.

Bring the service inside the VPC

A VPC, short for Virtual Private Cloud, is your own fenced-off network inside AWS, a private slice of their data centre that only you control. S3, DynamoDB and Secrets Manager (Amazon's file store, database and password vault) normally sit outside that fence, each with a public front door. Reaching them means routing out to the internet and back, even though both ends live in the same AWS region. It's two rooms in the same building where the only way across is to walk out the front door, down the public street, and back in through the other entrance. A VPC endpoint builds a private hallway between the rooms instead. The traffic never touches the street.

There are two kinds, and the difference is mechanical. A gateway endpoint is free and works for exactly two services, S3 and DynamoDB. It's really just an entry in your route table, the list of directions that tells traffic where to go, saying: anything bound for S3, send it down this private path. An interface endpoint, which AWS also calls PrivateLink, is an actual network card, an elastic network interface (ENI), planted inside your subnet with its own private IP address. It covers almost every other AWS service and plenty of partner APIs. It carries a small hourly fee per availability zone plus a per-gigabyte charge, so you point it at the services you genuinely call rather than blanketing the whole catalogue.

provision a gateway endpoint (S3) and an interface endpoint (Secrets Manager)
$ aws ec2 create-vpc-endpoint \
--vpc-id vpc-0a1b2c3d4e5f6a7b8 \
--vpc-endpoint-type Gateway \
--service-name com.amazonaws.eu-west-1.s3 \
--route-table-ids rtb-0f9e8d7c6b5a4c3d2
{
"VpcEndpoint": {
"VpcEndpointId": "vpce-0c1d2e3f4a5b6c7d8",
"VpcEndpointType": "Gateway",
"ServiceName": "com.amazonaws.eu-west-1.s3",
"State": "available",
"RouteTableIds": ["rtb-0f9e8d7c6b5a4c3d2"],
"CreationTimestamp": "2026-07-16T09:12:44+00:00"
}
}
$ aws ec2 create-vpc-endpoint \
--vpc-id vpc-0a1b2c3d4e5f6a7b8 \
--vpc-endpoint-type Interface \
--service-name com.amazonaws.eu-west-1.secretsmanager \
--subnet-ids subnet-0aa1b2c3 subnet-0dd4e5f6 \
--security-group-ids sg-0e1d2c3b4a596877 \
--private-dns-enabled
{
"VpcEndpoint": {
"VpcEndpointId": "vpce-09f8e7d6c5b4a3f21",
"VpcEndpointType": "Interface",
"ServiceName": "com.amazonaws.eu-west-1.secretsmanager",
"State": "pending",
"PrivateDnsEnabled": true,
"NetworkInterfaceIds": ["eni-0aa11bb22cc33dd44", "eni-0ee55ff66aa77bb88"],
"DnsEntries": [
{"DnsName": "secretsmanager.eu-west-1.amazonaws.com", "HostedZoneId": "Z2GHUB4EXAMPLE"}
]
}
}

Both services now answer from inside the fence, with no internet gateway or NAT device (the box that lets private machines dial out to the internet) anywhere in the path. Turning on private DNS for the interface endpoint quietly rewrites the service's public name to that internal IP. DNS is just the address book of the internet, the thing that turns a name like secretsmanager.eu-west-1.amazonaws.com into an actual address, and here you're swapping the public address for a private one. Your application code never changes. It keeps calling the same hostname as before, and the packets stay indoors. One endpoint serves every subnet in the VPC, so you pay for reach, not per caller.

Put a lock on the hallway door

A private path is not automatically a controlled path. Out of the box an endpoint ships with a wide-open policy, so anything your permissions already allow can flow straight through it. That's what an endpoint policy is for: a second, independent gate. IAM, which stands for Identity and Access Management and is the AWS system that decides who is allowed to do what, is the bouncer at the front door checking who you are. The endpoint policy is a second bouncer standing in the hallway, checking which room you're allowed into, whoever you turn out to be. If a compromised role has broad S3 access, an endpoint policy that names only acme-data means the endpoint itself refuses to carry a request for any other bucket. That's a hard ceiling on data theft, enforced by the network path rather than by identity.

s3-endpoint-policy.json — lock the endpoint to one bucket and one org
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "OnlyAcmeDataFromOurOrg",
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::acme-data",
"arn:aws:s3:::acme-data/*"
],
"Condition": {
"StringEquals": { "aws:PrincipalOrgID": "o-ab12cd34ef" }
}
}
]
}

Attach it, then prove it. Never trust a control you haven't watched turn something away.

attach the policy, then test both an allowed and a denied bucket over the endpoint
$ aws ec2 modify-vpc-endpoint \
--vpc-endpoint-id vpce-0c1d2e3f4a5b6c7d8 \
--policy-document file://s3-endpoint-policy.json
{
"Return": true
}
$ aws s3 ls s3://acme-data/
PRE incoming/
2026-07-15 22:04:11 81920 quarterly.parquet
$ aws s3 ls s3://marketing-open-bucket/
An error occurred (AccessDenied) when calling the ListObjectsV2 operation: User:
arn:aws:sts::111122223333:assumed-role/app-role/i-0ab is not authorized to perform:
s3:ListBucket on resource: "arn:aws:s3:::marketing-open-bucket" because no VPC
endpoint policy allows the s3:ListBucket action

The rejected bucket is the whole point. That request carried valid IAM permissions and still lost, because the destination wasn't on the endpoint's list. A single over-broad IAM policy stops being a one-shot path to every bucket in the account. In a setup with many accounts, the aws:PrincipalOrgID condition earns its place: the endpoint carries a request only if the caller belongs to your organization, so a credential leaked outside your accounts gets nothing even if it somehow reaches the path. Do the same for DynamoDB through its gateway endpoint, and for any sensitive interface endpoint where you want only certain APIs reachable.

Encrypt the hallway, not just the front door

TLS, short for Transport Layer Security, is the lock that scrambles data while it's moving so nobody on the wire can read it. Most teams put that lock on only one door. They terminate TLS at the load balancer and let traffic run in plaintext from there to the real service. Stop the encryption at the front door and every step deeper into the building becomes a conversation shouted across an open hallway. One compromised container, or a quiet packet capture on the wire, and all of that internal service-to-service traffic can be read and rewritten.

The answer is mutual TLS, where both sides present a certificate and prove who they are before a single byte flows, the way two people each show ID before they'll talk business. AWS App Mesh used to be the easy button for this, but it's on the way out. AWS ends support for App Mesh on September 30, 2026, so don't anchor anything new to it. The pattern that lasts is a certificate authority you own plus a service mesh (a layer that rides alongside every service and manages how they talk) that enforces mutual TLS for you. Use AWS Private CA, also called ACM PCA, as your root of trust: the in-house passport office that vouches for every workload's identity. Mint short-lived certificates automatically with the aws-privateca-issuer plugin for cert-manager, and let Istio's Envoy sidecars wrap every east-west call (traffic moving service to service inside your network) in mutual TLS, with certificates that rotate on their own.

enforce STRICT mTLS in the mesh and verify a workload picked it up
$ kubectl apply -f - <<'EOF'
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: payments
spec:
mtls:
mode: STRICT
EOF
peerauthentication.security.istio.io/default created
$ istioctl x describe pod checkout-7d9f4-abc12 -n payments
Pod: checkout-7d9f4-abc12
Pod Revision: default
Pod Ports: 8080 (checkout), 15090 (istio-proxy)
--------------------
Effective PeerAuthentication:
Workload mTLS mode: STRICT
Applied PeerAuthentication:
default.payments
An endpoint policy only guards traffic that uses the endpoint
A gateway endpoint is just an entry in a route table, and it applies only to the subnets whose route table actually points at it. Miss one association and hosts in that subnet still reach S3 the old way, out through the NAT gateway, sailing right past your endpoint policy. The lockdown looks finished in the console while a whole subnet quietly ignores it. After you attach the policy, check every private route table for the endpoint's prefix list (the special route AWS adds for the endpoint), and use VPC flow logs (a record of every connection in and out) to confirm S3 traffic is leaving through the endpoint and not the NAT.
Private endpoints and encrypted east-west traffic
Public internet, present but unused
S3 / DynamoDB / Secrets Manager public endpoints
reachable in theory, your workloads never route here
Inside your VPC
private subnet: app pods + Envoy sidecars
no route to an internet gateway
gateway endpoint (S3, DynamoDB)
route-table entry; endpoint policy scopes buckets and tables
interface endpoint / PrivateLink
ENI with a private IP; private DNS for the service
East-west, encrypted
Envoy mTLS in STRICT mode
every service hop mutually authenticated
AWS Private CA (ACM PCA)
issues short-lived workload identity certificates
Calls to AWS services stay on the backbone; calls between your own services are encrypted and identity-checked. Nothing your workloads depend on crosses the public internet in the clear.
Quick check
01You attach an S3 endpoint policy that allows only the acme-data bucket, yet an EC2 host in one private subnet can still write to a random bucket in another account. What's the most likely reason?
Correct — A gateway endpoint governs only the subnets whose route table points at it; the rest reach S3 the public way and skip the policy entirely.
Incorrect — An endpoint policy applies to any S3 action, including PutObject and ListBucket.
Incorrect — They're independent gates and both must allow the request; IAM does not override the endpoint policy.
Incorrect — The Resource element of an endpoint policy scopes access to named buckets and prefixes.
02What is the mechanical difference between a gateway VPC (virtual private cloud) endpoint and an interface VPC endpoint, which AWS also calls PrivateLink?
Incorrect — Interface endpoints carry an hourly per-availability-zone fee plus a per-gigabyte charge, so they are not free.
Correct — that is exactly the split: a free route-table gateway for two services versus a paid network-card interface endpoint for the rest of the catalogue.
Incorrect — This reverses the two — the gateway is the route-table entry and the interface endpoint is the network card.
Incorrect — Also reversed — S3 and DynamoDB are the gateway-only services, and interface endpoints cover the broad service catalogue.
03A service terminates TLS (Transport Layer Security) at its load balancer and then speaks plaintext to the backend pods behind it. An attacker gains a foothold on one pod in that subnet. According to the lesson, what is the exposure and the durable fix?
Incorrect — AWS does not silently encrypt your service-to-service traffic; the lesson's whole point is that it runs in plaintext past the load balancer.
Incorrect — App Mesh is being retired, with support ending September 30, 2026, so the lesson says not to anchor anything new to it.
Incorrect — Private DNS only rewrites a hostname to a private address; it does nothing to encrypt traffic.
Correct — mutual TLS in STRICT mode makes every hop authenticate and encrypt, using short-lived certificates issued by AWS Private CA and rotated automatically.

Try this

Work through “Encrypt the hallway, not just the front door” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: an endpoint policy only guards traffic that uses the endpoint. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related