Module 15 — SSH and Security Hardening
Updated 3 September 2026
Every server you will ever manage, you reach through SSH. It is the front door — which makes it the single most-attacked service on any internet-facing Linux box. This module teaches SSH from the cryptography up: what actually happens in the handshake, why key-based login is both easier and safer than passwords, and then the concrete, ordered steps to harden a server so that front door holds. This is the module that turns "I can log in" into "I can secure a fleet."
Legend used throughout: 🧠 concept → 🧩 analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)
You need Module 4 — Users, Groups, and Permissions (permissions — SSH lives or dies on ~/.ssh being 700 and authorized_keys being 600), Module 11 — systemd and Services (systemd — sshd is a service you reload, and you read its logs with journalctl), and Module 13 — Networking (networking — the firewall that fronts port 22). Tools: an Ubuntu 24.04 machine with OpenSSH (sudo apt install openssh-client openssh-server). Almost everything here is safe and local — we generate keys in a throwaway directory and validate config files without ever applying them. The one rule that matters: never change SSH auth on a remote box without a second session already open.
Part A — SSH: what it actually is
A1. The problem SSH solves, and the handshake
🧠 SSH — Secure Shell — is an encrypted protocol for running a shell (and copying files, and forwarding ports) on a remote machine over an untrusted network. It replaced telnet and rlogin, which sent everything — including your password — as plaintext anyone on the wire could read. SSH gives you three guarantees at once: confidentiality (the session is encrypted), integrity (tampering is detected), and authentication in both directions — the server proves it is the machine you meant to reach (host keys, A2), and you prove you are who you claim (a password or, better, a key, Part B).
🧠 The handshake, in plain terms: your client opens a TCP connection to port 22; the two sides negotiate algorithms they both support; they run a key exchange (Diffie-Hellman) that lets them agree on a shared secret without ever sending it — so even someone recording the whole conversation can't derive it; the server authenticates itself with its host key; and finally you authenticate. From that point the whole session is protected with fast symmetric encryption using the freshly-agreed key.
Two spies who've never met need to talk secretly in a crowded room. Using slow, elaborate public-key signatures they first verify each other's identity and agree — out loud, but in a way eavesdroppers can't reuse — on a shared codebook. That setup is fiddly and slow, so they do it once. From then on they just speak using the codebook: fast, and meaningless to everyone listening. The slow ceremony bootstraps trust; the fast codebook carries the conversation.
Where the analogy stops working. Real spies could be fooled by someone impersonating the other spy — the whole hard problem SSH solves with host keys (A2) is making that impersonation detectable. And the "codebook" isn't handed over at all: Diffie-Hellman lets both sides derive the same secret independently from public exchanges, so there's never a moment where the shared key exists on the wire to be stolen. The analogy's single shared object is exactly the thing SSH is careful never to transmit.
🧪 Exercise A1.1 — Look at your SSH client and its effective config (read-only, safe anywhere)
ssh -V # which OpenSSH version (and OpenSSL) you have
ls -la /etc/ssh/ # the system SSH files: client config, sshd config, host keys
ssh -G github.com | grep -E '^(user|hostname|port|identityfile)' # key lines of the EFFECTIVE client config✅ Expected result — click to reveal
OpenSSH_9.6p1 Ubuntu-3ubuntu13.18, OpenSSL 3.0.13 30 Jan 2024
total 660
-rw-r--r-- 1 root root 1649 ssh_config
-rw-r--r-- 1 root root 3517 sshd_config
-rw------- 1 root root 399 ssh_host_ed25519_key
-rw-r--r-- 1 root root 89 ssh_host_ed25519_key.pub
...
user zaeem
hostname github.com
port 22
identityfile ~/.ssh/id_ed25519What to read out of it:
- ssh -V prints to stderr, not stdout — a classic gotcha when you try to capture it in a script (ssh -V 2>&1). The version matters: OpenSSH ≥ 8.8 disabled the old ssh-rsa SHA-1 signature by default, which is a real-world "why can't I connect to this old server?" cause.
- /etc/ssh/ holds two different files people constantly confuse: ssh_config (defaults for the client, when you SSH out) and sshd_config (config for the server daemon, when others SSH in). The sshd — with a d for daemon — is the server. Getting these mixed up is the most common SSH config mistake.
- The ssh_host_* files are the machine's host keys (A2) — note the private one is -rw------- (600, owner-only) and the .pub is world-readable. ssh -G host is a gift: it prints the fully resolved config (merging ~/.ssh/config, /etc/ssh/ssh_config, and defaults) that would apply to that hostname, without connecting — the fastest way to answer "which key/user/port will ssh actually use here?"
A2. Host keys and known_hosts — trust on first use
🧠 When a server is installed, it generates host keys — a keypair that identifies the machine itself (separate from any user's key). On your first connection, ssh shows you the server's key fingerprint and asks "are you sure?" You say yes, and ssh records that key in ~/.ssh/known_hosts. On every future connection it checks the server's presented key against that stored copy. If they match, silence — you're talking to the same machine. This model is called TOFU: Trust On First Use. You take one leap of faith at the start, and SSH guarantees consistency ever after.
🧠 If the stored key and the presented key don't match, ssh refuses to connect and prints a loud banner: WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! with Someone could be eavesdropping on you right now (man-in-the-middle attack)! This means one of two things: something malicious is intercepting your connection, or — far more often — the server was legitimately rebuilt, reinstalled, or replaced (new machine, new host key). Knowing which is a judgement call, and it's exactly the kind of thing an interviewer probes.
The first time a friend calls you from a new number, you can't be sure it's them — until they say something only they'd know, and you save the number under their name. Every call after that, you recognise the number and the voice instantly; if one day "their" number calls but the voice is a stranger's, alarm bells ring. That's TOFU: one careful first verification, then effortless recognition, with a loud alert the moment something doesn't match.
Where the analogy stops working. A human can be fooled by a good impressionist; a host key cannot be faked without stealing the server's private key, which never leaves the server. And your phone's memory of the voice is fuzzy and forgiving, whereas known_hosts is a byte-exact cryptographic match — it will not "mostly recognise" a key. The human version degrades gracefully; the SSH version is deliberately all-or-nothing, because a partial match is exactly what an attacker would produce.
🧪 Exercise A2.1 — Read a host key's fingerprint (read-only, safe anywhere)
ls /etc/ssh/*.pub # the host's public keys, one per algorithm
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub # its fingerprint (what you verify on first connect)
# on a real first connection you'd compare the fingerprint ssh shows you to this value✅ Expected result — click to reveal
/etc/ssh/ssh_host_ecdsa_key.pub
/etc/ssh/ssh_host_ed25519_key.pub
/etc/ssh/ssh_host_rsa_key.pub
256 SHA256:tXnVM3+hlvAB7BDftbcd9jcAvuqP9FuKnrwo/so2TAE root@vm (ED25519)What to read out of it:
- A server has one host key per algorithm (ed25519, ecdsa, rsa). Modern clients prefer ed25519. The 256 is the key size in bits; SHA256:... is the fingerprint — the exact string ssh shows you as "The authenticity of host '...' can't be established. ED25519 key fingerprint is SHA256:tXnVM3..." on a first connection.
- The whole security of TOFU rests on you actually comparing that fingerprint to a trusted source the first time — cloud providers print the expected host-key fingerprints in the instance console/logs for exactly this reason. After that, known_hosts does the comparing for you automatically.
- ssh-keyscan hostname fetches a server's host keys over the network (useful for pre-seeding known_hosts in automation) — but note it fetches whatever key the host presents, so it provides no protection against a MITM at scan time; it's a convenience, not a verification.
TOFU does not scale: no one manually verifies 500 fingerprints, and rebuilding hosts (which changes their host keys) would trigger the scary warning constantly. Two real answers. (1) Distribute known_hosts centrally — your config-management tool (Ansible/Chef) collects every host's public key and pushes a complete known_hosts (or /etc/ssh/ssh_known_hosts) to every client, so trust is established by the automation, not by humans clicking yes. (2) Better: an SSH certificate authority. You sign each host key with an internal CA once; every client is told to trust that one CA (@cert-authority in known_hosts), and from then on any host with a CA-signed key is trusted automatically — even brand-new ones — with no TOFU prompt and no per-host distribution. Certificates are how large fleets escape the known_hosts treadmill entirely.
Part A — Interview questions
🎯 "What is SSH?" — asked verbatim in GeeksforGeeks' Linux Interview Questions (71 questions)
SSH (Secure Shell) is an encrypted protocol for secure remote access — running a shell, executing commands, and transferring files (via scp/sftp) over an untrusted network, on TCP port 22 by default. It replaced plaintext tools like telnet. It provides confidentiality (encryption), integrity (tamper detection), and mutual authentication: the server proves its identity with a host key, and the client authenticates with a password or a key pair.
The details that separate candidates: mention that SSH combines asymmetric crypto to bootstrap (key exchange + identity) and symmetric crypto for the session (speed) — and that "authentication" runs both ways, which is what host keys are for. Weak answers describe SSH as "logging in remotely"; strong answers describe what it protects against (eavesdropping and impersonation on a hostile network) and how the two kinds of crypto divide the labour.
🎯 "What's the difference between symmetric and asymmetric encryption, and where does SSH use each?"
Symmetric: one shared secret key encrypts and decrypts; fast; the problem is getting the shared key to both sides safely. Asymmetric: a keypair where the public key encrypts/verifies and the private key decrypts/signs; solves key distribution but is slow. SSH uses asymmetric crypto at connection start — a Diffie-Hellman key exchange to agree a shared secret without transmitting it, plus host-key and user-key signatures for authentication — then switches to a symmetric cipher (AES-GCM, ChaCha20-Poly1305) for the actual session data.
The details that separate candidates: the shared session key is never sent over the wire — Diffie-Hellman lets both ends derive it independently, so recording the handshake doesn't reveal it (and modern SSH rekeys periodically for forward secrecy). Candidates who say "SSH encrypts with your private key" have it backwards — your private key signs/proves identity; the session is symmetric.
🎯 "You SSH to a server you use daily and get 'REMOTE HOST IDENTIFICATION HAS CHANGED'. What do you do?"
Do not reflexively delete the known_hosts entry and reconnect — that's exactly what an attacker performing a man-in-the-middle needs you to do. First ask: is there a legitimate reason the host key would change? A rebuild, reinstall, IP reuse, or migration all change host keys. Verify the new fingerprint out of band — from the cloud console, provisioning logs, or a teammate who can read it locally. If it matches the legitimate new key, remove the stale entry with ssh-keygen -R hostname and reconnect, confirming the fingerprint. If you can't explain the change, treat it as a potential attack and investigate before connecting.
The details that separate candidates: naming that the warning is SSH's MITM detection working as designed, not a bug to silence; distinguishing the common benign cause (rebuild) from the dangerous one (interception) instead of assuming either; and knowing the surgical fix (ssh-keygen -R host) rather than nuking the whole known_hosts file.
Part B — Key-based authentication
B1. Keypairs — generating and understanding them
🧠 A key pair is two mathematically-linked files: a private key (id_ed25519) that never, ever leaves your machine, and a public key (id_ed25519.pub) that is safe to hand out freely. You put the public key on every server you want to reach; you guard the private key like a house key. Authentication works by challenge-response: the server sends a random challenge, your client signs it with the private key, and the server verifies the signature with your public key. Crucially, the private key itself is never transmitted — the server only ever sees signatures it can verify, never the secret that made them.
🧠 Choose ed25519 (ssh-keygen -t ed25519) for new keys: it's small, fast, and as strong as a 3000-bit RSA key. Use RSA (-t rsa -b 4096) only when you must talk to something too old to understand ed25519. A passphrase on the private key encrypts it at rest, so a stolen laptop doesn't hand over a working key — the thief also needs the passphrase (and ssh-agent, B3, means you only type it once per session anyway).
Imagine you manufacture a padlock and its one unique key. You keep the key forever and never show it to anyone. You mail copies of the open padlock to every friend whose house you want to enter. Each friend clips your padlock onto their door. When you arrive, only your key opens it — and you prove ownership by opening it, never by describing the key. Losing a padlock copy tells a thief nothing useful; only the key matters, and the key never left your pocket.
Where the analogy stops working. A physical key can be copied by anyone who briefly holds it; a private key proves ownership without being shown, via signatures, so no amount of watching the login reveals it. And the padlock/key here aren't used to lock a channel — they're used to prove identity; the actual "locking" of your SSH session is done by the separate symmetric session key from Part A. The analogy captures authentication, not encryption.
🧪 Exercise B1.1 — Generate and inspect a key pair (safe — we use a throwaway directory, not your real ~/.ssh)
mkdir -p /tmp/keydemo && cd /tmp/keydemo
ssh-keygen -t ed25519 -f ./id_ed25519 -N '' -C 'zaeem@laptop' # -N '' = no passphrase (demo only); -C = a label
ls -l id_ed25519 id_ed25519.pub # note the permissions on each
cat id_ed25519.pub # the public half — safe to share
ssh-keygen -lf id_ed25519.pub # its fingerprint✅ Expected result — click to reveal
Generating public/private ed25519 key pair.
Your identification has been saved in ./id_ed25519
Your public key has been saved in ./id_ed25519.pub
The key fingerprint is:
SHA256:hPrvsCqU1S8LFqGDxsSa1Mf2AKg8cUUJ0bM3/k6Q6NU zaeem@laptop
-rw------- 1 you you 399 id_ed25519
-rw-r--r-- 1 you you 94 id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMIMatWwoTcMYYAVLiCyyYoMiO9mwWZNMlbXdakD2kxv zaeem@laptop
256 SHA256:hPrvsCqU1S8LFqGDxsSa1Mf2AKg8cUUJ0bM3/k6Q6NU zaeem@laptop (ED25519)What to read out of it (your fingerprint will differ — every key is unique):
- Look at the permissions ssh-keygen set for you: the private key is -rw------- (600, owner read/write only) and the public key -rw-r--r-- (644). This isn't cosmetic — ssh will refuse to use a private key that others can read (UNPROTECTED PRIVATE KEY FILE!), which is the same permissions-discipline from Module 4 — Users, Groups, and Permissions showing up as a hard security rule.
- The public key is one line: ssh-ed25519 (the type), the base64 key material, and your -C comment (here zaeem@laptop — purely a label to tell keys apart; it has no security role). This exact line is what goes into a server's authorized_keys (B2).
- The ed25519 private key is tiny — 399 bytes. Generate an RSA-4096 key and its private file is ~3400 bytes for equivalent-or-lesser security. Smaller keys, faster crypto, same strength: the reason ed25519 is the modern default (and ssh-keygen with no -t now generates ed25519 by default).
B2. authorized_keys — putting your key on the server
🧠 On the server, each user account has a file ~/.ssh/authorized_keys — a list of public keys allowed to log in as that user. When you connect, sshd checks whether your key's signature matches any line in that file. To add your key, you append your .pub line to it. The tool ssh-copy-id user@host does exactly that safely (it also creates ~/.ssh with the right permissions) — the one time you do use password auth, to install the key that lets you stop using password auth.
🧪 Exercise B2.1 — Build an authorized_keys file and see the permissions rules (safe — all local, no server needed)
cd /tmp/keydemo
mkdir -p server_home/.ssh # pretend this is the target user's home
cat id_ed25519.pub >> server_home/.ssh/authorized_keys # install the PUBLIC key (never the private one)
chmod 700 server_home/.ssh # the directory: owner-only
chmod 600 server_home/.ssh/authorized_keys # the file: owner read/write only
ls -ld server_home/.ssh && ls -l server_home/.ssh/authorized_keys
# in real life, this whole dance is done for you by: ssh-copy-id zaeem@server✅ Expected result — click to reveal
drwx------ 2 you you 4096 server_home/.ssh
-rw------- 1 you you 94 server_home/.ssh/authorized_keysWhat to read out of it:
- The directory is drwx------ (700) and the file -rw------- (600) — the exact modes sshd's StrictModes demands. If you chmod 777 either one and try a real login, sshd would ignore the key and you'd be back at a password prompt, often with nothing but a generic Permission denied (publickey) to explain it.
- Notice you appended the .pub file — the public key. A shockingly common beginner mistake is copying the private key to the server "to log in"; that both fails and defeats the entire model. The private key stays on your laptop; only the public key is ever installed.
- ssh-copy-id exists precisely so you don't do this by hand on real servers: it appends your public key over one password-authenticated session and sets these permissions for you. After it succeeds, your next login uses the key — and that's the moment you can safely disable password auth (C1).
B3. ssh-agent — type your passphrase once
🧠 A passphrase-protected key would make you retype the passphrase on every single connection. ssh-agent is a small background process that holds your decrypted keys in memory: you unlock the key once with ssh-add (typing the passphrase that one time), and every ssh after that asks the agent to sign challenges — no more typing. Agent forwarding (ssh -A) extends this: it lets a remote server you're logged into use your local agent to authenticate onward to a third server, so you can hop A→B→C without copying keys onto B. Powerful — and, forwarded carelessly, dangerous.
🧪 Exercise B3.1 — Load a key into the agent (safe — local; we start our own agent and kill it after)
cd /tmp/keydemo
eval "$(ssh-agent -s)" # start an agent and export its env vars into this shell
ssh-add ./id_ed25519 # load the key (would prompt for a passphrase if the key had one)
ssh-add -l # list the identities the agent now holds
ssh-agent -k # stop this demo agent (cleanup)✅ Expected result — click to reveal
Agent pid 3190
Identity added: ./id_ed25519 (zaeem@laptop)
256 SHA256:hPrvsCqU1S8LFqGDxsSa1Mf2AKg8cUUJ0bM3/k6Q6NU zaeem@laptop (ED25519)What to read out of it:
- eval "$(ssh-agent -s)" is the idiom that trips people up: ssh-agent -s only prints shell commands that set SSH_AUTH_SOCK and SSH_AGENT_PID; the eval is what actually applies them to your current shell. Run ssh-agent without eval and the agent starts but your shell never learns its socket — so ssh-add fails with Could not open a connection to your authentication agent. (Your desktop session usually starts an agent for you, so day-to-day you just run ssh-add.)
- ssh-add -l lists loaded keys by fingerprint — the same SHA256:... you saw in B1. This is how you answer "which keys does my agent currently hold?" — useful when a login unexpectedly succeeds or fails.
- The key is now in memory decrypted. That's the convenience and the risk: it's why forwarding this agent (-A) to an untrusted host is dangerous, and why ssh-add -t 3600 (auto-expire after an hour) or ssh-add -D (drop all keys) exist.
You do not put your personal key in authorized_keys on 500 machines — rotating or revoking it would mean editing 500 files. Two fleet patterns replace it. (1) A bastion/jump host: every SSH into the fleet goes through one hardened gateway with ProxyJump, so only the bastion is exposed to the internet and only its authorized_keys (or CA trust) must be managed. (2) Short-lived SSH certificates: instead of static keys in authorized_keys, a central CA issues you a certificate that's valid for, say, 8 hours and names which servers and users it's good for; servers trust the CA, not individual keys. Revocation becomes "stop issuing certs to that person" rather than "edit 500 files," and there are no long-lived keys to leak. This is how Netflix (BLESS), Facebook, and most large shops actually do SSH at scale.
Part B — Interview questions
🎯 "How does SSH key-based authentication work, and why is it more secure than a password?" — a variant of GeeksforGeeks' Q32 ("securely connect to a remote server using SSH")
You generate a key pair; the public key goes into ~/.ssh/authorized_keys on the server, the private key stays on your client. At login the server issues a random challenge; your client signs it with the private key; the server verifies the signature with your stored public key. It's more secure than a password because the secret never travels — the server only ever sees signatures, so there's nothing on the wire to intercept and nothing short enough to brute-force. Password auth, by contrast, evaluates a guessable secret server-side on every attempt, which is what makes port 22 worth brute-forcing.
The details that separate candidates: it's challenge-response, not "the key is sent and compared"; the private key can be passphrase-encrypted at rest (so a stolen laptop isn't an instant compromise) with ssh-agent caching the unlock; and the correct hardening sequence is install key → verify it works → then disable passwords, never the reverse.
🎯 "What's in a user's ~/.ssh directory, and what permissions does each part need?"
~/.ssh/ (mode 700) typically holds: your private keys (id_ed25519, mode 600 — ssh refuses to use them otherwise), the matching public keys (.pub, 644), authorized_keys (mode 600 — public keys permitted to log in as this user), known_hosts (host keys of servers you've connected to — the TOFU database), and optionally config (per-host client settings: hostname, user, port, identityfile, ProxyJump). sshd's StrictModes ignores authorized_keys if ~/.ssh or the file is writable by group/other.
The details that separate candidates: distinguishing the two "keys" files that beginners conflate — authorized_keys (inbound: who may log in as me) versus known_hosts (outbound: servers I trust) — and naming permissions as a functional requirement, not a nicety: loose perms don't just look bad, they make key auth silently fail.
🎯 "What is SSH agent forwarding and what's the risk?"
ssh-agent caches your decrypted keys so you unlock them once; agent forwarding (ssh -A) lets a server you've logged into reach back to your local agent to authenticate onward — handy for multi-hop access without copying keys onto intermediate hosts. The risk: anyone with root on the forwarded-to host can use the agent socket to authenticate as you to anywhere your keys are trusted, for the life of your session. The safer alternative is ProxyJump/-J, which routes the connection through a jump host without exposing your agent to it.
The details that separate candidates: explaining why it's dangerous (root on the box owns the forwarded socket → impersonation, not just key theft), and reaching for ProxyJump as the modern default instead of -A. This is one of GeeksforGeeks/real-interview favourites because it separates people who've used SSH from people who understand it.
Part C — Hardening the SSH server
C1. sshd_config — the changes that actually matter
🧠 The server daemon's behaviour lives in /etc/ssh/sshd_config. Out of dozens of directives, a handful deliver almost all the security. In rough order of value: PasswordAuthentication no (require keys — the single biggest win; it makes brute-forcing port 22 pointless), PermitRootLogin no (nobody logs in directly as root — they log in as themselves and sudo, which preserves an audit trail), PubkeyAuthentication yes (keep key auth on), AllowUsers/AllowGroups (an explicit allowlist of who may SSH in at all), MaxAuthTries (cap guesses per connection), and X11Forwarding no (turn off what you don't use). Changing the Port away from 22 is popular but is noise reduction, not security — see the note below.
🧠 The rule that saves careers: the order of operations. Set up and test key login first; only once a key login works do you set PasswordAuthentication no. Do it backwards — disable passwords before your key works — and you lock yourself out of a remote machine with no way back in except the cloud console. Always make auth changes with a second SSH session already open as a safety line, and validate the file with sshd -t before reloading.
🧪 Exercise C1.1 — Validate and inspect sshd config without touching the running server (safe — -t and -T only read/validate; they change nothing)
sudo sshd -t ; echo "exit: $?" # syntax-check the live config (silent + exit 0 = valid)
sudo sshd -T | grep -iE '^(permitrootlogin|passwordauthentication|pubkeyauthentication|port|maxauthtries)'
# now prove the validator catches mistakes, on a COPY (never the live file):
cp /etc/ssh/sshd_config /tmp/sshd_test.conf
echo 'Prot 2' >> /tmp/sshd_test.conf # a typo'd directive
sudo sshd -t -f /tmp/sshd_test.conf ; echo "exit: $?"✅ Expected result — click to reveal
exit: 0
port 22
maxauthtries 6
permitrootlogin without-password
pubkeyauthentication yes
passwordauthentication yes
authorizedkeysfile .ssh/authorized_keys .ssh/authorized_keys2
/tmp/sshd_test.conf: line 132: Bad configuration option: Prot
/tmp/sshd_test.conf: terminating, 1 bad configuration options
exit: 255What to read out of it:
- sshd -t is your seatbelt: it parses the config and exits 0 silently if valid, non-zero with the offending line if not. Run it before every reload — a config that fails to parse can leave you with an sshd that won't start, and if you've closed your sessions, that's a lockout. The deliberate typo Prot produces Bad configuration option: Prot ... exit 255 — proof the check works.
- sshd -T dumps the fully-effective configuration — every directive with its actual value, including defaults you never set. This is how you answer "what is sshd really doing?" without guessing from the file's comments. Here the defaults show passwordauthentication yes and permitrootlogin without-password — i.e. this box is not yet hardened; those are the two lines you'd change.
- permitrootlogin without-password is an alias for prohibit-password: root may log in by key but not password. Full hardening for most shops is PermitRootLogin no (no direct root at all — log in as yourself, then sudo), which keeps every privileged action attributable to a person.
Nobody hand-edits sshd_config on 500 servers — it drifts, and one fat-fingered file locks out a host. The fleet approach: sshd_config is a template owned by configuration management (Ansible/Puppet/Chef). The desired hardening (no root, no passwords, allowlist, MaxAuthTries) is declared once; CI runs sshd -t against the rendered file before it's ever shipped; and the tool rolls it out gradually with a health check, so a bad change is caught on host 1, not host 500. The reload is automated too — and because config management also manages the keys/CA trust, "make every server refuse password logins" becomes a one-line change reviewed in a pull request, not 500 SSH sessions.
C2. Applying changes safely, and defending against brute force
🧠 After editing sshd_config and passing sshd -t, apply it with sudo systemctl reload ssh — reload, not restart. Reload re-reads the config without dropping existing connections, so your safety-line session survives even if the new config is wrong. (A restart briefly stops the daemon; on a misconfig it may not come back.) Then, as in C1's trap, open a fresh session to confirm before you trust it.
🧠 Even with keys-only auth, bots will still try — filling your logs and wasting cycles. Two layers stop them: fail2ban (or sshguard) watches the auth log for repeated failures from an IP and temporarily firewalls that IP out; and the firewall itself (Module 13 — Networking's ufw) can restrict port 22 to known office/VPN ranges, or rate-limit new connections with ufw limit. Failed and successful logins are recorded in /var/log/auth.log (Debian/Ubuntu) — the file fail2ban reads and the first place you look after an incident. (On a minimal Ubuntu 24.04 image that ships without rsyslog, there's no auth.log file — the same SSH auth events live in the systemd journal instead: journalctl -u ssh.)
🧪 Exercise C2.1 — Read the auth log the way fail2ban does (read-only, safe anywhere)
ls -l /var/log/auth.log # who owns it: syslog:adm, mode 640
sudo grep -iE 'sudo|session opened' /var/log/auth.log | tail -3 # real privileged-action trail
sudo grep -iE 'Failed password|Invalid user' /var/log/auth.log | tail -3 # brute-force footprints (if any)✅ Expected result — click to reveal
-rw-r----- 1 syslog adm 16781 /var/log/auth.log
vm sudo: root : PWD=/home/claude ; USER=root ; COMMAND=/usr/bin/apt-get remove -y tree
vm sudo: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=0)What to read out of it (a fresh box has few or no failed logins — that's the healthy case):
- /var/log/auth.log is owned syslog:adm, mode 640 — you need root or membership in the adm group to read it, because it's a security-sensitive record. Every sudo command is logged here with who ran what (USER=root ; COMMAND=...) — this is the audit trail that PermitRootLogin no exists to preserve. On a real internet-facing server, the Failed password for invalid user admin from <ip> lines would scroll endlessly — that background brute-force is exactly what fail2ban watches and bans.
- On a normal Debian/Ubuntu server under attack you'd see lines like Failed password for root from 203.0.113.9 port 54120 ssh2 and Invalid user oracle from 198.51.100.4 repeating from many IPs. fail2ban counts these per source IP over a time window and, past a threshold, inserts a firewall rule dropping that IP for a bantime — turning a relentless log into a quiet one. (This demo box isn't internet-facing, so it has none — the format above is what you'd match in production.)
- The takeaway chain: keys-only auth makes the brute force useless, fail2ban makes it quiet, and a ufw allowlist on port 22 makes it impossible from anywhere but trusted networks. Defense in depth — no single layer is the whole answer.
Part C — Interview questions
🎯 "How do you secure / harden a Linux server?" — GeeksforGeeks Q16 ("How do you secure a Linux server?"), asked in almost every ops interview
Give a layered answer, SSH first since it's the front door: require keys, disable passwords (PasswordAuthentication no), disable direct root login (PermitRootLogin no — log in as a user and sudo), allowlist users (AllowUsers/AllowGroups), cap MaxAuthTries. Then beyond SSH: a default-deny firewall exposing only needed ports (Module 13 — Networking's ufw), fail2ban against brute force, keep the system patched (Module 9 — Package Management / unattended-upgrades), least privilege (sudo not root, drop unused services), and audit logging (/var/log/auth.log, last).
The details that separate candidates: stating the order (test keys before disabling passwords; keep a session open) and framing it as defense in depth — no single control is trusted alone. Weak answers list random tips; strong answers give a prioritised, reasoned checklist and note the failure mode each control addresses.
🎯 "Does changing the SSH port improve security? Why or why not?"
Only marginally, and not in the way people think. Moving off port 22 reduces log noise from automated bots that only scan the default port — a real operational benefit for readability. But it is security through obscurity: a targeted attacker port-scans and finds SSH on any port in seconds, so it stops no one who matters. It also adds friction (everyone must remember the port). The controls that actually make brute force impossible are key-only auth and no root login; the firewall allowlist and fail2ban handle the rest.
The details that separate candidates: not dismissing the port change outright or overselling it — naming its one genuine benefit (quieter logs) while being clear it's cosmetic next to no-password/no-root. Interviewers use this to catch cargo-cult hardening.
🎯 "How would you defend an SSH server against brute-force attacks?"
Make guessing useless, then quiet, then impossible: (1) PasswordAuthentication no — keys can't be brute-forced, so the attack can't succeed at all; (2) fail2ban/sshguard — watch /var/log/auth.log and temporarily firewall IPs that rack up failures, stopping the log spam and load; (3) firewall the port to known networks (ufw allow from <cidr> to any port 22) or ufw limit to rate-limit new connections; (4) PermitRootLogin no and MaxAuthTries to shrink the attack surface further.
The details that separate candidates: leading with keys-only auth as the structural fix (fail2ban alone still allows a lucky guess against passwords), and knowing fail2ban works by reading the auth log and inserting firewall rules rather than being magic. Bonus: mention that a bastion host means only one machine is even reachable to attack.
Part D — Host hardening beyond SSH
D1. Least privilege — users, sudo, and the attack surface
🧠 The principle of least privilege is the spine of host security: every user, process, and service should have the minimum access it needs and nothing more. In practice that means: people log in as themselves, not root, and use sudo for the specific privileged actions they need — which both limits blast radius and records who did what in the auth log. Root's power is defined in /etc/sudoers (edited only with visudo, which validates syntax before saving so you can't lock yourself out of sudo) and drop-in files under /etc/sudoers.d/. Grant narrow rights (alice ALL=(ALL) /usr/bin/systemctl restart nginx) rather than blanket ALL=(ALL) ALL, and treat NOPASSWD as a red flag to justify, not a default.
🧪 Exercise D1.1 — Inspect privilege: sudo rights, the shadow file, and SUID binaries (read-only, safe anywhere)
sudo -l # what am I allowed to run as root?
getent passwd root ; getent passwd sshd # a login account vs a locked service account
sudo getent shadow root | cut -d: -f1,2 # the shadow entry (password hash lives here, not /etc/passwd)
find /usr/bin -perm -4000 -type f # SUID binaries: run as their OWNER regardless of who runs them✅ Expected result — click to reveal
User you may run the following commands on vm:
(ALL : ALL) ALL
root:x:0:0:root:/root:/bin/bash
sshd:x:104:65534::/run/sshd:/usr/sbin/nologin
root:*
(or on a box with a root password: root:$y$j9T$... )
/usr/bin/sudo
/usr/bin/passwd
/usr/bin/mount
/usr/bin/su
/usr/bin/chsh
...What to read out of it:
- sudo -l lists your privileges; (ALL : ALL) ALL means full root (a personal admin box). On a hardened server you'd want to see scoped entries instead. The sshd account shows the least-privilege pattern in the OS itself: UID 104, no home, shell /usr/sbin/nologin — it exists only to run the daemon and cannot be logged into, so compromising sshd doesn't hand over a usable shell.
- The x in /etc/passwd means "password is in /etc/shadow" — which only root can read (Module 4 — Users, Groups, and Permissions). The hash field here is * (or !), meaning no password login is possible for root — normal and good on a key-only box. A real password shows a hash like $y$... (yescrypt, Ubuntu 24.04's default) or $6$... (SHA-512); the $id$ prefix names the algorithm.
- SUID binaries (-perm -4000) run with the file owner's privileges (usually root), no matter who launches them — that's how an ordinary user can run passwd to edit the root-owned shadow file. A stock Ubuntu has ~13 of these (sudo, su, passwd, mount, umount, chsh, chfn, gpasswd, newgrp, …). Auditing this list matters: an unexpected SUID-root binary is a classic privilege-escalation backdoor, which is why find / -perm -4000 is a standard post-incident and hardening check.
D2. The security mindset — layers, patching, and the audit trail
🧠 No single control secures a host — you stack layers so that defeating one still leaves others (defense in depth). The everyday layers beyond SSH: patch promptly (most breaches exploit known, already-fixed bugs — apt upgrade, or unattended-upgrades for automatic security patches, Module 9 — Package Management); default-deny firewall exposing only required ports (Module 13 — Networking); minimise attack surface — every service not running is a service that can't be exploited, so disable unused ones (Module 11 — systemd and Services); mandatory access control — Ubuntu ships AppArmor (profiles confining what a program can touch), the Debian-world counterpart to SELinux; and a readable audit trail (/var/log/auth.log, last, lastlog, who) so you can answer "who logged in, and when?" after the fact.
Diagram source
flowchart TD
A["Attacker on the internet"] --> B{"Firewall: is the port even reachable?<br>(ufw default-deny, port 22 allowlisted)"}
B -->|"blocked"| X1["Stopped — never reaches sshd"]
B -->|"port 22 allowed"| C{"sshd: password auth?"}
C -->|"PasswordAuthentication no"| X2["Brute force is useless — keys only"]
C -->|"passwords on"| D{"fail2ban: too many failures?"}
D -->|"IP banned"| X3["Source firewalled out for bantime"]
D -->|"under threshold"| E{"Got a valid credential?"}
E -->|"no"| X4["Denied + logged to auth.log"]
E -->|"yes, as a normal user"| F{"Needs root? must sudo"}
F -->|"least privilege + audit"| G["Actions logged, blast radius limited"]A castle never relies on one wall. There's a moat, then an outer wall, then a gatehouse with a portcullis, then an inner keep, then the guards inside. An attacker must beat all of them in sequence; the defenders need only one layer to hold. Each layer also buys time and raises alarms — crossing the moat is noticed before the gate is even reached. That's defense in depth: firewall (moat), key-only SSH (gate), fail2ban (guards who lock out repeat troublemakers), least privilege (the keep — even inside, most rooms are locked).
Where the analogy stops working. Castle layers are physical and static; a server's layers are only as strong as their configuration, which drifts — an unpatched service is a secret tunnel under the wall, invisible until exploited. And a castle's attacker is one army at the gate, whereas a server faces thousands of automated, simultaneous, tireless probes a day, from everywhere, forever. The castle can eventually win a siege; a server is under permanent siege and must simply never fall — which is why patching (closing tunnels you didn't know existed) has no real castle equivalent.
🧪 Exercise D2.1 — Read the host's own audit trail and exposure (read-only, safe anywhere)
who # who is logged in right now
last -n 5 # recent login history (from /var/log/wtmp)
lastlog | head -5 # last login time PER account (flags never-used accounts)
ss -tlnp # what's LISTENING — every open port is attack surface (Module 13)✅ Expected result — click to reveal
(who: often empty on a non-interactive/cloud box — no TTY sessions)
wtmp begins Fri Apr 10 02:20:03 2026
Username Port From Latest
root **Never logged in**
daemon **Never logged in**
bin **Never logged in**
State Recv-Q Send-Q Local Address:Port
LISTEN 0 128 0.0.0.0:22
LISTEN 0 4096 127.0.0.1:5432What to read out of it:
- who/last/lastlog are the audit trail: who's on now, who logged in recently, and when each account was last used. On a real server, lastlog showing a service account that suddenly did log in — when it should read Never logged in — is a red flag worth investigating. These read /var/log/wtmp and the lastlog database; they're the human-facing companion to auth.log.
- ss -tlnp (Module 13 — Networking) is a security tool as much as a network one: every listening port is attack surface. The discipline is to look at this list and ask "does this need to be reachable?" A database on 0.0.0.0:5432 (all interfaces) instead of 127.0.0.1:5432 (localhost only) is a classic accidental exposure — bind services to localhost unless they genuinely must be remote, and firewall the rest.
- Put together, Part D's exercises are a mini hardening audit: who can escalate (sudo -l, SUID), who has logged in (last/lastlog), and what's exposed (ss -tlnp). Run these on any server you inherit and you'll know its security posture in five minutes.
Part D — Interview questions
🎯 "What is the principle of least privilege, and how do you apply it on Linux?"
Every user, process, and service gets the minimum access needed and no more, so a compromise or mistake has the smallest possible blast radius. On Linux: users log in as themselves and sudo for specific tasks (never a shared root login) — scoped in /etc/sudoers via visudo, granting narrow commands rather than blanket ALL; services run as dedicated unprivileged accounts (www-data, postgres) with nologin shells; file permissions and ownership (Module 4 — Users, Groups, and Permissions) restrict data to those who need it; and unused services are disabled to shrink the surface.
The details that separate candidates: the concrete illustration — the same RCE bug is a contained nuisance as www-data and a total breach as root — plus naming visudo's validation and NOPASSWD as a risk. It shows you understand least privilege as damage limitation, not just "don't use root."
🎯 "What is SELinux (or AppArmor), and what are the SELinux modes?" — GeeksforGeeks Q64 ("What is SELinux and different SELinux modes?")
Both are Mandatory Access Control (MAC) systems: on top of normal user/group permissions (which are discretionary — the owner decides), MAC enforces a system-wide policy that even root can't casually override, confining each program to only the files, ports, and capabilities its profile allows. SELinux (Red Hat/Fedora world) has three modes: Enforcing (policy is applied and violations blocked), Permissive (violations are logged but allowed — for testing/tuning a policy), and Disabled (off). AppArmor (Ubuntu/Debian default) does the same job with path-based profiles that are enforce or complain (its permissive equivalent).
The details that separate candidates: framing MAC as a layer above traditional permissions (contains a process even if it's running as root), and knowing Permissive/complain mode is the practical tool for rolling out a policy safely — you run permissive, watch the logs for what would have been denied, fix the profile, then switch to enforcing.
🎯 "How would you tell whether a Linux server has been compromised?"
Work the audit trail and the exposure: check /var/log/auth.log for unexpected successful logins or a burst of failures followed by a success; last/lastlog for logins from odd IPs or service accounts that logged in; ss -tlnp for listening ports you didn't put there (a backdoor); find / -perm -4000 for unexpected SUID-root binaries; running processes (ps, Module 8 — Processes) for unknown or misnamed ones; and crontab/systemd timers for persistence. Compare against a known-good baseline where you have one.
The details that separate candidates: naming specific artifacts and why (new listening port = backdoor; new SUID = privesc persistence; service account with a login = misuse), and the instinct to isolate first (firewall it off / pull it from the load balancer) before investigating, so you don't tip off the attacker or lose the machine. Bonus: on a truly compromised box, local tools may be trojaned — verify from outside where possible.
Part E — Toolkit
E1. Production practice — symptoms and fixes
| Symptom | What is really happening | What to run | The fix / next step |
|---|---|---|---|
| Permission denied (publickey) on a login that used to work | sshd isn't accepting your key — usually permissions, wrong key, or the key isn't in the agent | ssh -v user@host (read the verbose auth steps); on the server ls -ld ~/.ssh and ls -l ~/.ssh/authorized_keys | chmod 700 ~/.ssh • chmod 600 authorized_keys (StrictModes); ssh-add the key; confirm the right key with ssh -i |
| Locked out of a remote box after PasswordAuthentication no | Passwords were disabled before a key actually worked | Use the still-open session (if you kept one) or the cloud/serial console | Re-enable password auth or fix authorized_keys; then re-do it in the correct order (test key first) |
| WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! | The server's host key differs from known_hosts — a rebuild, or a man-in-the-middle | Verify the new fingerprint out of band (cloud console / provisioning logs) | If legitimate: ssh-keygen -R hostname, reconnect, confirm. If unexplained: do not connect — investigate |
| UNPROTECTED PRIVATE KEY FILE! — ssh refuses the key | Your private key is readable by group/other | ls -l ~/.ssh/id_ed25519 | chmod 600 ~/.ssh/id_ed25519 — ssh won't use a key others can read |
| auth.log scrolling with Failed password ... from <ip> | Automated brute force against port 22 (constant on any public IP) | sudo grep 'Failed password' /var/log/auth.log | tail | Keys-only auth makes it harmless; add fail2ban to quiet it and a ufw allowlist to block it |
| A service is running as root for no reason | Unnecessary privilege — any bug in it becomes full compromise | ps -eo user,comm | grep <svc>; ss -tlnp | Run it as a dedicated unprivileged user; bind to localhost if it needn't be remote |
E2. Capstone — four realistic tickets
Try each before opening the answer. These are the SSH/security scenarios that come up in real on-call and in interviews.
🎫 Ticket 1 — "I hardened the new server, disabled password login, and now I can't SSH in at all — Permission denied (publickey). It's in production in an hour."
Diagnose, then recover, then prevent. First, don't panic-rebuild — you almost certainly still have a way in. Recovery paths, in order: (1) any SSH session you left open to that box is your lifeline — in it, re-enable passwords (sudo sed -i 's/^PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config && sudo sshd -t && sudo systemctl reload ssh) or fix the key, then reconnect. (2) No open session? Use the cloud provider's serial/VNC console (AWS EC2 Serial Console, GCP, etc.) — it bypasses SSH entirely and gives you a root prompt. (3) Worst case, detach the disk and mount it on another instance.
Root cause — it's one of these: the key was never actually in ~/.ssh/authorized_keys; ~/.ssh/authorized_keys have wrong permissions so StrictModes ignores them (chmod 700/600); you installed the private key instead of the public; or you edited the wrong user's authorized_keys. Confirm with ssh -v (client) and sudo journalctl -u ssh / /var/log/auth.log (server).
Prevent forever: the ordered ritual — install key → test key login in a new session → only then set PasswordAuthentication no → sshd -t → systemctl reload ssh (not restart) → open a third session to confirm before closing your safety session. The whole outage exists because auth was disabled on faith; the fix is to never do that.
🎫 Ticket 2 — "Security flagged our public server: /var/log/auth.log shows thousands of failed root logins from IPs all over the world. Are we hacked? Make it stop."
Are we hacked? Almost certainly not yet — this is the background brute-force noise every internet-facing SSH server sees, automated bots trying common user/password combos. Confirm no successful unexpected login: sudo grep -E 'Accepted|session opened' /var/log/auth.log and last — look for logins you can't account for. Failed attempts alone are attempts, not breaches.
Make it stop, in layers: (1) Kill the attack surface — PermitRootLogin no and PasswordAuthentication no in sshd_config (verify keys work first, Ticket 1's ritual): now those thousands of guesses are structurally impossible to succeed. (2) Quiet the noise — install fail2ban; it watches auth.log and firewalls any IP that racks up failures, so the log stops scrolling and load drops. (3) Shrink exposure — ufw to allow port 22 only from your office/VPN CIDRs (sudo ufw allow from 203.0.113.0/24 to any port 22), or ufw limit to rate-limit; ideally put SSH behind a bastion so the app servers aren't reachable at all.
Report back: "Not compromised — no successful unauthorized logins. This is standard automated scanning. I've disabled root and password auth (brute force can no longer succeed), added fail2ban to auto-ban offenders, and restricted port 22 to our VPN range. Defense in depth: even a lucky guess now hits a key-only wall behind a firewall."
🎫 Ticket 3 — "Provision a brand-new Ubuntu server securely before it goes live. Walk me through your checklist."
A prioritised, reasoned checklist — lead with the front door. SSH: create a personal user in sudo group; install your key (ssh-copy-id); test key login; then in sshd_config set PasswordAuthentication no, PermitRootLogin no, PubkeyAuthentication yes, an AllowUsers/AllowGroups allowlist, and a sane MaxAuthTries; sudo sshd -t then reload; confirm in a new session. Firewall (Module 13 — Networking): ufw default deny incoming / allow outgoing, then allow only the ports the box actually serves (22 from trusted ranges, 80/443 if it's a web host), ufw enable. Brute-force: install fail2ban. Patching (Module 9 — Package Management): apt update && apt upgrade, enable unattended-upgrades for security patches. Least privilege: no shared accounts; services as unprivileged users; disable/remove unused services (systemctl disable, Module 11 — systemd and Services); audit find / -perm -4000 and ss -tlnp. AppArmor enabled. Monitoring/audit: ship logs off-box, know where auth.log is.
What makes this a strong answer: the order and the reasoning — front door (SSH) first because it's the most-attacked, each control paired with the threat it addresses, and the recurring theme of defense in depth so no single layer is load-bearing. Naming the test-before-you-lock discipline shows you've actually done this and been burned.
🎫 Ticket 4 — "A developer says ssh prod-web fails with Permission denied (publickey), but it worked fine yesterday and nobody changed the server."
Bisect client vs server with ssh -v. Run ssh -v prod-web and read which keys it offers and how the server responds. Common causes, roughly in order: (1) the agent lost the key — a reboot or logout cleared ssh-agent; ssh-add -l shows nothing → ssh-add ~/.ssh/id_ed25519 and retry (very common "it worked yesterday"). (2) Permissions changed on ~/.ssh or authorized_keys (a backup restore, a chmod -R, a new home directory) → StrictModes now ignores the key; fix to 700/600. (3) The dev is hitting a different host/user than they think (a ~/.ssh/config change, DNS now pointing elsewhere, wrong default user) → ssh -v shows the resolved hostname/user; ssh -G prod-web shows the effective config. (4) The key or its authorized_keys entry was rotated/removed server-side by automation the dev doesn't know about.
The details that separate candidates: reaching for ssh -v immediately (it narrates the exact auth negotiation and usually pinpoints the cause in seconds), checking the agent first because "worked yesterday, no server change" screams client-side state loss, and knowing that "nobody changed the server" is frequently false — config management or a key-rotation job changed it without the dev's knowledge. Methodical bisection beats guessing.
E3. Documentation reference
| Task | Command / file | Reference |
|---|---|---|
| Connect / inspect effective client config | ssh user@host • ssh -G host • ssh -v (debug) | ssh(1) |
| Generate / inspect a key | ssh-keygen -t ed25519 • ssh-keygen -lf key.pub | ssh-keygen(1) |
| Install a public key on a server | ssh-copy-id user@host | ssh-copy-id(1) |
| Cache keys / forward (carefully) | ssh-agent • ssh-add -l • ssh -J bastion host | ssh-agent(1) · ssh-add(1) |
| Configure / validate the server daemon | /etc/ssh/sshd_config • sshd -t • sshd -T | sshd_config(5) · sshd(8) |
| Grant scoped root / edit sudoers safely | sudo -l • visudo • /etc/sudoers.d/ | sudoers(5) |
| Audit trail & attack surface | /var/log/auth.log • last • lastlog • ss -tlnp • find / -perm -4000 | Ubuntu Server — Security |
E4. Self-assessment
- Walk through the SSH handshake. Where is asymmetric crypto used, where is symmetric, and why the split?
- What is a host key, what is known_hosts, and what does "trust on first use" mean? What are the two possible causes of the "host identification changed" warning?
- Explain challenge-response key auth. Why is it more secure than a password even though the key file is static and never sent?
- What permissions must ~/.ssh and authorized_keys have, and what breaks if they're wrong? What is StrictModes?
- What does ssh-agent do, what is agent forwarding, and why is -A to an untrusted host dangerous? What's the safer alternative?
- Give the ordered steps to switch a server from password to key-only auth without locking yourself out.
- Why is changing the SSH port weak security, and what are the two changes that actually stop brute force?
- State the principle of least privilege and give a concrete example of the same bug being contained vs catastrophic depending on privilege.
- What are SELinux/AppArmor and their modes, and how do they differ from normal file permissions?
- You inherit a server — which five commands tell you its security posture (who can escalate, who's logged in, what's exposed)?
E5. Sources
ssh(1) · ssh-keygen(1) · ssh-copy-id(1) · ssh-agent(1) · ssh-add(1) · sshd(8) · sshd_config(5) · sudoers(5) · OpenSSH project · Ubuntu Server — Security
Interview questions researched from (published/updated within the last two years):
GeeksforGeeks — Linux Interview Questions (71) — Q49 "What is SSH?", Q50 SCP vs SFTP, Q64 SELinux modes, Q40 SUID/SGID/sticky, Q41 passwd vs shadow (updated Jul 2026). GeeksforGeeks — Linux for DevOps Interview Questions — Q16 "secure a Linux server", Q32 "SSH protocol / securely connect", Q21 "block an IP" (updated Sep 2025).
Verification note: all commands were run on Ubuntu 24.04 (OpenSSH 9.6p1) in a clean container; key generation, ssh-agent, sshd -t/-T, auth.log, sudo -l, and the SUID audit reflect real output. Where this container isn't internet-facing (no live brute-force in auth.log) or has no interactive sessions (who/last empty), the expected-result blocks say so and show the canonical production form. Next: Module 16 — How Containers Work — how the isolation you've been securing is actually built.