AWS Networking

AWS PrivateLink & VPC Endpoints: Interface vs Gateway, Private Service Access Hands-On

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:

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 accessPrincipal:*, 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.

Left-to-right AWS VPC-endpoint architecture: a private EC2 with no NAT in a consumer VPC reaches AWS two ways. An interface endpoint (an ENI with a private IP, PrivateLink, private DNS, guarded by a security group allowing 443) fronts both AWS services like SSM/ssmmessages/ec2messages and a provider-published VPC endpoint service backed by an internal NLB with allow-listed principals and connection acceptance. A gateway endpoint, a free route-table entry to a managed prefix list, carries S3 and DynamoDB traffic. Six numbered badges mark the ENI cost, the endpoint security group, the gateway route, endpoint-service acceptance, private DNS, and the region-local gateway limit.

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

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 endpointsssm, 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

  1. Which endpoint type is free, and which two services does it support?
  2. Your interface endpoint says available but calls hang. What is the single most likely cause?
  3. You need private S3 access from on-premises. Gateway or interface endpoint — and why?
  4. 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?
  5. A consumer’s endpoint to your published service is stuck in pendingAcceptance. Name the two gates you must open.

Answers

  1. The gateway endpoint is free; it supports S3 and DynamoDB only.
  2. The security group on the endpoint ENI doesn’t allow inbound 443 from the client — the #1 interface-endpoint failure.
  3. 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.
  4. 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.
  5. 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

AWSPrivateLinkVPC EndpointsInterface EndpointGateway EndpointNLBPrivate DNSTerraform
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading