Week 3: Linux & OS Hardening Basics

Every server this course secures later — the web app in Week 6, the cloud instance in Week 8, the container in Week 9 — starts life as a Linux box, and most of them start insecure by default. This week is about the boring, high-leverage work of reducing what's actually exposed: who can do what, what's running that doesn't need to be, and how you'd know if any of it changed without your permission.

Module 3 of 15 Week 3 of 16 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Apply least privilege to users, groups and sudo access on a real Linux server
  • Reduce a server's attack surface by disabling unused services and locking down packages
  • Set up file integrity monitoring and auditd, and produce a baseline hardening checklist

1. Users, Groups & the Principle of Least Privilege

Least privilege means every user, process and service gets exactly the access it needs to do its job, and nothing more. On a fresh Linux box, this starts with never operating as root for day-to-day work — root can do anything, which means anything that compromises a root session compromises the entire machine.

creating a properly scoped user
# Create a non-root user for real work, add to sudo group for elevated commands only
$ adduser deploy
$ usermod -aG sudo deploy

# Check who's in a sensitive group before trusting the system's current state
$ getent group sudo
sudo:x:27:deploy,alice

# List all users with a valid login shell -- a service account with /bin/bash
# it never needs is itself a red flag
$ awk -F: '$7 ~ /sh$/ {print $1, $7}' /etc/passwd

Groups let you grant a capability to a set of users without editing permissions individually, and they're worth auditing specifically because group membership tends to only grow over time — someone gets added to docker or sudo for a one-off task and is never removed.

The docker group is functionally root

Membership in the docker group lets a user run containers with arbitrary host mounts — including mounting the entire host filesystem read-write into a container and editing anything as root from inside it. Adding a user to docker is, in practical security terms, equivalent to giving them passwordless sudo. Week 9 covers container security in depth, but this is worth knowing now.

2. sudo, SUID/SGID & Privilege Escalation Vectors

sudo lets a scoped user run specific commands as root — the mechanism that makes least privilege practical instead of purely theoretical. But a misconfigured sudo rule is one of the most common privilege-escalation paths on a real box:

a dangerous sudo rule, and why
# /etc/sudoers -- BAD: this user can run vim as root, with no restriction
deploy ALL=(ALL) NOPASSWD: /usr/bin/vim

# vim can shell out from inside itself:
#   :!bash
# ...which just became a root shell, no password required.

# GOOD: scope the command tightly, and never grant NOPASSWD on anything
# that can spawn a shell, edit arbitrary files, or read arbitrary files as root
deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx

A related, separate mechanism is the SUID bit — a file permission that makes a program run as its owner (often root) regardless of who executes it. It's how passwd lets a regular user update /etc/shadow (which they can't write directly) safely. An unexpected SUID binary is a red flag:

auditing SUID binaries
# Find every SUID binary on the system
$ find / -perm -4000 -type f 2>/dev/null

# A short, known list of legitimate SUID binaries is normal (passwd, sudo, su, ping).
# An unfamiliar SUID binary -- especially one in a writable directory, or one that
# lets you read/write arbitrary files -- is exactly what a privilege-escalation
# checklist (like the one Week 14's pentest lab walks through) looks for first.
The rule of thumb for sudo rules

If a command can be used to read an arbitrary file, write an arbitrary file, or spawn a shell — GTFOBins catalogs hundreds of common binaries that can do this — it should never be grantable via unrestricted sudo, NOPASSWD or otherwise. Scope every rule to the exact command and exact arguments needed.

3. Minimizing the Attack Surface: Services, Ports & Packages

Every running service is something an attacker can potentially reach and exploit. Hardening a fresh server starts with an honest inventory of what's actually listening, and turning off everything that isn't earning its place.

what's actually listening
# List every listening TCP/UDP port and the process behind it
$ ss -tulpn

Netid State   Local Address:Port   Process
tcp   LISTEN  0.0.0.0:22           sshd
tcp   LISTEN  0.0.0.0:80           nginx
tcp   LISTEN  127.0.0.1:5432       postgres    # bound to localhost only -- good

# Disable and stop anything you don't recognize or don't need
$ systemctl disable --now cups          # printing service on a headless server? unlikely needed
$ systemctl list-unit-files --state=enabled  # audit everything set to start at boot

The same discipline applies to installed packages, not just running services — a package installed "just in case" is still part of your attack surface even while dormant, because a future local vulnerability in it becomes exploitable the moment anything else on the box is compromised.

a minimal-footprint mindset
# Prefer installing exactly what a service needs, not a general-purpose toolbox image
$ apt list --installed | wc -l          # know your baseline; watch it creep upward over time
$ apt remove --purge    # remove, don't just disable, what you'll never use

# Bind services to localhost/internal interfaces unless they genuinely need
# to be reachable from outside -- 127.0.0.1 instead of 0.0.0.0 where possible
This is the same idea as Week 2's segmentation, one level down

Week 2 was about which networks can reach which zones. This is about which services, on a single box, are reachable at all — and which local users could reach them if that box were ever compromised. Both are the same underlying discipline: reduce what's exposed to only what's genuinely needed.

4. File Integrity Monitoring & auditd

Hardening reduces how a system can be attacked; monitoring tells you when something changed anyway. File integrity monitoring (FIM) takes a cryptographic snapshot of important files and alerts when they change unexpectedly — exactly the integrity leg of Week 1's CIA triad, applied to the filesystem itself.

a minimal FIM baseline with AIDE
$ apt install aide
$ aideinit                          # builds the initial database of file hashes

# Later, run a check -- any modified, added or removed file under watched
# paths (/etc, /bin, /sbin by default) gets reported
$ aide --check

# A modified /etc/passwd or /etc/sudoers you didn't change yourself is
# exactly the kind of finding this is designed to surface

auditd is a different, complementary tool: instead of periodic snapshots, it logs specific system calls and file access as they happen, in real time — who read or wrote a file, who ran a command, when.

a targeted auditd rule
# Watch for any write to /etc/passwd or /etc/shadow, and tag the log entries
$ auditctl -w /etc/passwd -p wa -k identity-changes
$ auditctl -w /etc/shadow -p wa -k identity-changes

# Later, search the audit log for anything tagged with that key
$ ausearch -k identity-changes

# This is exactly the "who did what, when" evidence Week 13's incident
# response process needs -- and it only exists if you turned it on beforehand
Logs you didn't turn on don't exist when you need them

The single most common gap in a real incident is discovering, mid-investigation, that the exact log needed to answer "when did this start, and what did the attacker touch" was never being collected. Setting up FIM and auditd now, before anything's wrong, is what makes Week 12's monitoring and Week 13's incident response actually possible later.

5. Building a Security Baseline Checklist

Hardening isn't a one-time task you do and forget — it's a baseline you define once and check every server against, every time. A short, concrete checklist beats a vague policy document that nobody actually applies:

a starter hardening checklist
[ ] No login as root; root login disabled in sshd_config (PermitRootLogin no)
[ ] SSH key-based auth only; password auth disabled (PasswordAuthentication no)
[ ] Every user account traced to a real person or documented service purpose
[ ] sudo rules scoped to specific commands, no blanket NOPASSWD ALL
[ ] `ss -tulpn` output reviewed; nothing listening that isn't required
[ ] Firewall default-deny inbound, explicit allow list only (Week 2)
[ ] Unattended security updates enabled for critical packages
[ ] FIM baseline established (AIDE or equivalent) and checked on a schedule
[ ] auditd watching /etc/passwd, /etc/shadow, /etc/sudoers at minimum
[ ] System time synced (NTP) -- accurate timestamps matter for every later log

Frameworks like the CIS Benchmarks formalize this into hundreds of specific, scored checks per operating system — worth knowing exists, even if this week's checklist is a deliberately smaller, practical starting point. Automating a checklist like this against every new server (via configuration management, or a tool like lynis) is what turns hardening from a one-time task into a repeatable guarantee.

6. Hands-on Exercise

Hands-on

Harden a fresh Linux VM, end to end

Spin up a disposable VM (a cloud free-tier instance, a local VirtualBox/UTM VM, or a Docker container with systemd if you're comfortable with that) and apply every control from this week, in order.

Part 1 — Users & access:

  1. Create a non-root user, add it to a sudo-equivalent group, and confirm you can perform admin actions without ever logging in directly as root.
  2. Write a scoped sudoers rule (via visudo) that lets your user restart one specific service with NOPASSWD, and confirm a different, unlisted command still prompts for a password.
  3. Run the SUID audit command from Section 2 and identify at least 5 SUID binaries present on a default install — for each, write one sentence on what it's for and why it needs the SUID bit.

Part 2 — Reduce the attack surface:

  1. Run ss -tulpn and list every listening service. For each one not required for your VM's actual purpose, disable it (systemctl disable --now) and confirm it no longer appears.
  2. Edit sshd_config to disable root login and password authentication (switch to key-based auth first, and confirm you can still log in, before disabling password auth — don't lock yourself out).
  3. Set up a default-deny firewall (ufw or iptables) allowing only SSH and any service you deliberately kept running.
Hint

Before disabling password authentication, open a second terminal session and confirm key-based login works in it while your first session stays connected — if you're wrong about your key setup, the second session is what saves you from being locked out of a VM with no other access path.

Part 3 — Monitoring & the final checklist:

  1. Install AIDE, initialize its database, then deliberately modify a file under /etc and confirm aide --check reports it.
  2. Set up an auditd rule watching /etc/passwd and /etc/sudoers, deliberately trigger it (edit one of those files), and confirm the change appears via ausearch.
  3. Go through Section 5's checklist item by item against your own VM, marking each as done, not applicable (with a reason), or a known gap — submit the completed checklist as your final deliverable.
Hint

"Not applicable, because..." is a completely legitimate answer on a real checklist — a VM with no web service genuinely doesn't need a WAF rule. The goal is a checklist you can defend line by line, not one where every box is checked without thinking about whether it applies.

7. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Why is granting a user unrestricted, passwordless sudo access to a text editor like vim a privilege-escalation risk?

Many common programs, including vim, can spawn a shell from within themselves. If that program is running as root via sudo, the shell it spawns is also root — turning "run this one editor as root" into "get an unrestricted root shell." The fix is scoping sudo rules to commands that genuinely can't be abused this way, or restricting arguments tightly.

Q2

What does the SUID bit actually do, and why is passwd a legitimate use of it?

SUID makes a program execute with the permissions of its owner (often root) rather than the user who launched it. passwd needs to write to /etc/shadow, which a regular user can't do directly — SUID lets it do that write safely, through a program with narrowly scoped logic, instead of giving the user broad write access to the file itself.

Q3

A service is bound to 127.0.0.1:5432 instead of 0.0.0.0:5432. What does that change about its attack surface?

Binding to 127.0.0.1 (localhost) means the service only accepts connections originating from the same machine — it's unreachable from the network at all, firewall rules aside. 0.0.0.0 means it listens on every network interface, making it potentially reachable from anywhere the network allows. A database that only ever needs to talk to an app on the same host has no reason to listen on anything but localhost.

Q4

What's the practical difference between what file integrity monitoring (AIDE) and auditd each tell you?

FIM (AIDE) takes periodic snapshots and tells you that a watched file changed since the last check — useful for catching drift, but with a gap in time and no record of who did it. auditd logs specific actions as they happen in real time, including which user and process performed them — the "who did what, when" detail an incident investigation actually needs, which a periodic hash comparison alone can't provide.