VPC Origin: exposing a private service on AWS

Goal: expose a service on the internet without anything listening on a public subnet.

The classic approach, a public ALB with a security group restricted to the CloudFront prefix list, is still bypassable: anyone who discovers the ALB’s DNS name can reach it directly, skipping CloudFront entirely.

The solution is CloudFront VPC Origin. CloudFront reaches the ALB directly inside the VPC, no public load balancer needed and no NAT for inbound traffic.

Full code is on GitHub .

Architecture Link to heading

All resources live in private subnets:

  • Internal ALB, no public IP and no route to the internet gateway.
  • ECS Fargate running an nginx task, also with no public IP.
  • EC2 (bastion), no public IP, to show the pattern isn’t tied to ECS.
  • CloudFront, the only entry point, reaching the ALB via VPC Origin.

Public subnets exist only to host the NAT Gateways, used for outbound traffic. They expose nothing.

infra

Two layers of protection Link to heading

Even with VPC Origin, the internal ALB is still reachable by anyone with access to the VPC, for example via peering or VPN. To cover that case I added two independent controls.

The first is at the network layer: a security group on the ALB accepts traffic only from the AWS-managed CloudFront prefix list.

The second is at the application layer: a randomly generated HTTP header, injected by CloudFront on every request and checked by a rule on the ALB. Requests arriving without the correct header are dropped.

This is the pattern AWS recommends: the security group blocks anyone not coming from CloudFront’s IP ranges, the header blocks anyone trying to impersonate CloudFront from within those ranges.

ECS Fargate Link to heading

The nginx task runs on Fargate in a private subnet, behind the internal ALB’s target group.

EC2 Link to heading

I also added an EC2 instance (bastion) to show the pattern works with any resource in a private subnet, not just ECS.

The instance has no public IP and no SSH port open. It’s reached exclusively through SSM Session Manager, via a dedicated IAM role. The result is zero open inbound ports, zero SSH keys to manage, and access that’s still traceable through CloudTrail.

Supporting traffic Link to heading

Both ECS and EC2 need to talk to AWS services like ECR, CloudWatch Logs and SSM; instead of routing that through NAT I used VPC Endpoints. This way traffic to AWS services never leaves the VPC. Private subnets still have a default route to a NAT Gateway, for outbound traffic not covered by the endpoints. So this isn’t a zero-NAT setup.

The point, though, isn’t eliminating all outbound traffic, it’s eliminating every inbound path that doesn’t go through CloudFront. These are two different problems, and it’s the second one that VPC Origin solves.

Conclusions Link to heading

VPC Origin is useful when you need to expose an internal service through a single, well-controlled entry point.

Main benefits:

  • No application resource with a public IP or exposed port.
  • Double control, network plus application, against bypassing CloudFront.
  • Works with both containers (ECS/Fargate) and EC2 instances.
  • Administrative access via SSM, no SSH and no keys to manage.

Limitations:

  • The NAT Gateway is still needed for outbound traffic not covered by VPC Endpoints.
  • Extra cost for VPC Origin and Interface Endpoints compared to direct public exposure.