Hermes Wiki

Public vs. Private Subnets

Concept

The difference between a public and a private subnet is entirely a routing fact, not an inherent subnet property: a subnet is public if its route table has a route sending 0.0.0.0/0 (all traffic not otherwise matched) to an Internet Gateway (IGW), and private if it doesn't. Nothing else distinguishes them — no flag, no separate resource type, no naming convention enforced by the platform. An Internet Gateway is a horizontally-scaled, redundant VPC component that provides the actual internet connection; resources in a subnet whose route table points at it can reach the internet directly if they also have a public IP address or Elastic IP assigned — the route alone doesn't automatically give an instance internet reachability without one.

A resource in a private subnet has no route to an IGW at all, so it cannot be reached from, or reach, the public internet directly. For a private-subnet resource that still needs outbound-only connectivity (pulling packages, calling a third-party API), the private subnet's route table instead points its 0.0.0.0/0 route at a NAT device — see NAT Gateway vs. NAT Instance — which forwards the traffic out without making the resource itself inbound-reachable.

Tradeoffs

Public subnet Private subnet
Inbound reachability Directly reachable from the internet (given a public IP) Not reachable from the internet under any circumstances
Outbound reachability Direct, via the IGW Only via a NAT device (extra hop, extra cost) or not at all
Attack surface Every resource here is a potential direct target Zero direct exposure — an attacker must pivot through something else first
Typical residents Load balancers, bastion hosts, NAT Gateways — things that must accept unsolicited inbound traffic Databases, internal services, application servers, agent runtimes — anything that doesn't need to be a direct target

There's no scenario where "public" is simply the safer or simpler default — it's a deliberate exception granted only to the small set of resources that genuinely need to accept inbound internet traffic.

When to use / when not to

  • Decide per-resource, not per-tier: the question is "does anything on the public internet need to initiate a connection to this resource?" — not "is this resource part of the frontend or backend."
  • Default every resource to a private subnet. Only place something in a public subnet when it must accept unsolicited inbound internet traffic — a load balancer terminating public HTTPS traffic, a bastion host, or a NAT Gateway itself.
  • A resource that needs outbound internet access (installing packages, calling an external API) does not need a public subnet for that — that's exactly what NAT solves. "Needs outbound access" and "should be publicly reachable" are independent questions, and conflating them is what puts databases in public subnets unnecessarily.
  • Don't rely on a security group alone to compensate for a resource being in a public subnet it doesn't need to be in — a security group is one more layer of defense, not a substitute for removing exposure the resource never needed in the first place.

Common pitfall

Putting a database or internal service in a public subnet "to keep things simple" or because an early prototype didn't distinguish tiers, then relying entirely on security groups to block inbound access. This is a defense-in-depth failure waiting to happen: it works until a security group rule is loosened during an incident, a new ingress rule is added carelessly, or a misconfigured NACL exposes the wrong port — at which point the resource is directly internet-attackable because it never should have been routable from the internet in the first place. Removing the IGW route (i.e. actually being in a private subnet) closes that entire class of misconfiguration outright, rather than depending on every other control staying correctly configured forever.

Engineering Lens

Public-vs-private subnet placement is network-layer least privilege: grant internet routability only to the resources that have an actual, ongoing need for it, exactly the same reasoning that governs IAM permissions or database grants. The reason this is worth calling out explicitly rather than assuming "everyone gets this right" is that the failure mode is silent — a resource in a public subnet without a public IP looks fine right up until something (a new load balancer target, an Elastic IP added for debugging, a lifted restriction) gives it one, and the network boundary that should have made that a non-event instead becomes the only thing standing between "internal service" and "internet-facing." A reviewer checking subnet placement is really checking whether the blast radius of every other misconfiguration is bounded by the network topology or not.

Sources

Hermes Wiki