← How Your App Gets to the Internet
Lesson 6 of 6

Cloud Networking: VPCs, Subnets, Security Groups, and Why NAT Exists

SoftwareIntermediate

The Layer Where Nothing Is Where You Left It

There is a particular kind of afternoon that almost everyone who works with cloud infrastructure has lost at least once. A database is running. An application is running. They are, according to the console, in the same network. The application cannot reach the database. Every guide found by searching says to check the security groups, so the security groups get checked, and they look fine, and the application still cannot reach the database, and by four o'clock somebody has opened a rule to the entire internet just to see whether that changes anything.

This happens because cloud networking is the one layer with no physical object to point at. A server is a computer, and you can hold that idea. A disk is a disk. But there is no cable in a virtual private cloud, no switch you can trace a wire out of, nothing you can look at to see where the packet stopped. All of it is configuration, and configuration that is invisible tends to get copied rather than understood.

The first lesson in this series introduced the shape of the arrangement: a private network, a public subnet with the load balancer in it, a private subnet with everything else, and a NAT gateway letting connections out but not in. That was the map. This lesson is the machinery underneath it - what a virtual network actually is when there is no wire, how addresses get carved up and why that decision is one of the few you cannot take back, what genuinely makes a subnet public, what NAT is really for, and how the two different firewalls work and why one of them behaves in a way that surprises people.

The examples use AWS names because they are the ones you are most likely to meet, but almost none of this is AWS-specific. Azure calls the network a virtual network and the firewall a network security group; Google calls the network a VPC and the firewall a firewall rule. The concepts underneath survive the rename.

A Virtual Network Has No Wires

Start with the thing that is genuinely strange, because everything else follows from it. Your server has an address, say 10.0.1.47. That address does not exist on any physical network. No switch anywhere has learned it. If you unplugged every cable in the data centre and read the traffic on them, you would not find a packet addressed to 10.0.1.47.

What actually happens is that the hypervisor on the physical host intercepts the packet your server sends, wraps it inside another packet addressed to the physical host where the destination virtual machine is currently running, and puts that on the real network. A mapping service - a distributed database of which virtual address currently lives on which physical machine - tells it where to send it. At the far end, another hypervisor unwraps it and hands the original packet to the destination as though it had arrived over a wire. Your network is a lookup table with an encapsulation format.

This is not trivia. It explains several things that are otherwise confusing. It explains how you and a thousand other customers can all use 10.0.0.0/16 without any of you colliding, because your packets are only ever interpreted in the context of your own network's identifier. It explains why isolation between two virtual private clouds is genuinely strong rather than a naming convention: there is no route for the mapping service to even look up. And it explains why you can create an entire network topology in eight seconds and destroy it in four, which is the property that makes the whole infrastructure-as-code approach from the previous lesson possible at all. You are not cabling anything. You are writing rows into a routing database.

It also explains a class of limits that look arbitrary until you know this. Traffic between instances is capped per instance rather than per switch, jumbo frames have a specific supported size because the encapsulation header has to fit, and you cannot run your own layer-2 protocols or send broadcast traffic. There is no layer 2. There is a lookup table pretending to be one.

One region, many zones

A virtual private cloud lives in exactly one region and spans every availability zone in it. The second lesson in this series covered what an availability zone is: a physically separate facility, close enough for fast links but far enough that one flood does not take out both. The VPC sits across all of them at once.

A subnet does not. A subnet lives in exactly one availability zone, and that single fact is the reason you end up with more subnets than you expected. You do not create a public and a private subnet. You create a public and a private subnet in each zone you intend to survive the loss of. If you have three zones and two tiers, you have six subnets, and each of them is a slice of your address range pinned to one building.

Diagram of a virtual private cloud spanning two availability zones. The VPC is labelled 10.0.0.0/16 and sits in one region. Each availability zone contains a public subnet and a private subnet: zone A has 10.0.0.0/20 public and 10.0.16.0/20 private, zone B has 10.0.32.0/20 public and 10.0.48.0/20 private. The public subnets contain load balancer nodes and a NAT gateway each. The private subnets contain application servers and a database. An internet gateway sits at the top edge of the VPC. Two route tables are shown: the public one sends 0.0.0.0/0 to the internet gateway, the private one sends 0.0.0.0/0 to the NAT gateway, and both carry an undeletable local route for 10.0.0.0/16.
The same arrangement from lesson 1, drawn honestly. Note that everything is duplicated per zone, including the NAT gateway, and that the two route tables are the only thing distinguishing a public subnet from a private one.

CIDR, Properly This Time

Lesson 1 gave the working rule: 10.0.0.0/16 means the first 16 bits are fixed and the rest are yours, bigger number means smaller range. That rule is correct and it will get you through most days. Here is what is underneath it, because the sizing decisions later in this section do not make sense without it.

An IPv4 address is 32 bits, which humans write as four decimal numbers so they can say them out loud. The number after the slash is the prefix length: how many of those 32 bits identify the network, leaving the remainder to identify hosts inside it.

text
10.0.1.0/24 written out in the form the machines actually use

    10  .   0  .   1  .   0
  00001010 00000000 00000001 00000000
  |------- network: 24 bits -------|-- host --|
                                    8 bits left

  So every address from 00000000 to 11111111 in that last octet
  belongs to this subnet:  10.0.1.0  through  10.0.1.255

  Widen it to /16 and you free up eight more bits:

  00001010 00000000 00000001 00000000
  |--- network: 16 bits ---|---- host: 16 bits ----|

  10.0.0.0  through  10.0.255.255

The arithmetic is always the same:  2 ^ (32 - prefix) = total addresses
text
Prefix    Total addresses    Usable in an AWS subnet    Rough purpose

  /28              16                        11          the smallest AWS allows
  /24             256                       251          a comfortable app tier
  /22           1,024                     1,019          a large tier
  /20           4,096                     4,091          a generous per-zone slice
  /16          65,536                          -         a whole VPC
  /8       16,777,216                          -         the entire 10.x private range

The gap between the second and third columns is the first thing that catches people. A /28 does not give you 16 addresses to use. AWS reserves five in every single subnet you create, and it is worth knowing which five, because the arithmetic never adds up until you do.

text
In a subnet 10.0.1.0/24, these five are never yours:

  10.0.1.0      the network address itself
  10.0.1.1      the VPC router
  10.0.1.2      the DNS resolver for this VPC
  10.0.1.3      reserved for future use
  10.0.1.255    the broadcast address
                (nothing broadcasts here, but the address is
                 still held back so the maths matches convention)

  256 - 5 = 251 usable

This is a real operational problem at the small end and people hit it during incidents rather than during planning. A /28 subnet has eleven usable addresses. A load balancer wants several per zone. Every container task on Fargate takes one. Every RDS instance takes one. Teams have had autoscaling silently stop scaling, in the middle of a traffic spike, because the subnet ran out of addresses, and the error surfaces as a launch failure rather than anything mentioning networks.

Two decisions you do not get to revisit

Almost everything in cloud infrastructure is reversible. You picked the wrong instance type, so you change it. You put the load balancer in the wrong subnet, so you move it. Networking has two exceptions, and they are worth spending real thought on because they will outlive your involvement with the system.

The first: you cannot shrink a VPC's address range after creating it. You can add extra ranges alongside the original one, up to a quota, but the primary block is fixed for the life of the VPC, and the only way to change it is to build a new VPC and migrate everything into it. Since private addresses cost nothing and consume nothing until an interface actually uses one, there is no reason to be frugal. Take a /16. It gives you 65,536 addresses and it costs the same as a /24 that gives you 251.

The second, and the more expensive mistake: two networks with overlapping address ranges can never be directly connected. Not peered, not joined over a transit gateway, not reached across a VPN to an office. Routing has no way to decide which of two identical destinations a packet is for, so the connection is simply refused at creation time.

This sounds abstract until you meet it. It is extremely common for a company to have several teams, each of whom independently accepted a console default of 10.0.0.0/16, or copied the same starter Terraform module. For years this is harmless, because the networks never touch. Then two products need to talk to each other, or the company acquires another company, or somebody wants a VPN from the office, and the answer is that one side has to be rebuilt from scratch. The fix costs weeks. The prevention costs one afternoon and a document.

text
An address plan, which is the entire prevention:

  10.0.0.0/16     production, eu-central-1
  10.1.0.0/16     staging, eu-central-1
  10.2.0.0/16     development, eu-central-1
  10.10.0.0/16    production, us-east-1
  10.20.0.0/16    the office network, over the VPN
  10.90.0.0/16    reserved: acquisitions, partners, anything unplanned

And then, inside production, carving one /16 into six /20 subnets
across three availability zones:

  10.0.0.0/20     public,  eu-central-1a      10.0.0.1   - 10.0.15.254
  10.0.16.0/20    private, eu-central-1a      10.0.16.1  - 10.0.31.254
  10.0.32.0/20    public,  eu-central-1b      10.0.32.1  - 10.0.47.254
  10.0.48.0/20    private, eu-central-1b      10.0.48.1  - 10.0.63.254
  10.0.64.0/20    public,  eu-central-1c      10.0.64.1  - 10.0.79.254
  10.0.80.0/20    private, eu-central-1c      10.0.80.1  - 10.0.95.254

  10.0.96.0 onward: still free, which is the point of taking a /16
A bit ruler showing how the same 32-bit address is divided at different prefix lengths. A horizontal bar of 32 cells is shown three times. At /8 the first eight bits are shaded as network and 24 remain for hosts, giving 16,777,216 addresses. At /16 the first sixteen are shaded, giving 65,536. At /24 the first twenty-four are shaded, giving 256. Below, a nesting bar shows the range 10.0.0.0/16 containing six /20 slices labelled with their availability zones and public or private role, with the remainder of the range marked as unallocated headroom.
Every bit you move the boundary to the right halves the range. The lower bar is the same /16 from the address plan above, carved into six zone-pinned slices with most of it deliberately left unused.

The private ranges you are choosing from are set by RFC 1918, and there are exactly three: 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16. The 10 range is the largest by an enormous margin, which is why cloud plans almost always live there. If you ever need to know why your home router hands out 192.168.1.x while your work laptop gets 10.something, that document from 1996 is the entire answer.

What Actually Makes a Subnet Public

Here is a claim that sounds pedantic and turns out to be the single most useful thing in this lesson: there is no such thing as a public subnet. There is no property on a subnet called public. Nothing in the data model records it. The word is a description that people apply to a subnet based on one line in a table somewhere else.

Every subnet is associated with exactly one route table. A route table is a list of destinations and where to send traffic for each. When a packet leaves an interface, its destination address is compared against every route in that table, and the most specific match wins - the one with the longest prefix, because a longer prefix means a narrower and therefore more deliberate statement about where that traffic goes.

text
The route table attached to the public subnets

  Destination        Target              Meaning
  10.0.0.0/16        local               anything inside this VPC, hand it
                                         straight over; cannot be deleted
  0.0.0.0/0          igw-0a1b2c3d        everything else, send to the
                                         internet gateway


The route table attached to the private subnets

  Destination        Target              Meaning
  10.0.0.0/16        local               same as above, always present
  0.0.0.0/0          nat-04f5e6d7        everything else, send to the NAT
                                         gateway, which will let it out but
                                         will not let anyone in


That is the whole difference. One word in one cell.

Two things in there deserve attention. The local route for 10.0.0.0/16 is created with the VPC, cannot be removed, and cannot be overridden by anything less specific. This is why every instance in a VPC can route to every other instance in it by default, across subnets and across availability zones, with no configuration at all. Routing is not where your isolation between tiers comes from. That job belongs to the firewalls, which are the next section.

And 0.0.0.0/0 is a prefix length of zero, meaning zero bits are fixed, meaning it matches every address in existence. It is the default route: the answer to "I have no more specific idea where this goes." Because longest match wins, adding a route for 52.94.0.0/16 pointing somewhere else would take precedence over it for exactly that range and leave everything else alone. That mechanism is how VPC endpoints, peering and on-premises links get inserted into an existing network without disturbing anything.

A useful consequence: to make a private subnet public you add one route, and to make a public subnet private you delete one. There is no migration, no rebuild, and no need to move anything. That is also the reason this is worth getting right in code rather than in the console, since a single-row difference between two environments is exactly the kind of drift nobody notices until it matters.

Why NAT Exists, Which Is Two Questions

Lesson 1 described the NAT gateway in one line: it lets connections out and lets the replies back in, while refusing to let anyone outside start a conversation. That is what it does. It is not why it exists, and the difference matters because people routinely treat NAT as a security control when security was never the design goal.

Network address translation was invented for arithmetic. IPv4 has 32 bits, so there are 4,294,967,296 addresses in total, which was an unimaginable number in 1981 and a visibly insufficient one by the early nineties. The response was to reserve a few ranges for private use, let every organisation on earth reuse them internally, and put a device at the boundary that rewrites the source address of outgoing packets to one genuinely public address that the organisation does own. A thousand machines, one public address. That is the whole idea, and it bought the internet roughly thirty extra years.

To do the rewriting, that device has to keep a table, because when a reply comes back to the single public address it needs to know which of the thousand internal machines the reply belongs to. It answers that by also rewriting the source port, giving each conversation a unique number to come back on.

text
The translation table, which is all a NAT gateway really is

Outbound: your app server calls an external API

  what the server sends      10.0.16.32:51234  ->  93.184.216.34:443
  what leaves the gateway    52.28.7.19:40001  ->  93.184.216.34:443
                             ^^^^^^^^^^^^^^^^
                             source rewritten, and the pair
                             (40001, 93.184.216.34:443) is written
                             into the table alongside 10.0.16.32:51234

Inbound: the reply arrives

  what arrives               93.184.216.34:443  ->  52.28.7.19:40001
  table lookup on 40001      found: 10.0.16.32:51234
  what gets delivered        93.184.216.34:443  ->  10.0.16.32:51234

Inbound: somebody on the internet tries to start a conversation

  what arrives               203.0.113.9:6666   ->  52.28.7.19:40001
  table lookup on 40001      the entry is for 93.184.216.34, not this
                             sender, so there is nothing to map it to
  what gets delivered        nothing. the packet is dropped.

Read that last block again, because it is the point. The inbound packet is not refused by a rule. Nobody wrote a policy saying to reject it. It is dropped because the gateway genuinely does not know which internal machine it could possibly be for. The one-way behaviour is not a feature that was designed; it is an unavoidable consequence of solving an addressing problem with a lookup table.

Diagram of network address translation in three flows. In the first, an outbound packet from an application server at 10.0.16.32 port 51234 passes through the NAT gateway and emerges with its source rewritten to 52.28.7.19 port 40001, with the mapping written into a translation table shown in the middle. In the second, the reply arrives addressed to 52.28.7.19 port 40001, the table is consulted, and the destination is rewritten back to 10.0.16.32 port 51234 and delivered. In the third, an unsolicited packet from 203.0.113.9 arrives at the same public address and port, finds no matching entry for that sender in the table, and is dropped before reaching the private subnet.
The third flow is the whole security reputation of NAT, and it is an accident. The packet is dropped because there is no table entry to map it back to, not because a rule rejected it.

Do not treat NAT as your inbound protection. It is an addressing mechanism whose side effect resembles a firewall, and the resemblance breaks down as soon as anything else is involved: a port forward, a peering connection, a VPN, or a compromised machine inside the subnet that opens the outbound connection itself and then does whatever it likes over it. The security group is doing the actual policy work. NAT is doing arithmetic.

There is a neat proof that these really are two separate concerns rather than one. IPv6 has 128 bits and therefore no scarcity problem at all, so there is nothing for translation to solve, and AWS does not offer a NAT gateway for it. What it offers instead is an egress-only internet gateway: a device that provides exactly the one-way property, outbound allowed and inbound refused, while performing no address translation whatsoever. Same behaviour, none of the rewriting. The two reasons come apart cleanly once the scarcity is gone.

On cost, lessons 2 and 5 in this series have already made the case at length and it will not be repeated here, beyond the shape of it: a NAT gateway bills by the hour whether or not anything uses it, bills again per gigabyte it processes, and you need one per availability zone if you want the zone-failure protection you built the multiple subnets for in the first place. Three zones means three of them, running continuously.

A large share of the traffic paying that per-gigabyte charge is going to the cloud provider's own services, which is faintly absurd - a private instance reaching S3 goes out through the NAT gateway, across the public internet path, and back into the same provider. A gateway VPC endpoint fixes this by adding a route so that S3 and DynamoDB traffic never leaves the VPC, and it is free. It is one of very few changes that reduces both your bill and your exposure at the same time. Interface endpoints, which cover most other services, do charge per hour, so those are a judgement call rather than an automatic yes.


The Two Firewalls, and Why One of Them Surprises You

There are two independent filtering layers in a VPC, and every packet has to satisfy both. They work differently enough that knowing which is which turns an afternoon of guessing into a two-minute check.

Security groups

A security group attaches to a network interface, not to a subnet. This is the first thing that makes it unlike the firewall you may be picturing. A traditional firewall sits at a boundary and everything crossing that boundary is subject to it. A security group travels with the machine, which means two servers sitting in the same subnet can have entirely different rules, and moving a server to another subnet changes nothing about its firewall.

It contains only allow rules. There is no deny. The absence of a matching allow is the denial, and the default for a new group is to allow nothing inbound and everything outbound. And it is stateful, which is the property that matters most in practice: if a connection is allowed in, its replies are allowed back out automatically, no matter what the outbound rules say. You never write a return rule.

The genuinely valuable feature, and the one most tutorials skip in favour of typing IP ranges, is that the source of a rule does not have to be an address range. It can be another security group. Read that rule as "anything carrying this badge may enter" rather than "anything from this address may enter."

text
A three-tier application, written the way that keeps working

  sg-alb          the load balancer
    inbound   443    from  0.0.0.0/0          the public, as intended
    inbound    80    from  0.0.0.0/0          only to redirect to 443
    outbound  8080   to    sg-app

  sg-app          the application servers
    inbound  8080    from  sg-alb             <- a group, not an address
    outbound  5432   to    sg-db
    outbound   443   to    0.0.0.0/0          calling external APIs

  sg-db           the database
    inbound  5432    from  sg-app             <- a group, not an address
    outbound (nothing configured; replies still work, because stateful)


Now scale the application tier from two servers to fifty.
The new servers launch with sg-app attached.
The database rule already permits them. Nothing to update, and
nothing to forget to update.

Compare that with the version written using address ranges. The database allows 10.0.16.0/20, which is the whole private subnet in one zone, so you also need 10.0.48.0/20 and 10.0.80.0/20 for the others. Now the database is reachable from anything at all that happens to be in those subnets, including a debugging container somebody started last month and forgot. The group-referencing version says what you actually mean: the database accepts connections from the application, and from nothing else, regardless of where anything is sitting.

The single most common serious mistake in this area is 0.0.0.0/0 on port 22, or 3389 for Windows. Automated scanners find a newly opened SSH port in minutes, not days, and from there it is a credential-guessing problem rather than a network problem. The modern answer is not a better bastion host, it is to have no inbound rule at all: Session Manager reaches the instance through an agent that dials outbound, so you get a shell on a machine whose inbound rules are completely empty.

Network ACLs, and the stateless trap

The second layer sits at the subnet boundary. A network ACL is evaluated for every packet entering or leaving the subnet, its rules are numbered and evaluated in order until one matches, and unlike a security group it can express an explicit deny.

It is also stateless, and that word is where the afternoon goes. Stateless means the ACL keeps no record of connections. It sees each packet in isolation, with no idea whether it is a reply to something. So allowing a request in does not allow the response out. You have to write that rule yourself, and to write it you need to know which port the response will be sent to.

text
What a security group needs to accept HTTPS:

  inbound   allow  tcp/443  from  0.0.0.0/0

  Done. Replies are allowed out because the group is stateful.


What a network ACL needs, for exactly the same traffic:

  INBOUND
    100   allow  tcp/443           from  0.0.0.0/0     the request
    *     deny   all               from  0.0.0.0/0     implicit, unremovable

  OUTBOUND
    100   allow  tcp/1024-65535    to    0.0.0.0/0     the reply <- this one
    *     deny   all               to    0.0.0.0/0     implicit, unremovable


Why that range: the client picked a random high-numbered port to
receive the answer on, so the reply is addressed there rather than
to 443. Linux picks from 32768-60999 by default, but you are not
filtering your own clients, so the safe range to permit is the wide
one. Miss this rule and the symptom is a connection that opens and
then hangs, which reads like an application problem and is not.
Side-by-side comparison of a stateful security group and a stateless network ACL handling the same HTTPS request and response. On the left, the security group checks the inbound request against one allow rule for port 443, records the connection, and lets the reply back out with no rule consulted. On the right, the network ACL checks the inbound request against its numbered inbound rules, keeps no record, and then evaluates the reply from scratch against a separate outbound rule list, where an allow rule for the ephemeral port range 1024 to 65535 is required or the reply is dropped.
One rule versus two, and the second one is on a port range nobody thinks about. This is the source of the classic symptom where a connection is accepted and then simply hangs.

The honest guidance about network ACLs is that most architectures should leave them alone. The default one permits all traffic in both directions, and that is a reasonable place for it to stay, because a subnet-wide stateless filter is a blunt instrument and every rule you add to it is a rule you have to remember exists at three in the morning. Put your policy in security groups, where it is stateful, attached to the thing it describes, and can reference other groups.

There is one job they do that security groups genuinely cannot, and it is the reason to keep them in mind: an explicit deny. If a specific address range is scanning you, or a compliance rule requires a hard block at the network edge, a security group has no way to express that at all, because it only knows how to allow. The ACL is where a deny lives. Use it for that, and resist using it for anything else.


When Traffic Does Not Flow: Check in This Order

Back to the lost afternoon from the opening. The reason it takes an afternoon is almost never that the problem is hard. It is that there are seven plausible causes and people check them in the order they happen to think of them, usually starting with the one they most recently read about. Checking them in a fixed order, outermost to innermost, turns it into a short exercise. Each rung either passes or gives you your answer.

A vertical ladder of seven checkpoints a packet must pass, drawn from outermost to innermost. From the top: the route table, asking whether a route to the destination exists at all; the network ACL, in both directions since it is stateless; the destination's security group inbound rules; the source's security group outbound rules; the host firewall such as ufw or iptables; whether the process is actually listening and on which interface; and finally DNS, asking whether the name resolved to the private address or the public one. Each rung is annotated with the command that answers it and the symptom it produces when it is the cause.
Seven rungs, checked top to bottom. The bottom two are not network problems at all, which is exactly why they cost people the most time.
bash
# 1. Is there a route to the destination at all?
#    Different VPC, on-premises, or internet: check before anything else.
aws ec2 describe-route-tables --filters Name=association.subnet-id,Values=subnet-0a1b2c3d

# 2. Network ACL, and remember it is stateless: check BOTH directions.
#    If the default has been modified, this is a strong suspect.
aws ec2 describe-network-acls --filters Name=association.subnet-id,Values=subnet-0a1b2c3d

# 3. The DESTINATION's security group: is there an inbound rule for
#    this port from this source? Usually the answer.
aws ec2 describe-security-groups --group-ids sg-0db1234

# 4. The SOURCE's security group outbound rules. Default allows
#    everything, so this only bites when somebody tightened it.

# 5. The host's own firewall, which the cloud console cannot see.
sudo iptables -L -n            # or: sudo ufw status verbose

# 6. Is the process even listening, and on which interface?
ss -tlnp | grep 5432
#    127.0.0.1:5432  -> listening on loopback only. Nothing outside
#                       the machine can ever reach it. Not a network
#                       problem, a config problem. This is number one
#                       on the list of things mistaken for firewalls.
#    0.0.0.0:5432    -> listening on all interfaces, as intended.

# 7. DNS: did the name resolve to the private address or the public one?
dig +short db.internal.example.com
#    A private address means you are going straight across the VPC.
#    A public one means you are leaving and coming back, which will
#    hit different rules, cost egress, and fail from a private subnet.

Before working through that list by hand, try VPC Reachability Analyzer. You give it a source and a destination and it evaluates the entire path statically - route tables, ACLs, security groups, gateways - without sending a single packet, and tells you which specific component blocked it. It answers rungs one to four in about thirty seconds. It cannot see inside the instance, so rungs five and six are still yours, but it removes most of the search space.

What to Take From This

The through-line of this lesson is that cloud networking looks arbitrary until you know which layer each piece belongs to, and then it stops. A virtual network is a lookup table, not a wire. A subnet is a slice of addresses pinned to one building. Public is not a property of a subnet, it is a description of its route table. NAT is arithmetic that happens to behave like a door. A security group travels with the machine and remembers your connections; a network ACL guards the subnet and remembers nothing.

Two things are worth doing something about rather than just knowing. Write down an address plan before you create your second VPC, because overlapping ranges are the one mistake here with a cost measured in weeks. And write your security group rules as references to other groups rather than address ranges, because that is the version that still says something true after the architecture has changed twice.

The next lesson in this roadmap is identity and access: least privilege, what a role actually is as distinct from a user, and how cloud accounts really get breached, which turns out to have very little to do with anything in this lesson and a great deal to do with a credential somebody committed to a repository.

Further reading