Cloud Networking

Public and Private Subnets

The difference is one line in a route table, not a property of the subnet. Which is why the useful question is not "is this subnet public" but "does this workload need a public address at all" — and for most workloads the answer is no.

▶ Run the lab

The question this answers

Infrastructure question

Which workloads need to be addressable from the internet, and what changes for the ones that do not?

Application requirement

Users must reach the API from anywhere. The API must reach the database. The database must be reachable from the API and from nothing else — not from the internet, not from a scanner, not from a compromised laptop on the corporate VPN.

What it provides

A placement rule with a hard consequence: workloads in a private subnet have no path from the internet at all, so an inbound attack has to compromise something else first.

Application RequirementInfrastructure RequirementComputeNetworkStorageIdentityDeploymentScalingReliabilityObservabilitySecurityCostTrade-offs

A subnet is public because of its route table

There is no attribute named "public" on a subnet. A subnet is public when its route table has an entry sending 0.0.0.0/0 to an internet gateway, and private when it does not. Everything else — that instances in it may have public addresses, that it can be reached from outside — follows from that one line. This is worth being pedantic about, because it means the property can be changed by a single route edit, and it means a subnet you believe is private is only private if you have actually read its route table.

The consequence for a private workload is total and cheap. There is no path for an inbound packet to take. A scanner sweeping the provider's public ranges cannot find it. A credential-stuffing bot cannot try passwords against it. A zero-day in the database's wire protocol cannot be exploited by anyone who has not already got inside the network. None of that required a rule, an agent or a subscription — it required the absence of a route.

The canonical layout falls straight out of it: the load balancer is in the public subnet because it must be dialable, and everything else is not. The application does not need a public address because the load balancer reaches it over the network's local route. The database does not need one because the application reaches it the same way. Adding a public address to either buys nothing and costs the entire benefit.

Internet → load balancer [public] → application [private] → database [private].PROVIDER-NEUTRAL
Userpublic
Virtual network 10.20.0.0/16
Internet gatewaypublic
public-a — route 0.0.0.0/0 → igwpublic
Load balancer :443public— public on purpose — this is the design
NAT gatewaypublic— outbound only, for the private tier
private-a — no 0.0.0.0/0 → igwprivate
API instancesprivate— no public address; reached over the local route
data-a — no egress route at allprivate
PostgreSQL :5432private— no inbound path from the internet exists to attack
UserInternet gateway· 443crosses boundary
Internet gatewayLoad balancer :443
Load balancer :443API instances· local route, port 8080
API instancesPostgreSQL :5432· local route, port 5432
API instancesNAT gateway· outbound API calls

Which workloads actually need a public address

The test is narrow: does something outside your network need to *initiate* a connection to this specific resource? Almost nothing does. A public entry point does — a load balancer, an API gateway, a CDN origin that cannot be private. A bastion or session-manager endpoint might, though a managed access service usually removes even that. Everything else is reached either through the entry point or over the network's local routes.

Note that "it needs to call out to the internet" is a different requirement and does *not* justify a public address. Outbound-initiated traffic is what a NAT Gateway or a private endpoint is for, and it preserves the no-inbound-path property. Conflating the two is how application instances end up with public addresses "so they can reach the payment API" — which grants inbound reachability the workload never needed.

Judge exposure in context rather than by reflex. A load balancer answering 443 from the whole internet is correct and should not be flagged. A database answering 5432 from the whole internet is a finding, and the fact that it has a strong password does not make it not a finding. See Public Exposure, Read With Context.

WorkloadNeeds inbound from the internet?PlacementVerdict on a public address
Load balancer / API gatewayYes — that is its entire job.Public subnetCorrect. This is the design, not an exposure finding.
Web or API application instancesNo — the entry point reaches them locally.Private subnetUnnecessary. Remove it; it only adds an attack surface.
Background workers and queue consumersNo — they pull work.Private subnetNever justified.
Relational databaseNo — only the application talks to it.Private data subnetA finding. A public 5432 is the classic breach headline.
Cache / message brokerNo.Private subnetA finding, and historically the source of some of the largest data exposures.
NAT deviceNo inbound; it needs a public address to translate to.Public subnetCorrect, and its state table is what blocks inbound.
Bastion / jump hostSometimes — prefer a managed access service instead.Public subnet if unavoidableTolerable with strong controls; better removed entirely.
Placement by requirement, not by habit.

What the private tier costs you

Private placement is not free, and pretending otherwise is how teams get surprised. A private workload that needs to reach anything outside — a package mirror, a container registry, a payment API, a telemetry endpoint — needs an egress path, and that path is a billed component with its own capacity limits and its own failure modes. One NAT device per zone is the correct reliability answer and it multiplies the fixed cost by the number of zones.

It also costs access. You can no longer SSH to an instance from your laptop, which is a security improvement and an operational inconvenience; the replacement is a managed session service or a bastion, both of which are more infrastructure. And debugging changes shape: a private workload that cannot reach the internet fails in ways that look identical to a firewall problem, because both produce a hang.

For a genuinely stateless public service with no private data — a static site, a stateless renderer — a single public subnet with tight security groups is a defensible design and saves all of this. The public/private split earns its cost when there is something behind the entry point worth keeping unreachable.

The bill for the private tier. Relative weights, not currency.COST-VARIES
Subnets and route tables fixed
driven by nothing — these are free objects · The split itself costs nothing. Everything below is the consequence.
NAT device-hours fixed
driven by one device per zone × hours · Zone redundancy multiplies this. That multiplication is the price of not having a single point of egress failure.
NAT data processed · surpriseusage
driven by every GB the private tier sends or receives externally · Image pulls and object-storage reads land here unless a private endpoint carries them. See NAT Gateway.
Private endpoints fixed
driven by per endpoint-hour plus data processed · Usually a net saving: it removes flows from the NAT meter entirely. See Private Connectivity.
Managed access service or bastion fixed
driven by session-hours or an always-on instance · The replacement for SSH from a laptop, and a real line item.

Bars are relative weights, not currency. Real rates depend on provider, region, commitment and volume.

Key points

  • A subnet is public if and only if its route table sends 0.0.0.0/0 to an internet gateway — there is no other attribute.
  • Private placement removes the inbound path entirely, which defeats scanning and remote exploitation without any rule being evaluated.
  • Needing to call out is not a reason for a public address; that is what a NAT device or private endpoint is for.
  • Exposure is judged in context: a public load balancer on 443 is the design, a public database on 5432 is the finding.
  • The private tier costs an egress path with its own bill and failure modes, plus the loss of direct SSH access.

The loop, answered

Every field is required, which is why no lesson here can recommend something without saying what it costs and what simpler thing to consider first.

How it works
  • Every subnet is associated with exactly one route table, which decides where non-local traffic goes.
  • A public subnet's table sends 0.0.0.0/0 to an internet gateway; resources there additionally need a public address to be reachable.
  • A private subnet's table has no such entry, so an inbound packet has no path in and an outbound one has no path out.
  • Local routes covering the network's own range exist in every table, which is why tiers reach each other without configuration.
  • Egress for the private tier is added by pointing 0.0.0.0/0 at a NAT device in a public subnet, preserving the no-inbound property.
What you still own
  • Own the placement rule per tier and enforce it in infrastructure code, so placement is not decided per deployment.
  • Own subnet sizing per zone, remembering that container platforms consume addresses far faster than virtual machines.
  • Own the periodic exposure review: enumerate every resource with a public address and justify it.
  • Own the replacement for SSH — a managed session service, audited, rather than a bastion everyone shares.
  • Own the egress design, because a private subnet without one produces failures that look like firewall problems.
How it fails
  • An instance in a public subnet with no public address: unreachable from outside, reachable from inside, and it looks like a firewall problem.
  • A private subnet with no egress route, so package installs and API calls hang at start-up while the instance itself is healthy.
  • A route added to a private subnet during debugging and left there, silently making a data tier internet-reachable.
  • Address exhaustion in the private subnet, blocking deploys while every existing instance keeps running.
  • A single-zone private subnet, so a zone failure removes a whole tier even though other zones have capacity.
How it scales
  • Private subnets need to be sized generously per zone; the fleet grows into them and the range cannot be widened afterwards.
  • Each zone needs its own private subnet and its own egress path, so the design scales by repetition rather than by growth.
  • Egress capacity scales separately from the fleet: doubling the instances doubles the outbound connections through one NAT device.
  • Public subnets stay small — they hold entry points, not fleets.
Security
  • The strongest control available: no inbound path means no inbound attack, independent of software versions or credentials.
  • It is a layer, not authentication. A compromised application instance can still reach the database exactly as designed. See Infrastructure Trust Boundaries.
  • Removing public addresses from application instances shrinks the attack surface to the entry points, which is a much smaller set to defend and to audit.
  • Default-deny outbound from the private tier is the next step most environments skip, and it is what turns a compromise into a contained one. See Public Exposure, Read With Context.
Cost shape
  • The split itself is free; the egress path it requires is not.
  • NAT devices bill per hour and per gigabyte, and reliability multiplies the hourly part by the zone count.
  • Private endpoints for provider services usually pay for themselves by removing traffic from the NAT meter.
  • The access replacement — a managed session service or bastion — is a small but permanent line item.
What to watch
  • A recurring inventory of public addresses and internet-facing security-group rules, diffed over time.
  • Available addresses per subnet, per zone, alerted before exhaustion rather than during a deploy.
  • Flow-log rejects at the private tier boundary, which show what is trying to reach it and failing.
  • The signal that lies: an instance health check that passes. It proves the process is alive, not that anything can reach it or that it can reach anything.
Simpler alternatives
  • A single public subnet with strict security groups, for a stateless service with no private data — cheaper and simpler, and defensible.
  • A platform-as-a-service that owns the network, when the application has nothing private to place.
  • Private endpoints instead of a NAT device, when the only outbound needs are provider services — cheaper and it removes a component.
  • No egress at all for a worker that reads a queue and writes a database over private endpoints. Not every private subnet needs a NAT.
What adopting this costs
  • Buys the removal of an entire class of attack; costs a metered egress path, an access-service dependency and harder debugging.
  • Buys clean tier boundaries that document themselves; costs an address plan with per-zone repetition.
  • Buys audit simplicity — the public list is short; costs the convenience of reaching a machine directly when something is wrong.

Public, private, and which one is actually a finding

Public, private, and which one is actually a finding
Place the load balancer, the application and the database, then run reachability. Not every public thing is a mistake — the point is telling the two apart.
Load balancer
Application
Database
Database security group
One VPC, one zone, two subnetsPROVIDER-NEUTRAL
VPC 10.0.0.0/16
public subnet 10.0.1.0/24public— route 0.0.0.0/0 → internet gateway
load balancer :443public— the front door — public on purpose
private subnet 10.0.2.0/24private— route 0.0.0.0/0 → NAT gateway
app :8080private
database :5432private— security group: sg-app → 5432
load balancer :443app :8080· 8080
app :8080database :5432· 5432
Internetload balancer:443REACHABLE
By design. This is the front door: a public address on 443 with TLS terminated here is what "reachable by users" means.
load balancerapp:8080REACHABLE
Allowed by a rule that names the load balancer’s security group as the source — not a CIDR. The app has no public route of its own.
Internetapp:8080no route
No route: a private subnet has no path from the internet gateway. The security group is the second layer, not the only one.
appdatabase:5432REACHABLE
The only path that should exist to 5432, and it is scoped to the app’s security group.
Internetdatabase:5432no route
No route and no rule. Two independent layers would both have to be wrong.
appInternet:443REACHABLE
Outbound only, through a NAT gateway. The app can call the payment API; the payment API cannot call the app.
Scanner output
no findings · public load balancer on 443 is the design, not a finding
The reference design: the load balancer is public because users must reach it, the app is private and only accepts traffic from the load balancer’s security group, and the database is private and only accepts traffic from the app’s. The database has no route to or from the internet at all — and note the honest limit of that: a private subnet reduces exposure, it is not a substitute for authentication. Anything already inside the VPC still needs credentials.
PROVIDER-NEUTRALSIMULATED

Public exposure scanner: the port is not the verdict

Public exposure scanner: the port is not the verdict
Eight resources, scanned. Half of the public ones are the design and half are the incident — and which is which depends on what the resource holds and who it is for, not on whether it has a public address.
rules that fired
R1 public + management or administrative plane → finding, whatever the data class. Control planes are reached through a private path or an identity-aware proxy, never from the open internet.
R2 public + audience is the internet + data is public → by design. This is what the resource is for.
R3 public + audience is the internet + non-public data + authenticated → by design. An authenticated API over TLS is the normal shape of a product.
R4 public + audience is the internet + non-public data + no authentication → finding. Anonymous read of customer data is the finding, not the public IP.
R5 public + audience is narrower than the internet → finding. The exposure exceeds the intended audience; that gap is the whole vulnerability.
R6 not public → no exposure finding. Private is not the same as authenticated: rule R6 says nothing about who inside the network may read it.
context for “object storage — customer invoices”
what does it hold?
who is it for?
public
7
by design
3
findings
4
rule
R5
“object storage — customer invoices” is a finding under R5: Intended audience is company staff, actual audience is everyone. The exposure is wider than the purpose. Change the context on the right and watch the verdict move — the same public bucket is a static website or a customer-data breach depending only on what somebody put in it, and the same open port is a product or an incident depending on who it was meant for. That is why exposure findings need an owner who knows the intent, and why "no public IPs" is a policy that both over- and under-protects.
ILLUSTRATIVErule-based, and the rules are printed below on purpose

What people believe, and what is true

Claim

Private subnet means the workload is secure.

Reality

It means unreachable from the internet. Lateral movement, stolen credentials and a compromised peer inside the same network are all unaffected by it.

Claim

The application needs a public address to call external APIs.

Reality

Outbound-initiated traffic goes through a NAT device or a private endpoint. A public address grants inbound reachability the workload never asked for.

Claim

Anything public is a security finding.

Reality

A load balancer on 443 and a NAT device are public by design. The finding is a public data store, an open admin port, or an entry point nobody can explain.

Apply it