Skip to content
Available for work
Adam
←  All posts

The Layer Under Everything - AWS IAM, VPCs, and Networking

The most recent stop on my AWS journey. IAM roles and least privilege, then the VPC fundamentals - subnets, route tables, gateways, and security groups - that finally explained why my ECS task couldn't reach the internet.

5 min readaws · iam · networking

TL;DR

  • IAM controls who can do what. Prefer roles (temporary credentials) over long-lived access keys.
  • A VPC is your private, isolated network in the cloud, carved into subnets.
  • Public vs. private subnets differ by one thing: a route to an Internet Gateway.
  • Security groups are stateful per-resource firewalls; NACLs are stateless per-subnet ones.
  • This is the layer that quietly breaks everything until you understand it.

Why I'm here

This is the most recent thing I'm learning. Getting ECS running last week kept surfacing the same wall: a task that couldn't pull its image, a database I couldn't reach, a security group I didn't understand. Every one was a networking or permissions problem. I'd been treating the network as magic. So I went down a layer to the foundation everything else sits on.

IAM: identity and least privilege

IAM (Identity and Access Management) answers "is this principal allowed to perform this action on this resource?" Everything in AWS is gated by it. The pieces:

  • Users - long-lived identities (ideally just for humans, with MFA).
  • Roles - identities that are assumed temporarily, handing out short-lived credentials. This is the right tool for services.
  • Policies - JSON documents granting/denying specific actions on specific resources.

The principle I now apply everywhere is least privilege: grant the minimum needed, scoped to specific resources. A policy for my app's S3 access looks like:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["s3:GetObject", "s3:PutObject"],
    "Resource": "arn:aws:s3:::taskflow-uploads/avatars/*"
  }]
}

Not s3:*, not Resource: "*" - exactly two actions on exactly one prefix. The big realization: roles beat access keys. My ECS task assumes a role and the SDK gets rotating temporary credentials automatically. No secret keys in env vars, nothing to leak. That connects straight back to the task role from my ECS post.

VPC: my own private network

A VPC (Virtual Private Cloud) is a logically isolated network inside AWS, defined by a CIDR block like 10.0.0.0/16 (65,536 private IPs). Nothing gets in or out except through gateways I explicitly create. Inside it, I carve subnets, each in a specific Availability Zone with a smaller CIDR like 10.0.1.0/24.

This is where "multi-AZ" from my durability lessons becomes concrete: I put subnets in at least two AZs so a single data-center failure doesn't take me down.

Public vs. private subnets - the thing that finally clicked

I assumed "public" and "private" were a checkbox. They're not - the difference is entirely in the route table:

  • A public subnet has a route 0.0.0.0/0 → Internet Gateway (IGW). Resources with a public IP can talk to the internet both ways.
  • A private subnet has no route to the IGW. Resources there have no inbound path from the internet.

So the design is: put the load balancer in public subnets, put the ECS tasks and database in private subnets. The world reaches the ALB; the ALB reaches my tasks internally; my database is unreachable from the internet entirely.

The NAT gateway mystery (my actual ECS bug)

Here's the exact thing that bit me last week. My ECS task in a private subnet couldn't pull its image or reach external APIs. Why? A private subnet has no route to the internet - but the task still needs outbound access (to pull from ECR, hit a third-party API).

The fix is a NAT Gateway: it lives in a public subnet and lets private resources make outbound connections while staying unreachable inbound. The private subnet's route table gets 0.0.0.0/0 → NAT Gateway. Outbound works; inbound is still blocked. That single concept explained a bug that had me stuck for an evening.

Security groups vs. NACLs

The last piece - two firewall layers people constantly mix up:

  • Security groups - attached to resources (ENIs/tasks/instances). Stateful: if you allow an inbound request, the response is automatically allowed back out. You write allow rules only. This is what I use 95% of the time.
  • Network ACLs (NACLs) - attached to subnets. Stateless: you must allow both directions explicitly, and they support deny rules. A coarser, subnet-wide layer.

For my stack, security groups do the work: the ALB's SG allows :443 from the internet; the ECS task's SG allows :3000 only from the ALB's security group (you can reference one SG from another - no IPs needed); the database's SG allows :5432 only from the task's SG. Each tier can talk only to the tier directly above it. Defense in depth, and it reads like the architecture itself.

How it all fits together now

Internet
   │  (:443)
[ ALB ]            ← public subnets
   │  (:3000, from ALB's SG only)
[ ECS tasks ]      ← private subnets, outbound via NAT Gateway
   │  (:5432, from task's SG only)
[ RDS Postgres ]   ← private subnets, no internet route at all

Every arrow is a security-group rule; every box's reachability is a route-table decision. The network stopped being magic.

Takeaways

  • Roles over keys, least privilege always. Scope actions and resources.
  • Public vs. private subnet = one route to an Internet Gateway. That's the whole difference.
  • NAT Gateway = outbound-only internet for private resources.
  • Security groups are stateful and reference each other - chain them tier to tier.

Where this leaves me

Eight months ago I was deploying by SSH and praying. Now I can package an app into a reproducible image, ship it through an automated pipeline, run it on Fargate across multiple AZs, store its data durably in S3, cache and stream and observe it, and secure the whole thing with least-privilege IAM and a properly segmented network. Each post in this series was a real wall I hit and climbed. The journey continues - next I want to go deeper on Kubernetes and infrastructure-as-code with Terraform - but for the first time, the cloud doesn't feel like magic. It feels like a system I understand.