
Last topic I went round a node ripping out services and closing ports it never needed. That's one half of a habit I keep coming back to. The other half is the question those open ports raised and I didn't fully answer: fine, the port needs to be open, but open to whom? And the same question applies to people and cloud accounts, not just ports. Who can actually do what in here?
Both of those are the same idea wearing two hats. It's least privilege, and it shows up in two places that feel unrelated until you notice they're the exact same move: identity (who's allowed to do what) and network (who's allowed to reach what). So this one is least privilege applied to cloud IAM on one side, and a proper host firewall on the other. Same principle, two locks.
What least privilege actually is
Least privilege means giving anything, a person, a service, a machine, only the access it truly needs, and nothing spare. That's it. The reason it matters isn't tidiness, it's blast radius. When (not if) something gets compromised, the damage is capped at whatever that thing was allowed to do. An account that can read one storage bucket and nothing else is a bad day if it leaks. An account that can do anything is the end of the account.
Every extra permission is a loan you've taken out against the day something goes wrong. Least privilege is just refusing to borrow more than you need.
I covered the principle itself back in Topic 19. Here I want to make it concrete in the two places people most often get it wrong: cloud identity, and the network.
Identity: stop living as root
Every cloud account starts with one all-powerful login. On AWS it's the root account, the email and password you signed up with. It's the master key: everything, on every service, with no limits. And its power can't be reduced, which is the actual problem. If it's ever phished or leaked, the attacker doesn't get some access, they get all of it. There's no "well, at least they couldn't touch billing".
It's the exact same trap as logging into a Linux box as root for daily work, which is the whole reason the sudo habit exists. So the cloud version of that habit is: use root once to set things up, put multi-factor authentication on it, and then never sign in with it again for day-to-day work. Everything after that happens as a scoped identity.
Give people (and only people) named identities
The replacement for "everyone shares root" is an IAM user per person. IAM stands for Identity and Access Management, the part of the cloud that answers "who are you, and what are you allowed to do". A fresh IAM user starts with almost no permissions, and you grant exactly what they need through a policy, which is just a document listing allowed actions. Need to spin up VMs and read a bucket? The policy says those two things and stops.
Doing that per person gets old fast, so you use groups. A group is a bucket of users who do the same job; you attach the policy to the group once, and every member inherits it. Add a new developer, drop them in the Developers group, done, they get exactly the developer permissions and not a scrap more.
Set up, add MFA, then lock it away. Never daily.
Attach one policy: e.g. "Developers can use VMs + read this bucket".
Add people to the group. They inherit exactly that, no more.
Machines get roles, not passwords
Here's the bit people trip on, and it took me a moment to get straight too. Your servers need permissions as well, not just people. A VM that has to read from a storage bucket can't do it by default, and there's a right way and a wrong way to grant it.
The wrong way is to generate long-lived access keys (a username-and-password pair for machines) and paste them into a config file on the box. That's how credentials end up committed to a git repo, printed in a log, or baked into an image, and once they leak they work from anywhere until someone notices. The right way is an IAM role. What a role is: a set of permissions a machine can temporarily assume, rather than an identity with a fixed password. You attach the role to the VM, and it gets short-lived credentials that rotate on their own and never sit on disk.
This is the cloud mirror of the token trick I keep running into on the offensive side: a Kubernetes pod gets a service-account token, an EC2 instance gets a role. Same idea, and the same warning, whatever that machine can reach, anything that lands on the machine can reach too. So the role gets the minimum: read that one bucket, nothing else.
Permissions rot, so audit them
The thing nobody tells you is that least privilege isn't a one-off setup, it's gardening. Permissions creep. Someone needs elevated access for an incident and it never gets removed. A service gets a broad policy "just to get it working" and stays that way for two years. So you review, on a schedule: who has what, which roles haven't been used, what can be trimmed. The major clouds all ship a tool that flags the obvious over-permissioning for you, AWS has Trusted Advisor, Google Cloud has Security Command Center, Azure has Advisor. They won't do the thinking, but they'll point at the mess.
Network: who can reach what
Now the other lock. Last topic we listed what was listening on a node and found things bound to 0.0.0.0, meaning every network interface, reachable by anyone who can route to the box. Finding them was step one. Deciding who should reach each one, and enforcing it, is step two, and that's a firewall's whole job.
You've got two broad places to enforce it. A network firewall sits out in the infrastructure (a dedicated appliance, or your cloud's security groups) and controls traffic across the whole estate. A host firewall runs on the machine itself and controls just that box. They're layers, not either/or, and I'm going to do the host one here because it's the piece you can practise on a single VM tonight.
On Linux the actual filtering is done by netfilter, built into the kernel. You could drive it with iptables, which is powerful and famously fiddly, or with UFW (Uncomplicated Firewall), a friendlier front-end that turns "allow SSH from this one IP" into one readable line. I use UFW for host rules precisely because I make fewer mistakes with it, and a firewall rule you got subtly wrong is worse than no rule.
Hands-on: lock a box down with UFW
Here's the scenario, on a server I'll call app01. It serves a website on port 80 and I reach it over SSH on port 22. I want exactly this, and nothing else:
| Who | Port | Allowed? |
|---|---|---|
| Jump server (172.16.238.5) | 22 (SSH) | ALLOW |
| Jump server (172.16.238.5) | 80 (HTTP) | ALLOW |
| Internal network (172.16.100.0/28) | 80 (HTTP) | ALLOW |
| Anyone else | anything | DENY |
Step 1, see what's exposed. Before I touch the firewall I check what's actually listening, so I know what I'm protecting. Say I find port 8080 open too, some old dev server, that nobody should reach.
sudo ss -tulpn | grep LISTEN
# 0.0.0.0:22 sshd (want this, but locked to the jump box)
# 0.0.0.0:80 nginx (want this, from specific sources)
# 0.0.0.0:8080 leftover (do NOT want this exposed)
Step 2, set the default stance. This is the most important idea in the whole lab: default-deny. Allow all outbound (the box can still reach the internet for updates), but block all inbound unless a rule opens it. Anything I forget stays closed, which is the safe way round to be wrong.
sudo ufw default allow outgoing
sudo ufw default deny incoming
Step 3, open only the exact holes I listed. Each rule names a source and a port. Read them out loud, they say exactly what the table says.
# SSH, only from the jump server
sudo ufw allow from 172.16.238.5 to any port 22 proto tcp
# HTTP, from the jump server
sudo ufw allow from 172.16.238.5 to any port 80 proto tcp
# HTTP, from the internal /28 network
sudo ufw allow from 172.16.100.0/28 to any port 80 proto tcp
Port 8080? I don't write a rule for it. Default-deny already blocks it. But I'll add an explicit deny anyway, because six months from now a comment that literally says "8080 is blocked on purpose" saves me from wondering whether I forgot it.
sudo ufw deny 8080
ufw enable.Step 4, turn it on, carefully. Enabling the firewall can drop your current SSH session if you got the SSH rule wrong, so this is the one moment to double-check step 3 before you commit. It even warns you.
sudo ufw enable
# Command may disrupt existing ssh connections. Proceed (y|n)? y
Step 5, verify it did what you meant. This is the check that it worked, read the rules back and make sure they match the table.
And when a rule needs to go, delete it by its text or by its number from ufw status numbered. Small thing, but useful:
sudo ufw delete deny 8080
# or list them and delete by index
sudo ufw status numbered
sudo ufw delete 3
default deny incoming → allow only named source+port pairs → keep SSH scoped to your admin box → verify with ufw status before you walk away.Why it's really one idea
Step back and the two halves rhyme. IAM decides who can do what; the firewall decides who can reach what. Both default to "no", both open up only the specific thing that's needed, and both fail safe, if you forget to grant something, the worst case is it doesn't work and you add a rule, not that it's silently wide open. That "forgot it? then it's closed" property is the entire reason default-deny beats trying to block bad things one at a time. You will never think of all the bad things.
From the attacker's chair, this is exactly what makes a target boring. An over-permissioned role means one leaked credential owns the account. A flat firewall means one foothold reaches everything. Lock both down and a compromise stays a small, contained problem instead of the whole story. That containment is the point.
Where this leaves things
So: use root once and lock it away, give people scoped users through groups, give machines roles instead of keys, and review the lot on a schedule because permissions rot. On the network, default-deny inbound and open only the exact source-and-port pairs you can justify. Neither half is hard. Both are the sort of thing that's easy to skip when you're rushing and painful to have skipped when something goes wrong.
The honest bit: I'm still building the muscle for doing this at scale rather than one box or one account at a time. Hand-writing UFW rules per node is fine for a lab and silly for a fleet, same as the sudo and SSH config from earlier topics, it all wants to live in provisioning so a machine is born locked down. That's the thread I keep pulling on, and I haven't tied it off yet. If you've got a clean way to express "these IAM permissions, these firewall rules, nothing else" as code across a fleet, I'd genuinely like to hear how you do it.
FAQ
What is the principle of least privilege?
Least privilege means giving every person, service or machine only the permissions it genuinely needs to do its job, and nothing more. It limits the damage when something is compromised: an account that can only read one bucket can't wipe your whole environment, even in an attacker's hands.
Why shouldn't I use the AWS root account day to day?
The root account has unrestricted power over everything and its permissions can't be limited. If it's phished or leaked, the attacker owns the whole account. Use it once to set up individual IAM users and then lock it away with MFA, and do daily work as a scoped IAM user instead.
Why attach an IAM role to an EC2 instance instead of using access keys?
A role gives the instance short-lived, automatically-rotated credentials that never sit on disk. Long-lived access keys pasted into a config file are the classic thing that leaks into a repo or a log and hands an attacker your permissions. A role removes that whole failure mode.
What does ufw default deny incoming actually do?
It tells the firewall to block every inbound connection unless a later rule explicitly allows it. You then open only the exact ports and sources you need. This default-deny approach is far safer than trying to block bad traffic one rule at a time, because anything you forget is closed, not open.
Related reading
- Topic 22: Cutting the Node's Attack Surface (finding the open ports this post locks down)
- Topic 19: Kubernetes Least Privilege (the principle these two locks apply)
- Topic 9: Authorization and RBAC (least privilege for the cluster's own identities)
- Browse the whole Kubernetes Journey