EC2 Private Subnet Has No Internet? The NAT Fix

Logeshwaran.C

If an EC2 instance in a private subnet can't reach the internet — apt or yum just hangs, curl times out, nothing resolves — the fix is almost always the same missing piece: the subnet's route table has no route to a NAT gateway sitting in a public subnet. That's the direct answer. The counterintuitive part is this: if you can already SSH into that instance from a bastion or through Session Manager, your security groups are not the problem. Security groups allow all outbound traffic by default, from the moment they're created. The block lives one layer over, in a route table almost nobody thinks to open first.

⚡ Quick Answer

Add a default route in the private subnet's route table: destination 0.0.0.0/0, target the NAT gateway ID (nat-xxxxxxxx), not the internet gateway.

Then check, in order: the NAT gateway is in a public subnet and shows Available; the private and public subnets use two separate route tables; security groups allow outbound 80/443; network ACLs on both subnets allow the round trip; DNS resolution is turned on for the VPC.

Skip straight to the five-minute diagnostic if you want to find out which of those five it is before changing anything.

Jake found this out the expensive way. He runs a small phone repair and resale shop, and the Saturday before a big trade-in promotion, he moved his inventory app's database server into a private subnet on the advice of a contractor "for security." The app server could reach the database fine — same VPC, private IP addresses, no problem. But the database server itself couldn't pull a security patch. Yum hung for twenty minutes, then timed out. Jake spent his slowest business day of the month, a Saturday, staring at a terminal instead of running his promotion, convinced he'd broken something in the security group he'd just tightened.

He hadn't. His mentor Ethan looked at it for four minutes and found the real gap: the private subnet's route table only had the local route. No path out at all, to anything. "You didn't break the security group," Ethan told him. "You just never gave this subnet a road out of the neighborhood. The guard at the door was never the issue — there was no door."

Why This Happens: Security Groups Already Say Yes

A subnet in a VPC is "private" for exactly one reason: its route table has no route pointing to an internet gateway. That's the whole definition. It has nothing to do with security groups, nothing to do with encryption, and nothing to do with how locked-down the instance is. A public subnet has a route table entry sending 0.0.0.0/0 traffic to an internet gateway (an igw- ID); a private subnet doesn't have that entry at all. If the instance also needs outbound internet access — for package updates, an external API, a license check — something in a public subnet has to forward that traffic on its behalf. That something is a NAT gateway.

Here's the part that trips people up: when you create a security group, AWS gives it one default outbound rule, and that rule allows all outbound traffic to 0.0.0.0/0. Nobody adds that rule; it's there from the first click. So unless someone has gone in and deliberately removed it — which is rare, because most teams tighten inbound rules and leave outbound alone — your security group was never blocking the instance from trying to leave. It's blocking nothing. The instance's traffic gets as far as the edge of the subnet, looks for a route to the internet, and finds none. That's a route table problem wearing a security group costume.

The Five-Minute Diagnostic

Before changing anything, match your symptom against the table below. It'll tell you which of the five fixes to open first, instead of working through all of them in order.

Symptom Likely cause Go to
Every outbound request just hangs, no error at all No route to a NAT gateway in the private route table Fix 1
Route looks correct, still nothing NAT gateway isn't in a public subnet, isn't Available, or the VPC has no internet gateway attached Fix 2
SSH/RDP into the instance works fine, only outbound fails Confirms security groups are fine — look at the route table and NAT gateway first, not the SG Fix 1 & 2
Someone recently tightened outbound rules and it broke Custom outbound rule replaced the default allow-all and doesn't cover 80/443 Fix 3
Connection resets or drops after a few seconds instead of hanging A custom network ACL is missing the return path on ephemeral ports Fix 4
curl: Could not resolve host even though the route looks right DNS resolution disabled on the VPC Fix 5

🙋‍♂️ Jake's Reality Check

"I already checked the security group three times. It says all outbound traffic is allowed. So why is it still not working?"

Because the security group was never the gate you needed to open. If it already allows all outbound traffic, it's doing its job. The instance's packets are leaving the network interface fine — they just have nowhere to go once they hit the edge of the subnet. That's a routing question, not a security group question.

Fix 1: Give the Private Route Table a Path to NAT

A VPC needs two different route tables to do this correctly: one for the public subnet, and a separate one for the private subnet. The public subnet's table routes internet-bound traffic to the internet gateway. The private subnet's table routes internet-bound traffic to the NAT gateway instead — never to the internet gateway directly, because the private subnet's instances don't have public IP addresses for the internet gateway to answer.

Here's what a working pair of route tables actually looks like, using the same layout AWS's own reference architecture uses:

Route table Destination Target
Public subnet10.0.0.0/16local
Public subnet0.0.0.0/0igw-id
Private subnet10.0.0.0/16local
Private subnet0.0.0.0/0nat-gateway-id
  1. In the VPC console, confirm the NAT gateway exists and note its ID (it starts with nat-).
  2. Open Route Tables and find (or create) a route table associated with the private subnet only — not the one the public subnet uses.
  3. Add a route: destination 0.0.0.0/0, target the NAT gateway ID from step 1.
  4. Under Subnet Associations, explicitly associate this route table with the private subnet.
  5. Confirm the public subnet's route table (a different one) still points its 0.0.0.0/0 route at the internet gateway, not the NAT gateway.

⚠️ What this actually breaks

Don't reuse the public subnet's route table for the private subnet "to save a step." If they share a table, either both get the internet gateway route (and your private subnet stops being private, because its instances would need public IPs to actually use that route) or neither gets a usable route at all. Give each tier its own table.

Fix 2: Confirm the NAT Gateway Itself Is Healthy

A correct route pointing at a broken NAT gateway looks identical to a missing route from the instance's point of view: nothing gets through. Check the NAT gateway's own state before spending more time on route tables.

In the NAT Gateways page of the VPC console, the status should read Available. If it reads Failed, open the details pane for the state message — common causes are a subnet with no free private IP addresses left for the gateway's own network interface, or the VPC not having an internet gateway attached at all (the error text literally says "Network vpc-xxxxxxxx has no Internet gateway attached"). A failed NAT gateway deletes itself automatically after roughly an hour, so if you don't see it anymore, that's likely what happened — check subnet capacity and the internet gateway attachment before recreating it.

Also confirm the NAT gateway is sitting in a public subnet — one whose own route table points to the internet gateway. A NAT gateway placed in a subnet that itself has no route to the internet gateway can't forward anything either; it needs that outbound path just as much as the instances behind it do.

Fix 3: Security Group Outbound Rules

This is genuinely rare as the actual cause, for the reason covered above, but it happens when a team follows least-privilege advice and strips the default allow-all outbound rule without replacing it with anything specific. If your security group has no outbound rules at all, no outbound traffic is allowed — full stop. Add explicit outbound rules for TCP 443 (HTTPS) and TCP 80 (HTTP) to 0.0.0.0/0, or to the specific destinations the instance needs to reach.

Security groups are stateful, which matters here: you only need to allow the outbound request. The response traffic is automatically permitted back in, regardless of what your inbound rules say, so you don't need a matching inbound rule for the reply.

Fix 4: Network ACLs, the One Everyone Forgets

The default network ACL that comes with a new VPC allows all inbound and all outbound traffic, so most people never think about NACLs again after setup. But network ACLs are stateless — unlike security groups, they do not remember that a connection was already permitted going out. If someone has replaced the default ACL with a custom one, a custom network ACL denies everything by default until you add rules explicitly allowing it, and every allowed request needs a matching allow rule for its return traffic on the high (ephemeral) port range, typically 1024–65535, because that's the range the reply lands on.

This has to be checked on both ends of the path: the network ACL attached to the private subnet, and the network ACL attached to the public subnet where the NAT gateway lives. AWS's own NAT gateway troubleshooting guidance calls this out directly — the NAT gateway's subnet ACL needs to allow both inbound and outbound internet traffic, or the gateway can't do its job even with a perfect route table pointed at it.

Rules are evaluated in ascending order by rule number, and the first match wins — so a low-numbered deny rule will silently override a higher-numbered allow rule for the same traffic, even if that wasn't the intent. If you're debugging this by hand, walk the rule list from the lowest number up.

Fix 5: DNS Resolution

If routing, security groups, and NACLs all check out but you're seeing Could not resolve host instead of a timeout, the problem has moved to DNS. The VPC's enableDnsSupport attribute needs to be turned on for instances to resolve public hostnames at all. This is a VPC-level setting, not a per-subnet or per-instance one, so it's easy to overlook if the VPC was built with a custom template that turned it off for another reason. Check it under the VPC's own settings, alongside enableDnsHostnames.

The Cheaper Fix: Skip NAT Entirely for Package Installs

Here's something most guides skip entirely: if the only thing your private instance needs the internet for is yum, you may not need a NAT gateway at all. Amazon Linux's package repositories are hosted in Amazon S3 buckets, and Amazon S3 supports a gateway VPC endpoint — a free route-table entry that lets a private subnet reach S3 directly over the AWS network, with no internet gateway and no NAT device involved.

  1. In the VPC console, go to Endpoints, choose Create endpoint, and select the S3 service with the Gateway type.
  2. Attach a policy that allows access to the Amazon Linux repository buckets for your Region (the bucket names follow a pattern like amazonlinux-2-repos-<region> or the Region-specific AL2023 mirror bucket).
  3. Select the private subnet's route table under Route Table Associations so AWS adds the S3 prefix-list route automatically.
  4. Re-run yum update — no NAT gateway, no internet gateway, and no data leaves the AWS network.

⚠️ What this actually breaks

This trick is specific to yum-based Amazon Linux images, because their repositories live on S3. Ubuntu and Debian's default apt sources point at Canonical's own mirrors, which aren't AWS-hosted, so a gateway endpoint doesn't get apt anywhere on its own — an Ubuntu instance in a private subnet still needs the NAT gateway (or a NAT instance, or a proxy) for real internet reach.

What This Actually Costs

A NAT gateway isn't free plumbing — it's a metered service, and it bills whether or not anything is passing through it. In the US East (N. Virginia) Region, AWS charges $0.045 per hour just for the gateway to exist, plus $0.045 per gigabyte of data it processes, on top of any standard data transfer charges for traffic that actually leaves AWS to the public internet. Run three NAT gateways for high availability across three Availability Zones, and you're paying that hourly rate three times over before a single package downloads.

✅ Why this is the one to use

Even if you keep the NAT gateway for general internet access, add the S3 gateway endpoint alongside it. Gateway endpoints for S3 and DynamoDB carry no hourly charge and no data-processing charge — Ethan's rule of thumb for Jake was blunt: "Anything that can go over a gateway endpoint instead of NAT is money you don't have to spend every single hour, forever. Turn it on even if you never look at the bill."

If you delete a NAT gateway you no longer need, the hourly charge stops immediately — there's no lingering commitment, so it's worth pruning NAT gateways in accounts where a private subnet's internet needs have shrunk to "just talk to S3."

When Nothing Works: Reachability Analyzer and Flow Logs

If you've checked all five fixes above and the instance still can't get out, stop guessing and let AWS trace the path for you.

  1. Open Reachability Analyzer in the VPC console and create a new path.
  2. For source, choose your instance; for destination, choose the internet gateway you expect the traffic to eventually reach.
  3. Run the analysis. It will report whether the path is reachable, and if it isn't, exactly which hop breaks it — a missing route, a blocking security group rule, or a network ACL denying the traffic.

If the path shows reachable but real traffic still fails, turn on VPC Flow Logs for the subnet and look for REJECT entries. A rejected flow tells you definitively whether a security group or a network ACL is the one dropping packets, instead of you working through both by inspection.

Public Subnet vs Private Subnet, Properly Defined

It's worth being precise here, since "public" and "private" get used loosely. Both terms describe the subnet's route table, not the instances in it or how locked-down they are.

  Public subnet Private subnet
Route table has0.0.0.0/0 → internet gatewayNo route to an internet gateway
Reachable from the internetYes, if the instance has a public or Elastic IPNo, never directly
Outbound internet accessDirect, via the internet gatewayOnly through a NAT gateway/instance sitting in a public subnet
Typical residentsLoad balancers, NAT gateways, bastion hostsApplication servers, databases, internal services

A subnet doesn't become private because you didn't assign public IPs to the instances in it — it's private because the route table itself has no path to an internet gateway. You can absolutely have a "public" subnet full of instances with no public IPs at all; they just wouldn't be reachable from, or able to reach, the internet even though the subnet's route table could support it.

Connecting a Public Subnet to a Private Subnet (the Right Way)

This is the exact conflation that trapped Jake, and it trips up almost everyone once: reaching an instance in a private subnet (for management, like SSH) and that instance reaching out to the internet (for updates) are two completely separate paths, using different mechanisms, and fixing one does nothing for the other.

For an administrator to reach a private instance, the traditional pattern is a bastion host: a small instance in the public subnet that you SSH into first, then hop from there to the private instance using its private IP address. Both subnets share the VPC's internal routing automatically — any two subnets in the same VPC can talk to each other over private IPs by default, using the local route that every route table gets, with no NAT gateway involved. That's why Jake's app server could always reach the database server; it never needed to leave the VPC to do it.

The more current alternative is AWS Systems Manager Session Manager, which lets you connect to the private instance's shell from the console or CLI without a bastion host, an open SSH port, or even a route to the internet on the instance itself — it uses the Systems Manager agent and an outbound connection the agent itself establishes, which for private instances typically goes through an interface VPC endpoint for Systems Manager rather than the public internet at all.

Edge Cases: Multi-AZ, IPv6, and Private NAT Gateways

A few situations that fall outside the standard five-fix path:

Option Handles internet access? Use it when
Public NAT gatewayYesStandard outbound-only internet access for a private subnet
Private NAT gatewayNo — internet gateway drops any traffic routed to it from a private NAT gatewayRouting between VPCs or to on-premises networks over a transit or virtual private gateway
Gateway VPC endpointOnly to S3 and DynamoDB, at no chargePackage repos, backups, or app data that lives in S3/DynamoDB
Egress-only internet gatewayIPv6 outbound only, no chargeIPv6-only workloads that need one-way outbound internet reach

For high availability, deploy one NAT gateway per Availability Zone and give each AZ's private subnets their own route table pointing at the NAT gateway in the same AZ. Skip this and route every private subnet through a single NAT gateway in one AZ, and an outage in that one AZ takes down internet access for private subnets in AZs that are otherwise perfectly healthy — because the traffic still has to physically cross to the surviving NAT gateway.

This same fix applies beyond EC2. AWS CodeBuild projects and Lambda functions attached to a VPC lose internet access for the identical reason — put them in a subnet with no route to a NAT gateway, and package downloads or external API calls from inside the build or function fail exactly like they do on an EC2 instance, because they're subject to the same VPC route tables.

Frequently Asked Questions

What's the difference between a public subnet and a private subnet in AWS?

A public subnet's route table has a route sending internet-bound traffic (0.0.0.0/0) to an internet gateway. A private subnet's route table doesn't have that route at all. It's entirely about the route table, not about the instances inside the subnet or their IP addresses.

How do I connect a public subnet to a private subnet in AWS?

They're already connected for internal traffic — every route table includes a local route, so any subnet in the same VPC can reach any other subnet over private IPs automatically. What you're usually really asking is how to reach a private instance for management (use a bastion host in the public subnet, or Systems Manager Session Manager) or how to give a private instance outbound internet access (route it through a NAT gateway placed in the public subnet).

Why can I SSH into my private instance but it still can't reach the internet?

Because you reached it through a bastion host or Session Manager over the VPC's internal routing, which doesn't require internet access at all. That confirms the instance and its security groups are healthy. The instance's own outbound internet access is a separate path, controlled by its subnet's route table and whether a NAT gateway is reachable from it.

Do I need a NAT gateway for every private subnet?

No — one NAT gateway, placed in a public subnet, can serve multiple private subnets in the same Availability Zone as long as each private subnet's route table points to it. For resiliency across Availability Zones, though, deploy one NAT gateway per AZ so an AZ failure doesn't take down internet access for private subnets in unaffected zones.

Can I use a NAT instance instead of a NAT gateway to save money?

Yes, a self-managed EC2 instance running as a NAT device is a valid alternative and is typically cheaper at low, steady traffic volumes. The trade-off is that you manage its patching, sizing, and failover yourself — a NAT gateway is fully managed and highly available within its Availability Zone with none of that operational overhead.

Why does my instance have no internet even though I created a NAT gateway?

Creating the NAT gateway is only half the setup. Check that it's in a public subnet, in the Available state, and that the private subnet's route table actually has a 0.0.0.0/0 route pointing at that specific NAT gateway ID — a NAT gateway with nothing routing to it does nothing.

Does a private subnet need an internet gateway at all?

Not directly — a private subnet's own route table never points to the internet gateway. But the VPC as a whole still needs one attached, because the NAT gateway (which lives in the public subnet) uses the internet gateway to reach the internet on the private instances' behalf.

How much does a NAT gateway cost per month?

In US East (N. Virginia), it's $0.045 per hour just for the gateway to exist — roughly $32 for a full month — plus $0.045 per gigabyte of data it processes, plus standard data transfer charges for traffic leaving AWS to the internet. Running one per Availability Zone for resiliency multiplies the hourly charge by the number of AZs.

Can I update yum or apt without any internet access at all?

For Amazon Linux, yes — its package repositories are hosted on Amazon S3, so a free S3 gateway VPC endpoint gets yum working with zero exposure to the public internet. Ubuntu and Debian's default apt mirrors aren't AWS-hosted, so apt still needs a real path out, through a NAT gateway or NAT instance.

Why does apt still hang after I fixed the route table?

Check network ACLs next. Unlike security groups, ACLs are stateless, so a custom ACL needs an explicit allow rule for the reply traffic on the ephemeral port range (roughly 1024–65535), on both the private subnet and the public subnet holding the NAT gateway. A missing return-path rule looks exactly like a routing problem from the instance's side.

What's the difference between a security group and a network ACL for this problem?

Security groups apply to individual instances and are stateful — allow the outbound request and the reply is automatically permitted. Network ACLs apply to the whole subnet and are stateless — you must separately allow both the outbound request and its return traffic. The default ACL allows everything; a custom ACL denies everything until you add rules.

Do I need a NAT gateway in every Availability Zone?

Not strictly — a single NAT gateway can serve private subnets across the VPC as long as their route tables point at it. But if that one AZ goes down, every private subnet routed through it loses internet access, even subnets in otherwise healthy AZs. One NAT gateway per AZ, with per-AZ route tables, avoids that single point of failure.

Can a private subnet reach another private subnet without a NAT gateway?

Yes. The local route that every route table includes automatically covers traffic between subnets in the same VPC over private IP addresses. A NAT gateway is only needed when the destination is outside the VPC — the public internet or another AWS service reached over the internet.

Why did my private subnet lose internet access after I changed something else?

The most common cause is a route table swap — associating the private subnet with a different route table (or the wrong one) removes its route to the NAT gateway without any obvious error. Recently deleted or recreated NAT gateways also get a new ID, and any route still pointing at the old ID silently stops working.

What is a VPC endpoint and when does it replace a NAT gateway?

A VPC endpoint lets a subnet reach specific AWS services privately, without going through an internet gateway or NAT device. Gateway endpoints (free) cover only Amazon S3 and DynamoDB. Interface endpoints, at their own hourly and per-GB cost, cover most other AWS services. Neither replaces a NAT gateway for traffic going to the general internet — they only replace NAT for the specific AWS services they support.

Is a bastion host the same thing as a NAT gateway?

No, and mixing them up is exactly what causes this confusion. A bastion host is for administrators reaching into a private instance (inbound management). A NAT gateway is for a private instance reaching out to the internet (outbound access). Both typically sit in the public subnet, but they solve opposite directions of traffic and neither one substitutes for the other.

Revision note. Written August 2026, covering the current Amazon VPC console and current NAT gateway, gateway VPC endpoint, and network ACL behavior. AWS occasionally renames console pages and adjusts NAT gateway pricing by Region, so double-check the rate you're quoted against your own Region before budgeting. If you've just spent your weekend chasing a "security group" that turned out to be a missing route, you're not the first, and you won't be the last — hopefully this got you back to the part of your Saturday that actually mattered.

Related