Quick take: a gateway endpoint is a route, a interface endpoint is a network card. The gateway type adds one line to your route table so S3 and DynamoDB traffic never leaves the VPC — free, but only those two services. The interface type drops an ENI with a private IP into your subnets, powered by PrivateLink, so any supported service (SSM, KMS, ECR, your own app) answers on a private address — powerful, but you pay per hour and per gigabyte, and a security group on that ENI is the real gate.
Every serious AWS network eventually faces the same question: how does a machine with no public IP, no Internet Gateway, and no NAT still reach S3, pull an image from ECR, be managed by Systems Manager, or call a partner API — without a single packet touching the public internet? The naive answer is “give it a route to a NAT gateway.” That works, but it is the expensive answer: NAT bills you per hour and per gigabyte processed, and it puts your private workloads one misconfiguration away from talking to the internet at large. The right answer, for AWS service traffic and for private service-to-service calls, is a VPC endpoint — and knowing which of the two endpoint types to use is one of the sharpest lines between a junior and a senior AWS network engineer.
There are two endpoint families and they are almost nothing alike under the hood. A gateway endpoint is a target you add to a route table; it works for exactly S3 and DynamoDB, costs nothing, has no ENI, no IP address, and no security group. An interface endpoint is an elastic network interface (ENI) with a private IP that AWS places in your subnets; it is powered by AWS PrivateLink, works for a long list of AWS services and for services you or a partner publish, resolves the service’s normal DNS name privately, is guarded by a security group, and is billed per AZ-hour plus per GB. Confuse the two — reach for an interface endpoint for DynamoDB (it does not exist), or wonder why your “available” SSM endpoint times out (the ENI’s security group blocks 443) — and you will lose an afternoon.
This guide is the senior-engineer treatment. You will learn why keeping traffic off the internet is both a security and a cost decision, the exact mechanics of gateway endpoints (route tables, managed prefix lists, endpoint policies) and interface endpoints (ENIs, private DNS, security groups, PrivateLink), a full side-by-side comparison, how endpoint policies restrict which principals and resources an endpoint may reach, and — the crown jewel — how to expose your own service with an NLB-backed VPC endpoint service that a consumer in another VPC or account reaches through an interface endpoint, with no VPC peering and no CIDR-overlap pain. Then you will build all of it in a hands-on lab: an S3 gateway endpoint, the three interface endpoints that let a private instance use SSM with no NAT, and a tiny PrivateLink service end to end — with aws CLI and Terraform — followed by a troubleshooting playbook for every failure these things throw.
What problem this solves
A private subnet is a subnet whose route table has no 0.0.0.0/0 route to an Internet Gateway. Instances there cannot be reached from the internet, and by default cannot reach out either. But they still need AWS services: pushing logs to CloudWatch, reading objects from S3, being managed by SSM, pulling container images from ECR. You have four ways to bridge that gap, and they differ enormously in cost, security, and blast radius.
| Approach | How traffic leaves | Reaches which targets | Cost model | Security posture |
|---|---|---|---|---|
| Internet Gateway + public IP | Straight to the internet | Anything public | IGW free; you pay egress data | Weakest — instance is internet-exposed |
| NAT gateway in a public subnet | Private → NAT → IGW → internet | Any public endpoint (incl. AWS public APIs) | ~$0.045–0.056/hr per NAT + ~$0.045–0.056/GB processed | Better — one-way egress, but still traverses the internet |
| Gateway endpoint | Route-table target inside the VPC | S3 and DynamoDB only | Free | Strong — traffic stays on the AWS network |
| Interface endpoint (PrivateLink) | ENI with a private IP inside the VPC | Most AWS services + partner + your own | ~$0.01/AZ-hr per endpoint per AZ + ~$0.01/GB | Strongest — private IP, SG-gated, never on the internet |
What breaks when you get this wrong is concrete and expensive. A data-heavy job in a private subnet that reads terabytes from S3 through a NAT gateway pays the NAT data-processing charge on every gigabyte — a bill that a free S3 gateway endpoint erases entirely. A fleet of private instances you “manage with SSM” silently falls off the managed-instances list because the SSM agent cannot reach ssm.<region>.amazonaws.com without a NAT or the right interface endpoints. A compliance auditor asks you to prove that your S3 buckets are only reachable from inside the VPC, and you cannot, because your traffic goes out through NAT to the public S3 endpoint like everyone else’s. And a partner integration that “must not traverse the internet” cannot be satisfied by NAT at all — it needs PrivateLink.
Who hits this: every network or platform engineer building a landing zone; anyone running workloads in private subnets (which should be most of them); teams under PCI/HIPAA/RBI mandates that forbid internet paths for regulated data; SaaS builders who need to publish a service to customers privately; and every candidate sitting SAA-C03, ANS-C01 (Advanced Networking), or SCS-C02 (Security), where interface-vs-gateway and PrivateLink are guaranteed territory.
Learning objectives
By the end of this guide you can:
- Explain the two reasons to keep traffic off the internet — security isolation and skipping NAT data-processing cost — and quantify the second.
- Build and reason about a gateway endpoint: the route-table entry, the managed prefix list, the endpoint policy, and why it is S3/DynamoDB-only and free.
- Build and reason about an interface endpoint: the ENI and private IP, PrivateLink, private DNS, the security group on the ENI, and the per-hour + per-GB cost.
- Choose interface vs gateway for any given service with a decision table, and know the traps (DynamoDB has no interface endpoint; S3 has both).
- Write endpoint policies that restrict which principals and resources an endpoint may reach, and pair them with
aws:sourceVpceconditions on resource policies. - Stand up a VPC endpoint service: an NLB fronting your app, allowed principals, acceptance, custom private DNS, consumed by an interface endpoint in another VPC/account with no peering and overlapping CIDRs allowed.
- Handle cross-region and hybrid (on-prem) access correctly — including why a gateway endpoint is unreachable from on-prem.
- Diagnose every endpoint failure: unreachable S3 via gateway, DNS still resolving public, SG blocking the ENI,
pendingAcceptance, cross-account allow-list, and region mismatch.
Prerequisites & where this fits
This is an advanced networking topic. You should already be fluent with VPC fundamentals; if any row below is shaky, read the linked foundation first.
| You should know | Why it matters here | Where to get it |
|---|---|---|
| VPCs, subnets, route tables, security groups | Endpoints live in subnets; gateway endpoints ARE route-table entries; interface endpoints have SGs | AWS VPC, Subnets & Security Groups Explained |
| NAT gateways & private-subnet egress | Endpoints are what let you drop NAT for AWS-service traffic | NAT Gateway & Private-Subnet Egress Hands-On |
| VPC peering & its limits | PrivateLink is the alternative to peering for one-way service access with overlapping CIDRs | VPC Peering: Setup, Routing & Limits |
| Transit Gateway hub-and-spoke | Where centralized (shared) interface endpoints live in a multi-VPC estate | Transit Gateway Hub-and-Spoke Hands-On |
| IAM policy evaluation & cross-account roles | Endpoint policies and allowed-principals are IAM; two-sides-allow logic applies | IAM Policy Evaluation & AccessDenied Troubleshooting |
Basic aws CLI + a sandbox VPC |
You will run real commands | AWS CLI v2 configured |
Where this fits: VPC endpoints are the connective tissue that lets a well-segmented network stay well-segmented. In a landing zone you typically run a set of centralized interface endpoints in a shared-services VPC reachable over Transit Gateway, plus a free gateway endpoint in every VPC that touches S3 or DynamoDB. PrivateLink is also how you consume third-party SaaS privately and how you publish your own internal platform services across account boundaries. Master this and you rarely need NAT for anything except genuine internet calls.
Core concepts
Three ideas carry the topic: a VPC endpoint is a private on-ramp to a service; there are two structurally different types; and PrivateLink is the technology that makes the interface type (and endpoint services) work.
The mental model: a private door into a service
A VPC endpoint lets resources in your VPC connect to a service using private IP addressing, so the traffic stays on the AWS network and never traverses the public internet or needs an Internet Gateway, NAT device, or public IP. That is the whole idea. The two implementations of that idea could not be more different.
| Term | What it is | Concrete form |
|---|---|---|
| VPC endpoint | A private on-ramp from your VPC to a service | vpce-0a1b2c3d4e5f |
| Gateway endpoint | A route-table target for S3/DynamoDB | Route: pl-78a54011 → vpce-… |
| Interface endpoint | An ENI with a private IP, PrivateLink-powered | ENI eni-…, IP 10.0.1.50 |
| AWS PrivateLink | The tech carrying interface-endpoint & endpoint-service traffic privately | (a capability, not an ARN) |
| Endpoint service | Your service published for others to reach via PrivateLink | com.amazonaws.vpce.ap-south-1.vpce-svc-… |
| Managed prefix list | An AWS-maintained set of a service’s CIDRs, referenced by route/SG | pl-78a54011 (com.amazonaws.ap-south-1.s3) |
| Private DNS | Makes the service’s public name resolve to the ENI inside the VPC | ssm.ap-south-1.amazonaws.com → 10.0.1.50 |
| Endpoint policy | A resource policy on the endpoint restricting principals/resources | JSON attached to the vpce-… |
| Service name | The identifier you point an endpoint at | com.amazonaws.ap-south-1.ssm |
The two types at a glance
Keep this teaser table in your head; the rest of the article expands every row.
| Dimension | Gateway endpoint | Interface endpoint |
|---|---|---|
| Supported services | S3 and DynamoDB only | ~100+ AWS services, partner (Marketplace), and your own |
| Underlying mechanism | Route-table entry to a managed prefix list | An ENI with a private IP (PrivateLink) |
| Cost | Free | ~$0.01/AZ-hour + ~$0.01/GB processed |
| Has an IP / ENI? | No | Yes — one ENI per AZ subnet |
| Security group? | No (controlled by route + policy) | Yes — the SG on the ENI is the gate |
| Changes DNS? | No | Optionally — private DNS for the public name |
| Reachable from on-prem (DX/VPN)? | No | Yes |
| Cross-Region targets? | No (same-Region only) | No (endpoint is regional) |
What PrivateLink actually is
AWS PrivateLink is the service that connects a consumer to a provider over the AWS backbone using an interface endpoint (an ENI) on the consumer side and a Network Load Balancer on the provider side, without exposing anything to the public internet, without VPC peering, and — crucially — regardless of whether the two VPCs have overlapping CIDR blocks. Traffic is unidirectional (consumer initiates to provider) and is effectively NATed through the ENI, which is why overlapping addresses do not matter. Every interface endpoint you create for an AWS service is PrivateLink under the hood; publishing your own endpoint service is the same machinery pointed at your NLB.
Why keep traffic off the internet
There are two independent reasons, and both matter on their own.
Security: shrink the reachable surface
| Security benefit | Gateway endpoint | Interface endpoint | Why it matters |
|---|---|---|---|
| No Internet Gateway needed | Yes | Yes | The subnet can be truly private — nothing routes to 0.0.0.0/0 |
| No public IP on the workload | Yes | Yes | Nothing is directly addressable from the internet |
| Traffic stays on AWS backbone | Yes | Yes | Data never transits the public internet — satisfies many compliance mandates |
| Restrict which principals/resources | Endpoint policy | Endpoint policy + SG | Only your accounts/buckets are reachable through the on-ramp |
| Prove access is VPC-only | aws:sourceVpce in bucket policy |
aws:sourceVpce / aws:sourceVpc |
Auditor-grade proof that a bucket is only reachable via the endpoint |
| Block data exfiltration to other accounts’ buckets | Endpoint policy Resource restriction |
Endpoint policy | Stops a compromised host writing to an attacker’s S3 bucket |
The exfiltration point is the one seniors care about most. With a NAT path, a private host can reach any S3 bucket on the internet, including an attacker-controlled one. With a gateway endpoint carrying a tight endpoint policy (Resource limited to your buckets), that same host can only talk to your S3 — the endpoint becomes a data-egress firewall for S3/DynamoDB.
Cost: the NAT bill you stop paying
For S3/DynamoDB traffic the money argument is decisive because the gateway endpoint is free and removes NAT data-processing charges.
| Path for S3 traffic | Hourly | Per-GB processed | 10 TB/month example (approx) |
|---|---|---|---|
| Through a NAT gateway | ~$0.045–0.056/hr per NAT | ~$0.045–0.056/GB | ~$32/mo NAT-hours + ~$460–560 data |
| Through a gateway endpoint | $0 | $0 | ~$0 (you still pay S3 request/storage) |
| Through an interface endpoint (S3) | ~$0.01/AZ-hr | ~$0.01/GB | ~$22/mo (3 AZ) + ~$100 data |
For a chatty S3 workload, the free gateway endpoint can save hundreds of dollars a month versus NAT — the single easiest network cost win in AWS. For interface endpoints the calculus is subtler: you add a per-hour + per-GB cost, but you remove NAT for that service’s traffic and gain private, SG-gated access. The rule of thumb: use the free gateway endpoint for S3/DynamoDB always; use interface endpoints for the AWS services your private workloads actually call, and consolidate them.
Gateway endpoints in depth
A gateway endpoint is the simpler, older mechanism. It is not an appliance — it is a route. When you create one, AWS adds a route to the route tables you associate with it: destination = the service’s managed prefix list (e.g. pl-78a54011 for S3 in ap-south-1), target = the endpoint id. Any instance whose subnet uses that route table now reaches S3 over the AWS network instead of via IGW/NAT.
The defining facts
| Fact | Gateway endpoint behaviour |
|---|---|
| Services | S3 and DynamoDB only — nothing else |
| Mechanism | A route in a route table to a managed prefix list |
| ENI / IP address | None |
| Security group | None (there is no ENI to attach one to) |
| Cost | Free — no hourly, no data-processing charge |
| DNS | Unchanged — public service names still resolve to public IPs; the route is what redirects |
| Region scope | Same-Region service only (a ap-south-1 VPC reaches ap-south-1 S3) |
| On-prem (DX/VPN) reachable? | No — the prefix-list route is local to the VPC route table |
| Peered / TGW VPC reachable? | No — cannot be used across peering, TGW, VPN, or DX |
| Access control | Route-table association + endpoint policy + the resource’s own policy (bucket policy) |
| High availability | Managed and highly available by design; no AZ choice to make |
| Quota | ~20 gateway endpoints per Region (soft, raise via Service Quotas) |
Settings you actually set
| Setting | Values | Default | Notes / gotcha |
|---|---|---|---|
--vpc-endpoint-type |
Gateway |
— | Must be Gateway (default for create-vpc-endpoint is Gateway) |
--service-name |
com.amazonaws.<region>.s3 / .dynamodb |
— | Region must match the VPC’s Region |
--route-table-ids |
one or more rtb-… |
none | If you forget this, nothing routes to the endpoint |
--policy-document |
JSON | full access (*) |
Default allows everything; tighten it |
| Prefix list | auto-created reference | pl-… |
Reference pl-… in SG/route rules; do not hardcode CIDRs |
The endpoint policy
Every gateway endpoint carries a resource policy. The default is full access — Principal:*, Action:*, Resource:* — which most teams never tighten and should. Restricting Resource to your own buckets turns the endpoint into an S3 egress control.
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "OnlyKloudVinBuckets",
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::kloudvin-data",
"arn:aws:s3:::kloudvin-data/*"
]
}]
}
| Endpoint-policy element | Purpose | Gotcha |
|---|---|---|
Principal |
Who (usually * — the VPC’s own identities) |
The identity’s IAM policy still applies separately |
Action |
Which S3/DynamoDB actions may traverse the endpoint | Narrower than the identity policy = an extra ceiling |
Resource |
Which buckets/tables are reachable via this endpoint | The exfiltration control — limit to your ARNs |
Condition |
e.g. aws:PrincipalOrgID |
Restrict to your Organization’s principals |
| Default | Full access if you set nothing | Least-privilege means you should set something |
Create it — CLI and Terraform
# Gateway endpoint for S3, wired into a route table, with a scoped policy
aws ec2 create-vpc-endpoint \
--vpc-id vpc-0abc123 \
--service-name com.amazonaws.ap-south-1.s3 \
--vpc-endpoint-type Gateway \
--route-table-ids rtb-0priv1 rtb-0priv2 \
--policy-document file://s3-endpoint-policy.json \
--query "VpcEndpoint.{Id:VpcEndpointId,State:State}"
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.ap-south-1.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = [aws_route_table.private_a.id, aws_route_table.private_b.id]
policy = data.aws_iam_policy_document.s3_endpoint.json
tags = { Name = "s3-gw" }
}
resource "aws_vpc_endpoint" "dynamodb" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.ap-south-1.dynamodb"
vpc_endpoint_type = "Gateway"
route_table_ids = [aws_route_table.private_a.id, aws_route_table.private_b.id]
}
Verify the route landed: aws ec2 describe-route-tables --route-table-ids rtb-0priv1 --query "RouteTables[].Routes[?GatewayId!=null]" should show a route whose DestinationPrefixListId is the S3 prefix list and whose GatewayId is your vpce-….
Interface endpoints & PrivateLink in depth
An interface endpoint is a different animal: AWS provisions an ENI with a private IP in each subnet (AZ) you select, and PrivateLink carries traffic from that ENI to the target service over the backbone. Because it is a real network interface, it has a security group, it can be reached from on-premises over Direct Connect or VPN, and — with private DNS — the service’s normal public hostname resolves to the ENI so your existing SDK code “just works.”
The defining facts
| Fact | Interface endpoint behaviour |
|---|---|
| Services | Most AWS services (SSM, EC2, KMS, ECR, CloudWatch, Secrets Manager, STS, SNS, SQS, …), partner services, your own |
| Mechanism | One ENI with a private IP per selected AZ subnet, via PrivateLink |
| Security group | Yes — must allow inbound 443 from consumers |
| Cost | ~$0.01/AZ-hour per endpoint per AZ + ~$0.01/GB processed (region-dependent) |
| DNS | Endpoint-specific names always; optionally private DNS for the public service name |
| Region scope | Regional resource; same-Region service |
| On-prem (DX/VPN) reachable? | Yes — it is a private IP in your VPC |
| Peered / TGW VPC reachable? | Yes — reachable across peering/TGW (unlike gateway endpoints) |
| High availability | You choose the AZs; put an ENI in each AZ you use |
| Quota | ~50 interface endpoints per VPC (soft) |
Common interface-endpoint service names
You will reach for these constantly. Every name is com.amazonaws.<region>.<suffix>; the suffix is what changes.
| Service | Endpoint suffix(es) | Needs an S3 gateway too? | Typical reason |
|---|---|---|---|
| Systems Manager | ssm, ssmmessages, ec2messages |
No | Session Manager / Run Command with no NAT |
| EC2 API | ec2 |
No | Instance metadata-driven tooling, ASG hooks |
| ECR | ecr.api, ecr.dkr |
Yes (image layers live in S3) | Private container image pulls |
| KMS | kms |
No | Envelope encryption/decryption privately |
| Secrets Manager | secretsmanager |
No | Fetch secrets from private subnets |
| STS | sts |
No | AssumeRole from private workloads |
| CloudWatch | monitoring, logs |
No | Push metrics and logs privately |
| SNS / SQS | sns, sqs |
No | Private pub/sub and queueing |
| S3 (special) | s3 (interface) |
n/a | On-prem / cross-VPC S3, not default in-VPC |
| Kinesis | kinesis-streams |
No | Private streaming ingest |
| ELB / Auto Scaling | elasticloadbalancing, autoscaling |
No | Control-plane calls from private hosts |
Interface-endpoint limits & quotas
| Limit | Value (default) | Notes |
|---|---|---|
| Interface endpoints per VPC | ~50 (soft) | Raise via Service Quotas; centralize to avoid hitting it |
| ENIs per endpoint | one per selected AZ | Choose only the AZs you use |
| Endpoint policy size | 20,480 characters | Same ceiling as most IAM documents |
| Idle timeout (to AWS service) | Managed by the service | For your own service via NLB it is the NLB’s 350 s |
| Port | 443 (HTTPS) for AWS services | Your own endpoint service uses the NLB’s listener port |
| Cross-Region | Not supported | The endpoint is a regional resource |
Settings you actually set
| Setting | Values | Default | Notes / gotcha |
|---|---|---|---|
--vpc-endpoint-type |
Interface |
Gateway |
Must explicitly set Interface |
--subnet-ids |
subnets, one per AZ | none | Each becomes an ENI; pick every AZ your workload runs in |
--security-group-ids |
sg-… |
VPC default SG | The default SG may not allow 443 — the #1 hang cause |
--private-dns-enabled |
true/false |
true (most services) |
Requires VPC enableDnsSupport + enableDnsHostnames |
--policy-document |
JSON | full access | Same idea as gateway policy |
--ip-address-type |
ipv4/ipv6/dualstack |
ipv4 |
Match the service + subnet capability |
Private DNS — the piece that makes it seamless
Without private DNS, an interface endpoint only answers on endpoint-specific DNS names. With private DNS enabled, AWS creates a Route 53 private hosted zone so the default public name resolves to the ENI inside the VPC.
| DNS name type | Example | When you get it |
|---|---|---|
| Default public (private-DNS ON) | ssm.ap-south-1.amazonaws.com → ENI IP |
private_dns_enabled = true + VPC DNS flags on |
| Regional endpoint-specific | vpce-0a1b-xyz.ssm.ap-south-1.vpce.amazonaws.com |
Always created |
| Zonal endpoint-specific | vpce-0a1b-xyz-ap-south-1a.ssm.…vpce.amazonaws.com |
Always created (per AZ) |
| Requirement | enableDnsSupport = true and enableDnsHostnames = true on the VPC |
Else private DNS silently does nothing |
The practical rule: enable private DNS so existing code that calls ssm.<region>.amazonaws.com transparently uses the endpoint — but know that this is exactly where the nastiest conflicts live (two endpoints for one service; an S3 interface endpoint hijacking the S3 gateway path — covered in troubleshooting).
Create it — CLI and Terraform (SSM example)
aws ec2 create-vpc-endpoint \
--vpc-id vpc-0abc123 \
--service-name com.amazonaws.ap-south-1.ssm \
--vpc-endpoint-type Interface \
--subnet-ids subnet-0priv-a subnet-0priv-b \
--security-group-ids sg-0endpoint \
--private-dns-enabled \
--query "VpcEndpoint.{Id:VpcEndpointId,State:State}"
resource "aws_security_group" "endpoints" {
name = "vpce-ingress"
vpc_id = aws_vpc.main.id
ingress { # the gate for every interface endpoint
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [aws_vpc.main.cidr_block]
}
egress { from_port = 0, to_port = 0, protocol = "-1", cidr_blocks = ["0.0.0.0/0"] }
}
locals { ssm_services = ["ssm", "ssmmessages", "ec2messages"] }
resource "aws_vpc_endpoint" "ssm" {
for_each = toset(local.ssm_services)
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.ap-south-1.${each.key}"
vpc_endpoint_type = "Interface"
subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id]
security_group_ids = [aws_security_group.endpoints.id]
private_dns_enabled = true
tags = { Name = "vpce-${each.key}" }
}
Interface vs gateway — the full comparison
This is the table the exam and the design review both come back to. Read it once, top to bottom.
| Dimension | Gateway endpoint | Interface endpoint (PrivateLink) |
|---|---|---|
| Supported services | S3, DynamoDB only | ~100+ AWS services + partner + your own |
| Underlying object | Route-table entry → managed prefix list | ENI with a private IP per AZ |
| Powered by | Route table | AWS PrivateLink |
| Cost | Free | ~$0.01/AZ-hr + ~$0.01/GB |
| IP address in your VPC | No | Yes (one per AZ) |
| Security group | No | Yes (on the ENI) |
| Endpoint policy | Yes | Yes |
| DNS behaviour | Unchanged (route redirects) | Optional private DNS for the public name |
| Access from on-prem via DX/VPN | No | Yes |
| Access from peered/TGW VPC | No | Yes |
| Cross-Region service | No | No (endpoint is regional) |
| High availability | Managed, automatic | You pick AZs — use ≥2 |
| Typical use | S3/DynamoDB traffic in-VPC, free | SSM, ECR, KMS, Secrets, your own service, hybrid |
| Failure signature | Route/prefix-list/policy issue | SG blocks 443, private-DNS conflict, pendingAcceptance |
Which endpoint for which service
| Service | Gateway | Interface | Recommended |
|---|---|---|---|
| S3 | Yes (free) | Yes (billed) | Gateway in-VPC; interface only for on-prem / cross-VPC / PrivateLink-to-S3 needs |
| DynamoDB | Yes (free) | No | Gateway (there is no interface endpoint for DynamoDB) |
| Systems Manager (ssm, ssmmessages, ec2messages) | No | Yes | Interface (all three) |
| ECR (api + dkr) + S3 | No (ECR) / S3 gateway | Yes | Interface for ECR api/dkr plus an S3 gateway for image layers |
| KMS, Secrets Manager, STS | No | Yes | Interface |
| CloudWatch (monitoring, logs), SNS, SQS | No | Yes | Interface |
| Your own / partner service | No | Yes | Interface to an endpoint service |
Decision table
| If you need… | It’s probably… | Do this |
|---|---|---|
| Private S3/DynamoDB from inside the VPC, cheapest | Gateway endpoint | Create gateway EP, associate route tables, tighten policy |
| Private S3 reachable from on-premises | Interface endpoint for S3 | Create an S3 interface endpoint (gateway can’t reach on-prem) |
| A private instance managed by SSM with no NAT | Three interface endpoints | ssm + ssmmessages + ec2messages, private DNS on, SG 443 |
| To pull ECR images privately | Interface + gateway mix | ecr.api + ecr.dkr interface EPs and an S3 gateway EP |
| To publish your app to another VPC/account privately | Endpoint service | NLB → endpoint service → consumer interface endpoint |
| Consumer & provider VPCs have overlapping CIDRs | PrivateLink | Endpoint service — peering/TGW would fail on overlap |
S3 — the one service with both endpoint types
S3 is the only service where you genuinely have to choose, and the choice has cost and DNS consequences. Read this before you ever create an S3 interface endpoint.
| Aspect | S3 gateway endpoint | S3 interface endpoint |
|---|---|---|
| Cost | Free | ~$0.01/AZ-hr + ~$0.01/GB |
| In-VPC access | Yes (the default choice) | Yes (but you pay) |
| On-prem (DX/VPN) access | No | Yes — the reason to use it |
| Cross-VPC (peered/TGW) access | No | Yes |
| DNS | Unchanged; route redirects | Endpoint-specific; private DNS optional and dangerous here |
| Private-DNS side effect | None | Enabling it reroutes all in-VPC S3 onto the billed endpoint |
| Recommended default | Gateway for in-VPC | Interface only for on-prem/cross-VPC, private DNS off |
Endpoint policies
An endpoint policy is a resource-based policy attached to the endpoint (gateway or interface) that acts as an additional filter on what can pass through it. It does not grant permissions — the caller’s IAM policy and the resource’s own policy still apply — it only restricts. Effective access is the intersection of the identity policy, the resource policy, and the endpoint policy.
| Aspect | Endpoint policy |
|---|---|
| Type | Resource-based policy attached to the vpce-… |
| Grants access alone? | No — restrict-only; the identity/resource policies still gate |
| Default | Full access (Principal:*, Action:*, Resource:*) |
| Gateway vs interface | Same concept; both support it |
| Best use | Limit reachable buckets/tables/actions through the on-ramp |
| Pairs with | aws:sourceVpce / aws:sourceVpc conditions on the resource policy |
The powerful pattern is the two-way lock: an endpoint policy limits what the endpoint can reach, and a bucket policy on the resource requires that access arrive through the endpoint. Together they make a bucket reachable only from your VPC, only for your principals.
// Bucket policy: deny any access that did NOT come through our VPC endpoint
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "DenyUnlessViaEndpoint",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::kloudvin-data", "arn:aws:s3:::kloudvin-data/*"],
"Condition": { "StringNotEquals": { "aws:sourceVpce": "vpce-0a1b2c3d4e5f" } }
}]
}
| Condition key | Meaning | Where you use it |
|---|---|---|
aws:sourceVpce |
Request came through this endpoint id | Bucket/key/queue policy — pin access to the endpoint |
aws:sourceVpc |
Request originated from this VPC | Broader than sourceVpce |
aws:VpcSourceIp |
Source IP within the VPC | Narrow to a subnet/host range |
aws:PrincipalOrgID |
Caller belongs to your Org | Endpoint policy — trust the whole org |
Expose your own service with PrivateLink
This is the capability that turns “VPC endpoints” from a cost/security tweak into an architecture pattern. You can publish your own service so that consumers — in another VPC, another account, even a customer’s account — reach it through an interface endpoint, privately, with no VPC peering, and with overlapping CIDRs allowed because traffic is one-way and NATed through the ENI.
The components
| Component | Role | Notes |
|---|---|---|
| Network Load Balancer (NLB) | Fronts your targets (the “provider” side) | Internal NLB; targets can be instances, IPs, or an ALB (for L7) |
| VPC endpoint service | The published service backed by the NLB | Gets a name com.amazonaws.vpce.<region>.vpce-svc-… |
| Allowed principals | Account/role/user ARNs permitted to connect | Default: none but you; add ARNs or * for public |
| Acceptance | Provider approves each consumer connection | AcceptanceRequired=true by default → pendingAcceptance |
| Interface endpoint (consumer) | The consumer’s on-ramp to your service | Created against the vpce-svc-… name |
| Custom private DNS name | Optional friendly name (e.g. api.kloudvin.internal) |
Requires TXT domain-ownership verification |
Provider vs consumer responsibilities
| Step | Provider (service owner) | Consumer (service user) |
|---|---|---|
| 1 | Deploy targets + internal NLB on the service port | — |
| 2 | Create endpoint service pointing at the NLB | — |
| 3 | Add consumer ARNs to AllowedPrincipals | Get told the vpce-svc-… name |
| 4 | (Optional) set up custom private DNS + TXT verify | — |
| 5 | Accept the connection (or auto-accept via SNS→Lambda) | Create an interface endpoint to the service name |
| 6 | Watch connection notifications | Enable private DNS / use endpoint DNS name |
NLB & endpoint-service settings
| Setting | Values / default | Notes |
|---|---|---|
| NLB scheme | internal | Public NLB defeats the purpose |
| NLB idle timeout | 350 s (TCP), fixed | Long-lived flows need keepalives < 350 s |
| Cross-zone load balancing | off by default on NLB | Turn on so all AZs’ targets are used |
AcceptanceRequired |
true (default) |
false for hands-off onboarding of trusted principals |
AllowedPrincipals |
list of ARNs / * |
* makes discovery public (still needs acceptance unless off) |
| Supported AZs | the NLB’s AZs | Consumers must have a subnet in a shared AZ (compare AZ IDs, not names) |
| Private DNS name | your FQDN | Needs a public-DNS TXT record; state must be verified |
| Client IP preservation | NLB target-group setting | Off = targets see the endpoint’s IP; on = the client’s |
Endpoint-service limits & quotas
| Limit | Value (default) | Notes |
|---|---|---|
| Endpoint services per Region | soft quota | Raise via Service Quotas |
| Allowed principals per service | soft quota | Add exact ARNs; * = public discovery |
| Connections per endpoint service | soft quota | Scales with the NLB behind it |
| NLB idle timeout | 350 s (fixed) | Use TCP keepalives for long-lived flows |
| Load balancer type | NLB (or GWLB for appliances) | ALB may sit behind the NLB as a target |
| Consumer AZ requirement | shares an AZ ID with the provider NLB | Compare AZ IDs (aps1-az1), not names |
| Custom private DNS names | 1 per service | Requires public-DNS TXT verification |
Provider side — CLI and Terraform
# 1) Create the endpoint service in front of an existing internal NLB
aws ec2 create-vpc-endpoint-service-configuration \
--network-load-balancer-arns arn:aws:elasticloadbalancing:ap-south-1:111111111111:loadbalancer/net/kv-svc/abc \
--acceptance-required \
--query "ServiceConfiguration.{Name:ServiceName,State:ServiceState}"
# 2) Allow a specific consumer account/role to discover & connect
aws ec2 modify-vpc-endpoint-service-permissions \
--service-id vpce-svc-0abc123 \
--add-allowed-principals arn:aws:iam::222222222222:root
resource "aws_vpc_endpoint_service" "kv" {
acceptance_required = true
network_load_balancer_arns = [aws_lb.kv_nlb.arn]
allowed_principals = ["arn:aws:iam::222222222222:root"]
private_dns_name = "api.kloudvin.internal" # optional
tags = { Name = "kv-endpoint-service" }
}
Consumer side — CLI and Terraform
# Consumer (account 222…) creates an interface endpoint to the provider's service
aws ec2 create-vpc-endpoint \
--vpc-id vpc-0consumer \
--service-name com.amazonaws.vpce.ap-south-1.vpce-svc-0abc123 \
--vpc-endpoint-type Interface \
--subnet-ids subnet-0c-a subnet-0c-b \
--security-group-ids sg-0consumer-egress \
--query "VpcEndpoint.{Id:VpcEndpointId,State:State}"
# State returns 'pendingAcceptance' until the provider accepts
resource "aws_vpc_endpoint" "consume_kv" {
vpc_id = aws_vpc.consumer.id
service_name = "com.amazonaws.vpce.ap-south-1.vpce-svc-0abc123"
vpc_endpoint_type = "Interface"
subnet_ids = [aws_subnet.c_a.id, aws_subnet.c_b.id]
security_group_ids = [aws_security_group.consumer.id]
private_dns_enabled = false # only true if the provider verified a custom private DNS name
}
Then the provider accepts: aws ec2 accept-vpc-endpoint-connections --service-id vpce-svc-0abc123 --vpc-endpoint-ids vpce-0consumer-ep. The connection flips from pendingAcceptance to available, and the consumer’s app can now reach the provider’s service on the endpoint’s private IP/DNS — no peering, no route to the provider VPC, overlapping CIDRs irrelevant.
Cross-region & hybrid considerations
Endpoints are regional, and the two types behave differently across Region and network boundaries. Get these straight before you design a multi-Region or hybrid estate.
| Scenario | Gateway endpoint | Interface endpoint |
|---|---|---|
| VPC and service in the same Region | Works | Works |
| Reach a service/bucket in another Region | No — same-Region only | No — endpoint is regional; put one in each Region |
| Reach from on-premises (DX/VPN) | No | Yes — it’s a private IP in the VPC |
| Reach from a peered VPC | No | Yes |
| Reach from a TGW-attached VPC | No | Yes (route via TGW to the endpoint’s VPC) |
| Endpoint service consumed cross-Region | n/a | Traditionally same-Region; place a consumer endpoint in the provider’s Region and route to it |
| Hybrid access pattern | How to do it |
|---|---|
| On-prem needs private S3 | An interface endpoint for S3 (gateway can’t serve on-prem), + a Route 53 inbound resolver so on-prem DNS resolves the endpoint |
| On-prem needs private SSM/other AWS service | Interface endpoints + inbound resolver forwarding the service zone |
| Many VPCs need the same interface endpoints | Centralize endpoints in a shared-services VPC, reach them over Transit Gateway, share the private hosted zone |
| DynamoDB from on-prem | No gateway-over-DX; use a DynamoDB interface… which doesn’t exist — front it with your own service or route via a proxy |
The centralization pattern is the money-saver at scale: instead of paying for ssm/ecr/kms/logs interface endpoints in every VPC, you run one set in a shared-services VPC behind Transit Gateway and share the Route 53 private hosted zone across the org — turning N×(per-VPC endpoints) into one set. See Transit Gateway Hub-and-Spoke Hands-On for the routing.
Architecture at a glance
The diagram traces both endpoint paths from a single private subnet, left to right. A private EC2 in the consumer VPC has no public IP and no NAT. For AWS-service traffic it uses VPC endpoints: an interface endpoint (an ENI with a private IP, powered by PrivateLink) whose private DNS makes ssm.<region>.amazonaws.com resolve to that ENI — but only if the security group on the ENI allows 443. That interface endpoint reaches two kinds of target: AWS services like Systems Manager (so the box is managed with no NAT), and your own service published as a VPC endpoint service behind an internal NLB, gated by an allow-list and per-connection acceptance. Separately, a gateway endpoint — just a route to a managed prefix list, free — carries S3 and DynamoDB traffic. Each numbered badge marks where a specific failure bites; the legend narrates every one as symptom, confirm, and fix.
Real-world scenario
KloudVin Health, a fictional but realistic health-tech company in Mumbai (ap-south-1), runs a claims-processing platform under an RBI/HIPAA-style mandate: no regulated data may traverse the public internet. Their VPC has three private application subnets (one per AZ) and, at first, a single NAT gateway for “AWS API calls.” The platform reads and writes ~14 TB/month of claim documents to S3, is managed with Systems Manager, pulls container images from ECR, and calls an internal fraud-scoring service that lives in a separate account run by their data-science team — whose VPC, embarrassingly, uses the same 10.0.0.0/16 CIDR as the app VPC.
The first architecture failed on three fronts. Cost: the 14 TB of S3 traffic through NAT cost roughly $630/month in NAT data processing alone, on top of NAT-hours. Security: an auditor pointed out that any compromised app host could reach any S3 bucket on the internet — the NAT path was a wide-open egress. And connectivity: they could not VPC-peer to the fraud-scoring account because the CIDRs overlapped, so they had bolted on a public HTTPS endpoint for the scoring API — exactly the internet path the mandate forbade.
The rebuild followed this article. They created a free S3 gateway endpoint and a DynamoDB gateway endpoint, associating both with all three private route tables, and attached an endpoint policy limiting Resource to KloudVin’s own buckets — killing both the NAT data-processing bill for S3 and the exfiltration path in one move. They added interface endpoints for ssm, ssmmessages, ec2messages (SSM with no NAT), ecr.api, ecr.dkr (plus the S3 gateway already carried image layers), kms, and logs, all sharing one security group that allows 443 from the VPC CIDR, with private DNS enabled so no application code changed. For the cross-account fraud-scoring call — the overlapping-CIDR problem — the data-science team published their service as a VPC endpoint service behind an internal NLB, added KloudVin Health’s account to AllowedPrincipals, and KloudVin created a consumer interface endpoint. PrivateLink’s one-way, NATed-through-the-ENI design made the overlapping 10.0.0.0/16 irrelevant — the very thing that made peering impossible.
The outcomes matched the design. The S3 NAT data-processing charge went to zero; the interface endpoints added about $45/month (seven services × ~3 AZ-hours + modest data) — a large net saving. The auditor got their proof: a bucket policy with aws:sourceVpce now denies any S3 access that did not arrive through the endpoint. And the fraud-scoring integration runs entirely on the AWS backbone with no internet hop and no peering. The one incident during rollout: the SSM interface endpoints were created against the default security group, which did not allow 443 from the app subnets, so instances showed “Connection lost” in Session Manager for an hour until Flow Logs revealed the ENI REJECTs — the single most common interface-endpoint mistake, and the reason badge #2 exists on the diagram.
Advantages and disadvantages
| Advantages | Disadvantages |
|---|---|
| Traffic stays on the AWS backbone — no internet path | More moving parts than one NAT gateway |
| Gateway endpoints are free and kill NAT data charges for S3/DynamoDB | Interface endpoints add per-AZ-hour + per-GB cost |
| Interface endpoints work for ~all AWS services + your own | DynamoDB has no interface endpoint; S3 has two (confusion) |
| Endpoint policies restrict reachable resources (anti-exfiltration) | Default endpoint policy is wide-open until you tighten it |
| PrivateLink connects overlapping-CIDR VPCs with no peering | PrivateLink is one-way (consumer → provider) only |
| SG on the interface ENI gives fine L4 control | That SG is also the #1 silent failure (blocks 443) |
| Private DNS makes existing code “just work” | Private-DNS conflicts (two endpoints, S3 gateway vs interface) |
| Reachable from on-prem (interface) for hybrid | Gateway endpoints are not reachable from on-prem/peered/TGW |
When each matters: for S3/DynamoDB the gateway endpoint is a no-brainer — free, more secure, less NAT. For other AWS services the interface endpoint’s cost is usually dwarfed by the NAT data charges it removes and the security it adds, provided you consolidate (centralize in a shared-services VPC at scale). For your own cross-account services, PrivateLink’s overlapping-CIDR tolerance and one-way isolation are often the only clean option. The disadvantages are almost entirely operational-knowledge items — the SG gate, the DNS conflicts, the DynamoDB asymmetry — which is exactly what the troubleshooting section drills.
Hands-on lab
You will build all three patterns in one VPC: (A) an S3 gateway endpoint, (B) the three interface endpoints that let a private instance use SSM with no NAT, and © a tiny PrivateLink service (NLB → endpoint service → consumer endpoint). Both aws CLI and Terraform paths are shown; a full teardown follows.
⚠️ Cost warning. Gateway endpoints and IAM are free. Interface endpoints and the NLB cost money while they exist (~$0.01/AZ-hour each + the NLB’s hourly + data). This lab’s interface endpoints and NLB run for well under an hour of testing — pennies — but you must do the teardown in Step 8 or they bill continuously. Use a sandbox account in
ap-south-1(adjust the region as needed).
Step 0 — Set variables and find your VPC/subnets.
REGION=ap-south-1
VPC=vpc-0abc123 # your VPC id
SUB_A=subnet-0priv-a # a private subnet in AZ a
SUB_B=subnet-0priv-b # a private subnet in AZ b
RTB=rtb-0priv # the private subnets' route table
CIDR=$(aws ec2 describe-vpcs --vpc-ids $VPC --query "Vpcs[0].CidrBlock" --output text)
echo "VPC $VPC ($CIDR) in $REGION"
# Ensure DNS flags are on (required for private DNS):
aws ec2 modify-vpc-attribute --vpc-id $VPC --enable-dns-support
aws ec2 modify-vpc-attribute --vpc-id $VPC --enable-dns-hostnames
Step 1 — (A) Create the S3 gateway endpoint and wire it to the route table.
aws ec2 create-vpc-endpoint \
--vpc-id $VPC \
--service-name com.amazonaws.$REGION.s3 \
--vpc-endpoint-type Gateway \
--route-table-ids $RTB \
--query "VpcEndpoint.VpcEndpointId" --output text
Expected: a vpce-… id. Verify the route: aws ec2 describe-route-tables --route-table-ids $RTB --query "RouteTables[].Routes[?DestinationPrefixListId!=null].[DestinationPrefixListId,GatewayId]" should show pl-… vpce-….
Step 2 — Create the security group the interface endpoints will share. This is the gate — it must allow 443 from the VPC CIDR.
SG=$(aws ec2 create-security-group --group-name vpce-ingress \
--description "Allow 443 to interface endpoints" --vpc-id $VPC \
--query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id $SG \
--protocol tcp --port 443 --cidr $CIDR
echo "Endpoint SG: $SG"
Step 3 — (B) Create the three SSM interface endpoints with private DNS.
| Service name | Why SSM needs it |
|---|---|
com.amazonaws.$REGION.ssm |
Core SSM API (registration, commands) |
com.amazonaws.$REGION.ssmmessages |
Session Manager data channel (interactive shell) |
com.amazonaws.$REGION.ec2messages |
Agent ↔ SSM message transport |
for SVC in ssm ssmmessages ec2messages; do
aws ec2 create-vpc-endpoint --vpc-id $VPC \
--service-name com.amazonaws.$REGION.$SVC \
--vpc-endpoint-type Interface \
--subnet-ids $SUB_A $SUB_B \
--security-group-ids $SG \
--private-dns-enabled \
--query "VpcEndpoint.VpcEndpointId" --output text
done
Expected: three vpce-… ids, each moving from pending to available within a minute or two (aws ec2 describe-vpc-endpoints --query "VpcEndpoints[].[ServiceName,State]" --output table).
Step 4 — Verify SSM works with no NAT. Launch a private instance (no public IP, in $SUB_A) with an instance profile carrying AmazonSSMManagedInstanceCore, then:
aws ssm describe-instance-information \
--query "InstanceInformationList[].[InstanceId,PingStatus]" --output table
aws ssm start-session --target i-0yourinstance # opens a shell — no NAT, no bastion
Expected: your instance shows Online, and Session Manager opens a shell. If it does not, jump to troubleshooting row 3 (the SG) — the #1 cause.
Step 5 — © Provider side: NLB + endpoint service. Create an internal NLB with a listener on 443 and a target group with at least one healthy target (a tiny instance or IP running a TLS listener), then publish the service:
NLB_ARN=arn:aws:elasticloadbalancing:$REGION:111111111111:loadbalancer/net/kv-demo/abc
SVC_ID=$(aws ec2 create-vpc-endpoint-service-configuration \
--network-load-balancer-arns $NLB_ARN --acceptance-required \
--query "ServiceConfiguration.ServiceId" --output text)
# Allow this same account (single-account lab) to consume it:
ACCT=$(aws sts get-caller-identity --query Account --output text)
aws ec2 modify-vpc-endpoint-service-permissions --service-id $SVC_ID \
--add-allowed-principals arn:aws:iam::$ACCT:root
SVC_NAME=$(aws ec2 describe-vpc-endpoint-service-configurations \
--service-ids $SVC_ID --query "ServiceConfigurations[0].ServiceName" --output text)
echo "Service name: $SVC_NAME"
Step 6 — Consumer side: interface endpoint to your own service, then accept.
EP=$(aws ec2 create-vpc-endpoint --vpc-id $VPC \
--service-name $SVC_NAME --vpc-endpoint-type Interface \
--subnet-ids $SUB_A $SUB_B --security-group-ids $SG \
--query "VpcEndpoint.VpcEndpointId" --output text)
# It starts pendingAcceptance; the provider accepts:
aws ec2 accept-vpc-endpoint-connections --service-id $SVC_ID --vpc-endpoint-ids $EP
aws ec2 describe-vpc-endpoints --vpc-endpoint-ids $EP \
--query "VpcEndpoints[0].State" --output text # -> available
Expected: the endpoint transitions to available; the consumer can now hit the service on the endpoint’s private DNS name (aws ec2 describe-vpc-endpoints --vpc-endpoint-ids $EP --query "VpcEndpoints[0].DnsEntries[0].DnsName").
Step 7 — (Optional) Terraform equivalent. The snippets in the gateway, interface, and endpoint-service sections above compose into one main.tf: an aws_vpc_endpoint (Gateway) for S3, a for_each set of interface endpoints for SSM, and an aws_vpc_endpoint_service + consumer aws_vpc_endpoint. terraform init && terraform apply, then terraform destroy for teardown.
Step 8 — Teardown (do this — interface endpoints and the NLB bill hourly).
# Delete endpoints (interface + gateway + consumer)
aws ec2 describe-vpc-endpoints --filters Name=vpc-id,Values=$VPC \
--query "VpcEndpoints[].VpcEndpointId" --output text | tr '\t' '\n' | \
xargs -I{} aws ec2 delete-vpc-endpoints --vpc-endpoint-ids {}
# Delete the endpoint service, then the NLB + target group (console or CLI)
aws ec2 delete-vpc-endpoint-service-configurations --service-ids $SVC_ID
# Delete the SG (after its ENIs are gone) and terminate the test instance
aws ec2 delete-security-group --group-id $SG
⚠️ Confirm no vpce-… interface endpoints and no test NLB remain — those are the only line items that keep billing. The S3/DynamoDB gateway endpoints are free but delete them too for a clean slate.
Common mistakes & troubleshooting
This is the section you will return to. Endpoint failures are almost always one of the rows below — match the symptom, run the confirm step, apply the fix. The golden split: gateway problems are route/prefix-list/policy; interface problems are SG/DNS/acceptance.
| # | Symptom | Root cause | Confirm (exact command) | Fix |
|---|---|---|---|---|
| 1 | S3 calls still egress via NAT/internet, gateway EP exists | Route table not associated with the endpoint | aws ec2 describe-route-tables --route-table-ids <rtb> --query "RouteTables[].Routes[?DestinationPrefixListId!=null]" |
modify-vpc-endpoint --add-route-table-ids <rtb> |
| 2 | S3 via gateway returns AccessDenied though IAM allows it |
Endpoint policy too tight (wrong bucket/action) | aws ec2 describe-vpc-endpoints --query "…PolicyDocument" |
Widen the endpoint policy Resource/Action to the bucket |
| 3 | Interface endpoint available but every call hangs/times out |
The ENI’s security group doesn’t allow 443 | VPC Flow Logs on the ENI show REJECT; describe-security-groups on the endpoint SG |
Allow inbound TCP 443 from the VPC CIDR/SG on the endpoint SG |
| 4 | SDK still resolves the public IP for an AWS service | Private DNS not enabled, or VPC DNS flags off | dig ssm.<region>.amazonaws.com from the instance; describe-vpc-endpoints --query "…PrivateDnsEnabled" |
modify-vpc-endpoint --private-dns-enabled; set enableDnsSupport+enableDnsHostnames |
| 5 | Enabling private DNS errors with a conflict | Another endpoint for the same service already owns private DNS | describe-vpc-endpoints — two endpoints, same ServiceName, both private-DNS |
Disable private DNS on one; keep a single private-DNS endpoint per service |
| 6 | After adding an S3 interface endpoint, S3 costs jump / gateway bypassed | S3 interface endpoint private DNS hijacked the free gateway path | dig s3.<region>.amazonaws.com now returns the ENI IP |
Disable private DNS on the S3 interface EP; use its endpoint-specific/regional name for on-prem only |
| 7 | Consumer endpoint stuck in pendingAcceptance |
Provider hasn’t accepted, or principal not allow-listed | aws ec2 describe-vpc-endpoint-connections --filters Name=service-id,Values=<svc> |
Add the ARN to AllowedPrincipals and accept-vpc-endpoint-connections |
| 8 | Consumer can’t even create the endpoint (InvalidServiceName/not found) |
Consumer principal not on the provider’s allow-list | Provider: describe-vpc-endpoint-service-permissions --service-id <svc> |
modify-vpc-endpoint-service-permissions --add-allowed-principals <arn> |
| 9 | DynamoDB: “there is no interface endpoint” | DynamoDB supports gateway only | create-vpc-endpoint … dynamodb --vpc-endpoint-type Interface fails |
Use a gateway endpoint for DynamoDB |
| 10 | S3/DynamoDB gateway endpoint unreachable from on-prem | Gateway endpoints are not reachable over DX/VPN/peering/TGW | Traffic from on-prem to S3 uses public path | Use an interface endpoint for S3 + inbound Resolver; DynamoDB needs a proxy |
| 11 | Endpoint to a bucket/table in another Region fails | Endpoints are regional; gateway is same-Region only | Compare bucket Region to VPC Region | Keep data same-Region, or add an endpoint in the other Region |
| 12 | Works in one AZ, dead in another (endpoint service) | Provider NLB not in that AZ, or cross-zone off | Compare AZ IDs (use1-az1), not names; check NLB subnets |
Add NLB subnets in every consumed AZ + enable cross-zone LB |
| 13 | Long-lived gRPC/DB connection resets after ~6 min | NLB 350 s TCP idle timeout | Correlate resets with idle periods | Set TCP keepalives < 350 s on the client |
| 14 | Custom private DNS name not effective for consumers | Domain-ownership TXT not verified | describe-vpc-endpoint-service-configurations --query "…PrivateDnsNameConfiguration.State" != verified |
Publish the TXT token in public DNS; start verification |
| 15 | Reached quota creating endpoints | Interface-per-VPC or gateway-per-Region soft limit | Service Quotas console for “VPC endpoints” | Request an increase; or centralize endpoints in a shared VPC |
Error / status reference
| Error / state | Where | Meaning | Fix |
|---|---|---|---|
pendingAcceptance |
Endpoint connection | Provider hasn’t accepted the consumer | Accept it / auto-accept via SNS→Lambda |
pending → stuck |
Interface endpoint | ENIs provisioning or subnet/AZ issue | Check the subnet exists in a service-supported AZ |
InvalidServiceName |
create-vpc-endpoint |
Wrong service name or not allow-listed | Verify com.amazonaws.<region>.<svc> / the vpce-svc-… name |
AccessDenied (via gateway) |
S3/DynamoDB call | Endpoint policy or bucket policy denies | Fix endpoint policy; check aws:sourceVpce deny |
RequestTimeTooSkewed after private DNS |
STS/KMS | Actually a red herring — usually the SG/DNS | Verify SG 443 + private DNS resolution first |
PrivateDnsName…not verified |
Endpoint service | TXT ownership check incomplete | Add TXT record; re-verify |
Connection REJECT in Flow Logs |
Interface ENI | SG blocks the client | Allow 443 from client CIDR on the endpoint SG |
The three nastiest failures, in prose
The security group that isn’t there. You create an SSM interface endpoint, it reports available, and every start-session hangs. Everyone stares at IAM. The problem is that an interface endpoint has a security group on its ENI, and if you did not specify one it inherited the VPC default SG — which typically does not allow inbound 443 from your app subnets. The endpoint is “up” but the door is bolted. Confirm with VPC Flow Logs on the endpoint’s ENI (you will see REJECT on 443) or by reading the endpoint SG; fix by allowing TCP 443 inbound from the VPC CIDR. This one failure accounts for the majority of “my interface endpoint doesn’t work” tickets.
The S3 gateway-vs-interface DNS collision. You have a happy, free S3 gateway endpoint. Someone adds an S3 interface endpoint (for on-prem access) and enables private DNS on it. Now s3.<region>.amazonaws.com resolves, VPC-wide, to the interface ENI — so all your in-VPC S3 traffic silently moves off the free gateway path onto the billed interface endpoint, and your S3 data-processing charges appear from nowhere. Confirm with dig s3.<region>.amazonaws.com from an instance (it returns the ENI’s private IP, not a public one). Fix by disabling private DNS on the S3 interface endpoint and using its endpoint-specific/regional DNS name only for the on-prem clients that need it — keep the gateway endpoint as the default in-VPC path. This is the single most expensive endpoint mistake, and why S3 is the only service where you must think hard about running both types.
The connection stuck in pendingAcceptance. A consumer creates an interface endpoint to your published service and it never becomes available. Two independent gates both have to open: the consumer’s principal must be on the provider’s AllowedPrincipals list, and the provider must accept the specific connection (unless AcceptanceRequired=false). Confirm on the provider side with describe-vpc-endpoint-connections (you will see pendingAcceptance) and describe-vpc-endpoint-service-permissions (to check the allow-list). Fix by adding the consumer ARN to the allow-list and accepting the connection — or wire an SNS topic on connection events to a Lambda that auto-accepts trusted principals so onboarding is hands-off.
Best practices
- Always use a free gateway endpoint for S3 and DynamoDB in any VPC that touches them — it is cheaper and more secure than NAT, with no downside.
- Tighten every endpoint policy. The default is wide-open; restrict
Resourceto your buckets/tables to turn the endpoint into an exfiltration control. - Pair endpoint policies with
aws:sourceVpcedeny statements on bucket/key policies so a resource is provably reachable only through your endpoint. - Give interface endpoints a dedicated security group that allows 443 from the VPC CIDR — never rely on the default SG (the #1 silent failure).
- Put an interface-endpoint ENI in every AZ your workload runs in; a single-AZ endpoint is a single point of failure.
- Enable private DNS for AWS-service interface endpoints so application code is unchanged — but keep one private-DNS endpoint per service to avoid conflicts.
- Never enable private DNS on an S3 interface endpoint if a free S3 gateway endpoint exists in the same VPC, or you will silently reroute (and pay for) all in-VPC S3 traffic.
- Consolidate interface endpoints in a shared-services VPC behind Transit Gateway at scale, sharing the private hosted zone — one set instead of N×per-VPC.
- Front your own PrivateLink service with an internal NLB, enable cross-zone load balancing, and mind the fixed 350 s idle timeout for long-lived connections.
- Keep
AcceptanceRequired=truefor endpoint services and auto-accept trusted principals via SNS→Lambda; scope AllowedPrincipals to exact account/role ARNs, not*. - Verify custom private DNS names via the TXT record so consumers can use a friendly hostname.
- Remember the asymmetries: DynamoDB is gateway-only; gateway endpoints are unreachable from on-prem/peered/TGW; endpoints are regional.
Security notes
VPC endpoints are a security control, so configure them like one. Least privilege on the endpoint policy: the default full-access policy should almost always be narrowed — restrict Resource to your own bucket/table ARNs and Principal/Condition to your Organization (aws:PrincipalOrgID), so the endpoint cannot be used to reach an attacker’s S3. Resource-side enforcement: the strongest pattern is a two-way lock — an endpoint policy limiting what the endpoint reaches, plus a bucket/key policy with a Deny when aws:sourceVpce (or aws:sourceVpc) does not match, which makes the resource reachable only through the endpoint and gives auditors provable network isolation. Network isolation: the security group on an interface endpoint’s ENI is your L4 control — allow only 443 from the specific subnets/SGs that need the service, not 0.0.0.0/0; and because the endpoint keeps traffic on the AWS backbone, you can operate genuinely private subnets with no Internet Gateway at all. Identity for endpoint services: scope AllowedPrincipals to exact ARNs (a security account, a specific role — see IAM Cross-Account Roles & AssumeRole), keep acceptance required, and treat the allow-list plus acceptance as the two locks that gate who may even connect. Encryption in transit still matters — PrivateLink keeps traffic off the internet but you should terminate TLS on your NLB/targets for your own services. Auditing: endpoint creation, policy changes, and connection accept/reject are all CloudTrail events; centralize them (see CloudTrail, Config & Audit for Compliance) and alert on new endpoint services or allow-list changes, which are high-signal for data-movement risk.
Cost & sizing
The whole topic has a split personality on cost: gateway endpoints are free and save money; interface endpoints and endpoint services cost money that you weigh against the NAT and security they replace.
| Item | Cost (approx, region-dependent) | Notes |
|---|---|---|
| Gateway endpoint (S3/DynamoDB) | Free | No hourly, no data charge; removes NAT data cost for that traffic |
| Interface endpoint | ~$0.01 per AZ-hour per endpoint + ~$0.01/GB | Multiply by number of AZs and endpoints; ~$7/AZ-month each |
| Endpoint service (provider) | NLB hourly + LCU/NLCU + data | You run the NLB; the endpoint-service object itself is free |
| NAT gateway (what you’re avoiding) | ~$0.045–0.056/hr + ~$0.045–0.056/GB | The per-GB is the big one for chatty S3 |
| Cross-AZ data (endpoint traffic) | Standard inter-AZ rates may apply | Keep endpoints in the caller’s AZ where practical |
| Data processing (interface EP) | ~$0.01/GB | Cheaper than NAT’s per-GB |
Rough INR figures (Mumbai ap-south-1, approximate): one interface endpoint across 3 AZs is roughly ₹1,800/month in AZ-hours before data; seven such services (a typical SSM+ECR+KMS+logs set) run ~₹8,000–12,000/month if run per-VPC — which is precisely why centralizing them behind Transit Gateway is the standard cost optimization. Against that, a free S3/DynamoDB gateway endpoint can erase a NAT data-processing bill of tens of thousands of rupees a month for data-heavy workloads. Sizing guidance: create interface endpoints only for the services your private workloads actually call (check VPC Flow Logs / CloudTrail source IPs), use one shared SG, put an ENI in each AZ you use (no more), and consolidate across VPCs at scale. For endpoint services, right-size the NLB and turn on cross-zone LB so you are not paying for idle AZs.
Interview & exam questions
1. What are the two types of VPC endpoint and how do they differ structurally? A gateway endpoint is a route-table entry pointing a service’s managed prefix list at the endpoint; it supports S3 and DynamoDB only, is free, and has no ENI or security group. An interface endpoint is an ENI with a private IP (powered by PrivateLink) in each chosen AZ; it supports most AWS services plus partner and your own services, is billed per AZ-hour and per GB, and is guarded by a security group. (SAA-C03, ANS-C01)
2. Which services support gateway endpoints, and what does an interface endpoint cost? Gateway endpoints support only S3 and DynamoDB and are free. Interface endpoints cost roughly $0.01 per AZ-hour per endpoint plus ~$0.01/GB processed, region-dependent. (SAA-C03)
3. A private instance can’t be managed by SSM and has no NAT. What do you add? Three interface endpoints — ssm, ssmmessages, and ec2messages — with private DNS enabled and a security group allowing 443 from the VPC, plus an instance profile with AmazonSSMManagedInstanceCore. (SOA-C02, SAA-C03)
4. Your S3 gateway endpoint exists but traffic still goes via NAT. Why? The endpoint is not associated with the subnet’s route table, so no pl-… → vpce-… route exists. Add the route table with modify-vpc-endpoint --add-route-table-ids. (SAA-C03)
5. What is AWS PrivateLink and what problem does it uniquely solve? PrivateLink connects a consumer to a provider service over the AWS backbone via an interface-endpoint ENI and a provider NLB, with no VPC peering and overlapping CIDRs allowed (traffic is one-way and NATed through the ENI). It uniquely enables private, cross-account service access when CIDRs overlap or when you must not peer. (ANS-C01, SAA-C03)
6. How do you expose your own service via PrivateLink? Front it with an internal NLB, create a VPC endpoint service pointing at the NLB, add consumer ARNs to AllowedPrincipals, and (by default) accept each consumer connection; the consumer creates an interface endpoint to your vpce-svc-… name. (ANS-C01)
7. What is pendingAcceptance and how do you resolve it? A consumer’s endpoint connection to your endpoint service is waiting because AcceptanceRequired=true. Add the consumer to AllowedPrincipals and run accept-vpc-endpoint-connections (or auto-accept via SNS→Lambda). (ANS-C01)
8. Why might an interface endpoint be available yet every request time out? The security group on the endpoint ENI does not allow inbound 443 from the client. Confirm with VPC Flow Logs (REJECT) and fix the SG. (SAA-C03, ANS-C01)
9. You add an S3 interface endpoint and S3 costs rise. What happened? Enabling private DNS on the S3 interface endpoint made s3.<region>.amazonaws.com resolve to the ENI VPC-wide, rerouting in-VPC S3 traffic off the free gateway endpoint onto the billed interface endpoint. Disable private DNS on the interface EP. (ANS-C01)
10. Can a gateway endpoint be reached from on-premises over Direct Connect? No. A gateway endpoint is a route in the VPC route table and is not reachable from on-prem, peered VPCs, TGW, or VPN. For private S3 from on-prem, use an interface endpoint. (ANS-C01)
11. How do you make a bucket reachable only through a VPC endpoint? Add a bucket policy Deny when aws:sourceVpce does not equal your endpoint id (optionally aws:sourceVpc), paired with a scoped endpoint policy. This gives provable network isolation. (SCS-C02)
12. Does DynamoDB have an interface endpoint? No — DynamoDB supports gateway endpoints only. If you need DynamoDB-style access from on-prem you must front it yourself (e.g. a proxy/service), because the gateway type can’t serve on-prem. (SAA-C03, ANS-C01)
Quick check
- Which endpoint type is free, and which two services does it support?
- Your interface endpoint says
availablebut calls hang. What is the single most likely cause? - You need private S3 access from on-premises. Gateway or interface endpoint — and why?
- Two VPCs in different accounts have the same CIDR and one must call the other’s service privately. What do you use, and why not peering?
- A consumer’s endpoint to your published service is stuck in
pendingAcceptance. Name the two gates you must open.
Answers
- The gateway endpoint is free; it supports S3 and DynamoDB only.
- The security group on the endpoint ENI doesn’t allow inbound 443 from the client — the #1 interface-endpoint failure.
- An interface endpoint — a gateway endpoint is a VPC route-table construct and is not reachable from on-premises; the interface ENI’s private IP is.
- PrivateLink (an endpoint service consumed by an interface endpoint). Peering fails because it requires non-overlapping CIDRs; PrivateLink is one-way and NATs through the ENI, so overlap is irrelevant.
- The consumer principal must be on the provider’s AllowedPrincipals list, and the provider must accept the specific connection (unless acceptance is disabled).
Glossary
| Term | Definition |
|---|---|
| VPC endpoint | A private on-ramp letting VPC resources reach a service without an IGW/NAT/public IP; traffic stays on the AWS network. |
| Gateway endpoint | A VPC endpoint implemented as a route-table entry to a managed prefix list; supports S3 and DynamoDB only; free. |
| Interface endpoint | A VPC endpoint implemented as an ENI with a private IP (PrivateLink); supports most services; billed per AZ-hour + per GB. |
| AWS PrivateLink | The technology carrying interface-endpoint and endpoint-service traffic privately between a consumer ENI and a provider NLB. |
| Endpoint service | Your own service published behind an NLB so others reach it via an interface endpoint; identified by a vpce-svc-… name. |
| Managed prefix list | An AWS-maintained set of a service’s CIDR ranges (pl-…) referenced by routes/SGs so you never hardcode IPs. |
| Private DNS | A setting that makes a service’s default public DNS name resolve to the interface endpoint’s ENI inside the VPC. |
| Endpoint policy | A resource-based policy on an endpoint that restricts (never grants) which principals/resources/actions may traverse it. |
| AllowedPrincipals | The list of account/role/user ARNs permitted to discover and connect to an endpoint service. |
| Acceptance | The provider’s per-connection approval of a consumer endpoint (AcceptanceRequired); until accepted the state is pendingAcceptance. |
| ENI (Elastic Network Interface) | The virtual network card an interface endpoint places in each AZ subnet, carrying a private IP and a security group. |
| aws:sourceVpce | An IAM condition key matching the VPC endpoint id, used on resource policies to require access via a specific endpoint. |
| NLB (Network Load Balancer) | The L4 load balancer that fronts targets for a PrivateLink endpoint service; fixed 350 s idle timeout. |
| Service name | The identifier an endpoint points at — com.amazonaws.<region>.<svc> for AWS services, vpce-svc-… for your own. |
Next steps
- Master the network it lives in: AWS VPC, Subnets & Security Groups Explained — the subnets, route tables, and SGs every endpoint depends on.
- See what endpoints replace: NAT Gateway & Private-Subnet Egress Hands-On — the cost and egress path a gateway endpoint erases for S3/DynamoDB.
- Know when peering is the wrong tool: VPC Peering: Setup, Routing & Limits — the overlapping-CIDR limit that PrivateLink sidesteps.
- Scale endpoints across many VPCs: Transit Gateway Hub-and-Spoke Hands-On — where centralized interface endpoints live behind a hub.
- Gate who may connect: IAM Cross-Account Roles & AssumeRole — the ARNs you put in AllowedPrincipals and endpoint policies.