Linux Lesson 22 of 47

Server Hardening: CIS Benchmarks, SSH & Kernel Hardening, auditd, fail2ban & Attack-Surface Reduction

A fresh Linux install is not secure. It is convenient: it listens on ports you forgot about, ships packages you’ll never run, permits password logins from the entire internet, dumps core files that leak secrets, and trusts every process with the full rights of the user that started it. Hardening is the deliberate, layered work of turning that convenient default into a server an attacker cannot easily use — and doing it without locking yourself out, which is the single most common way hardening goes wrong.

This lesson is the capstone of the security track. It assumes you already know the pieces — users and permissions, SSH keys, firewalls, SELinux, PAM — and shows you how to assemble them into a coherent, defensible baseline you can apply, verify, and re-verify. Everything here is drawn from the two standards the industry actually uses: the CIS Benchmarks and the DISA STIGs. You will not memorise a thousand settings; you’ll learn the model they encode, apply the highest-value controls by hand so you understand them, and then let a scanner (OpenSCAP) check the rest.

⚠️ Read this first. Almost every step below can lock you out of a remote server if done in the wrong order. The golden rule, repeated throughout: keep a second SSH session open and a console/out-of-band path available while you harden. Never close your last working session until you’ve opened a fresh one and confirmed the new rules let you back in. On a cloud VM, know how to reach the serial console before you start.

Why this matters

Security people talk about “defense in depth” so often it sounds like a slogan, but it is a concrete, testable idea: no single control is trusted to be perfect, so you stack independent controls such that an attacker must defeat every one of them. The firewall might be misconfigured, so you also require SSH keys. SSH might have a zero-day, so you also confine the service with SELinux. SELinux might be in permissive mode by accident, so you also record everything with auditd. Each layer is cheap; the combination is expensive to beat.

The beginner mistake is to treat security as a binary — “is there a firewall? yes, done.” The professional treats it as a surface-area problem: every listening port, every installed package, every setuid binary, every account with a shell, every kernel module that can be auto-loaded is a piece of surface an attacker can push on. Hardening is, more than anything, making that surface as small as possible and then watching what’s left.

There are two reasons to care beyond “hackers bad.” First, compliance: if you handle payment data (PCI-DSS), health data (HIPAA), or government workloads (FedRAMP, DoD), you will be audited against a documented baseline, and “we set up a firewall” will not pass. Second, blast radius: breaches are not prevented so much as contained. A hardened host is one where a compromised web app can’t read the database password, can’t load a rootkit module, can’t disable the audit log, and leaves a trail even as it fails. That containment is what turns a catastrophe into an incident.

The mental model for the whole lesson is a set of concentric shells around your workload. Read it from the outside in, and internalise that an attacker has to get through all of them:

Defense in depth and the standards

Picture the server as an onion. The outermost layer is everything the internet can even see; the innermost is the running workload you actually care about. Each layer you add is one more independent thing that must fail before the workload is exposed.

Defense-in-depth diagram for Linux server hardening drawn as concentric layers read outside-in: an outer attack-surface-reduction layer (minimal install, mask unused services, audit with ss -tulpn) wraps a network layer (a default-deny firewall plus fail2ban banning brute-force), which wraps an authentication layer (key-only SSH, PAM faillock lockout, sudo least-privilege with no root login), which wraps a mandatory-access-control layer (SELinux and AppArmor in enforcing mode confining each service), which wraps the innermost kernel-and-filesystem layer (sysctl hardening with full ASLR and tcp_syncookies, nosuid/noexec mount options, and auditd recording who touched what) — all protecting the workload at the centre; six numbered badges mark reduce-surface-first, network default-deny, key-only SSH, enforcing MAC, sysctl-plus-auditd, and an ongoing patch cadence

Walk it once, outside → in. Reduce surface (badge 1): strip the box to a minimal install and mask services you don’t run, so there’s less to attack at all. Network (badge 2): a default-deny firewall answers on only the ports you allow, and fail2ban bans IPs that hammer them. Auth (badge 3): SSH is key-only with no root login; PAM/faillock locks accounts after repeated failures; sudo grants least privilege. MAC (badge 4): SELinux or AppArmor in enforcing mode confines each service so a compromise can’t wander. Kernel/FS (badge 5): sysctl flips safe kernel defaults, mount options neuter planted binaries, and auditd records everything. And running through all of it (badge 6): a patch cadence, because a hardened host still runs software with CVEs. Every layer below is one of these shells.

The two standards you should know

You do not invent a hardening baseline from scratch — you adopt one. Two dominate:

Standard Who publishes it What it is How you consume it
CIS Benchmarks Center for Internet Security (non-profit) Consensus-built, per-OS hardening guides (e.g. CIS Ubuntu 22.04 Benchmark) with numbered, justified recommendations Free PDF; automated as CIS-CAT (paid) or the open SCAP Security Guide cis profiles run by OpenSCAP
DISA STIG US Defense Information Systems Agency Security Technical Implementation Guides — mandatory config for DoD systems, stricter than CIS XCCDF/SCAP content, checked with OpenSCAP or the STIG Viewer; also shipped as SSG stig profiles
Vendor baselines Red Hat, Canonical, cloud providers Security guides and hardened images (e.g. RHEL “hardened” AMIs) Image + docs; often pre-apply a CIS or STIG profile
A “baseline” You The specific documented set of settings your org agrees is the minimum for every host Codified in your config management (Ansible/Terraform) so every host is identical and drift is detectable

The word baseline is the important one. A baseline is a written, versioned, enforceable definition of “secure enough here.” Its power is not any single setting but that it is uniform and checkable: every host gets the same config, and a scanner can tell you which hosts have drifted. Hardening one server by hand is a demo; a baseline applied by config management to a fleet is the job.

CIS Benchmarks are graded into profiles so you can pick your pain tolerance:

CIS profile Intent Typical impact Use when
Level 1 Practical, sensible security with minimal impact on function Low — safe on almost any server The default target for general-purpose servers
Level 2 Defense-in-depth for high-security environments Higher — may break some workflows (e.g. noexec /tmp, no core dumps) Regulated / high-value systems where you can test impact
Server vs Workstation Separate profiles per role Server profiles disable desktop things; workstation profiles keep them Match the profile to the machine’s actual job

Pragmatism matters more than zeal. Applying every Level 2 STIG control blindly to a developer box will generate a support queue and teach your team that “security” means “things break.” Apply Level 1 everywhere, apply Level 2 where you can afford to test the impact, and document the exceptions — a known, justified deviation is fine; an unknown one is a finding.

The hardening-domain checklist

Here is the whole lesson as one table: the domains of hardening, what each is for, the flagship action, and how you verify it. Everything after this is a deep-dive into one row.

# Domain Goal Flagship action Verify with
1 Attack-surface reduction Fewer things to attack Minimal install; mask/remove unused services & packages ss -tulpn, systemctl list-units --type=service
2 Kernel modules Block unneeded drivers/protocols Blacklist usb-storage, unused filesystems/protocols modprobe -n -v <mod>, lsmod
3 Accounts & auth Only real users, strong policy Password policy, faillock, disable unused accounts, no root login passwd -S, faillock, awk -F: '($3<1000)' /etc/passwd
4 sudo / privilege Least privilege Scoped sudoers, remove blanket ALL sudo -l, visudo -c
5 SSH Safe remote access Key-only, no root, AllowGroups, MaxAuthTries sshd -T, key login test
6 fail2ban Slow down brute-force sshd jail, ban repeat offenders fail2ban-client status sshd
7 Firewall Default-deny inbound Allow SSH + app only, drop the rest firewall-cmd --list-all / nft list ruleset
8 MAC Confine services SELinux/AppArmor enforcing getenforce, aa-status
9 Kernel / sysctl Safe kernel defaults /etc/sysctl.d/ hardening drop-in sysctl -a, sysctl <key>
10 Filesystem Neuter planted binaries Separate partitions + nodev,nosuid,noexec findmnt, find / -perm -4000
11 Auditing Tamper-evident trail auditd rules on sensitive files/commands auditctl -l, ausearch, aureport
12 Integrity Detect tampering AIDE file-integrity baseline aide --check
13 Compliance scan Prove & find gaps OpenSCAP against a CIS/STIG profile oscap xccdf eval → HTML report
14 Patching Close known CVEs unattended-upgrades / dnf-automatic, track EOL apt list --upgradable, dnf updateinfo

Attack-surface reduction

The highest-leverage, lowest-cost thing you can do is run less software. A service that isn’t installed has no CVEs, needs no patching, opens no ports, and can’t be misconfigured. This is why hardened builds start from a minimal install (Ubuntu Server minimized, RHEL “Minimal” install group) rather than a desktop image — no GUI, no compilers, no print server, no Bluetooth stack on a rack server.

Audit what is actually listening and running

Two commands tell you your real surface. First, what’s listening on the network:

# Every listening TCP/UDP socket, with the owning process (-p needs root)
sudo ss -tulpn
Netid State  Local Address:Port  Process
tcp   LISTEN 0.0.0.0:22          users:(("sshd",pid=812,fd=3))
tcp   LISTEN 127.0.0.1:5432      users:(("postgres",pid=999,fd=5))
tcp   LISTEN 0.0.0.0:25          users:(("master",pid=740,fd=13))   # Postfix — do you need it?
udp   LISTEN 0.0.0.0:68          users:(("dhclient",pid=601,fd=6))

Read every line as a question: does this host need to answer on this port, on this address? The Postfix listener on 0.0.0.0:25 is a classic — a mail transfer agent bound to all interfaces on a box that only needs to send local cron mail. Either bind it to 127.0.0.1 or remove it. The difference between 0.0.0.0 (all interfaces, reachable from the network) and 127.0.0.1 (loopback only, local) is one of the most important distinctions in this whole lesson.

Second, what’s running as a service, whether or not it listens:

# Running services; then everything installed but perhaps unneeded
systemctl list-units --type=service --state=running
systemctl list-unit-files --type=service --state=enabled
Command Shows Use it to
ss -tulpn Listening sockets + owning process Find network-exposed surface
ss -tulpn | grep 0.0.0.0 Only externally-bound listeners Prioritise what the world can reach
systemctl list-units --type=service --state=running Currently-running services See live surface
systemctl list-unit-files --state=enabled Services set to start at boot Catch things that’ll come back after reboot
systemctl --failed Failed units Spot broken/abandoned services
ps aux --sort=-%mem All processes Cross-check daemons with no unit

Disable vs stop vs mask

Turning a service off has three levels, and the difference is a common gotcha — a disabled service can be dragged back to life by another unit’s dependency; only mask truly forbids it.

Action Command Effect Survives reboot? Can be re-activated by a dependency?
Stop systemctl stop cups Kills it now No — starts again at boot Yes
Disable systemctl disable --now cups Won’t start at boot; stops now Yes Yes — a Wants=/Requires= can pull it up
Mask systemctl mask --now cups Symlinks the unit to /dev/null; cannot start by any means Yes No — fully forbidden
# Example: a rack server has no printers — kill CUPS for good
sudo systemctl disable --now cups.service cups.socket cups.path
sudo systemctl mask cups.service        # belt-and-braces: nothing can start it

For packages you’re sure you’ll never use, go further and remove them, so the code isn’t even on disk:

# Debian/Ubuntu                         # RHEL/Fedora/Rocky
sudo apt purge telnet rsh-client        sudo dnf remove telnet rsh
sudo apt autoremove --purge             sudo dnf autoremove

Prime candidates for removal on a server: telnet, rsh/rlogin (cleartext protocols), xinetd/inetd, avahi-daemon (mDNS), cups (printing), nfs-common if not used, ftp clients, and compilers/toolchains (gcc, make) on a production box where an attacker would otherwise use them to build exploits.

Kernel-module blacklisting

Even with a service removed, the kernel can auto-load a driver when hardware appears or a filesystem is mounted — a real surface (many CVEs live in obscure filesystem and protocol modules). Blacklisting stops that. Create a drop-in under /etc/modprobe.d/:

# /etc/modprobe.d/hardening-blacklist.conf
# The 'install ... /bin/false' line is what actually prevents loading;
# 'blacklist' alone only stops autoload-by-alias, not an explicit modprobe.
install usb-storage /bin/false   # block USB mass storage (data exfiltration)
install cramfs   /bin/false
install freevxfs /bin/false
install jffs2    /bin/false
install hfs      /bin/false
install hfsplus  /bin/false
install udf      /bin/false
install dccp     /bin/false      # rare network protocols with CVE history
install sctp     /bin/false
install rds      /bin/false
install tipc     /bin/false
blacklist usb-storage
# Verify a module is now inert — 'install /bin/false' means it won't load
modprobe -n -v usb-storage
# → install /bin/false
lsmod | grep usb_storage         # should return nothing after reboot/unload
Module What it is Why blacklist on a server
usb-storage USB mass-storage driver Blocks USB-drive data exfiltration / malware drop
cramfs,freevxfs,jffs2,hfs,hfsplus,udf Legacy/optional filesystems Rarely used on servers; mount-time attack surface
dccp,sctp,rds,tipc Uncommon network protocols CVE-prone, almost never needed
bluetooth,firewire-core Bluetooth / FireWire No place on a headless server

⚠️ Two blacklist traps. (1) Do not blacklist vfat — your EFI System Partition (/boot/efi) is FAT32 and the box won’t boot cleanly without it. (2) Do not blacklist overlay on a container host, or Docker/Podman storage breaks. Always cross-check what your workload needs before adding a module to this list, and reboot into it on a machine you can reach via console.

Account and authentication hardening

Reducing surface handles the software; now handle the humans and accounts. This section deliberately overlaps with the dedicated users/PAM material — see the deep-dive lesson on PAM, sudoers and password policy — but the hardening baseline pulls the key controls together.

Strong password policy and faillock

Two independent controls: complexity (make passwords hard to guess) via pam_pwquality, and lockout (stop online guessing) via pam_faillock. Both are PAM modules; the hardening-relevant knobs:

Setting File Meaning CIS-ish value
minlen /etc/security/pwquality.conf Minimum password length 14
minclass same Distinct character classes required (upper/lower/digit/other) 4
dcredit/ucredit/ocredit/lcredit same Credit per digit/upper/other/lower (negative = require that many) -1 each (or use minclass)
maxrepeat same Max identical consecutive chars 3
dictcheck same Reject dictionary words 1
deny /etc/security/faillock.conf Failed attempts before lockout 5
unlock_time same Seconds until auto-unlock (0/negative = admin-only unlock) 900
fail_interval same Window in which failures are counted 900
even_deny_root same Also lock the root account on failures often off — see warning
# Inspect / reset a locked account (faillock, the modern pam_tally2 replacement)
faillock                       # show all users' failure counts
faillock --user bob            # just bob
sudo faillock --user bob --reset   # clear bob's counter (unlock)

⚠️ Faillock can lock out your only admin. With deny=5 and a long unlock_time, a fat-fingered password a few times will lock the account — and if that’s your sole sudo user on a remote box with password auth, you’re stuck until the timer expires or you reach the console. Mitigations: keep unlock_time sane (e.g. 900s), set even_deny_root off unless you have console access, and always retain a second logged-in session. On RHEL 8+/9 these PAM stacks are managed by authselect — edit the profile, don’t hand-hack /etc/pam.d/system-auth, or the next authselect apply reverts you.

Root, sudo, umask, unused accounts, banners

Control What to do Command / file
No direct root login Disable root’s password/SSH; use sudo sudo passwd -l root; SSH PermitRootLogin no
sudo least-privilege Grant specific commands, not blanket ALL visudo / drop-ins in /etc/sudoers.d/
Log sudo Ensure every sudo is logged default in /var/log/auth.log / journal; add Defaults logfile
Restrictive umask New files not world-readable by default umask 027 in /etc/profile.d/, /etc/login.defs UMASK 027
Disable unused accounts Lock and shell-nologin service accounts usermod -L -s /usr/sbin/nologin app
Find no-password accounts Ensure no empty-password logins awk -F: '($2==""){print $1}' /etc/shadow
Audit UID 0 accounts Only root should have UID 0 awk -F: '($3==0){print $1}' /etc/passwd
Login banners Legal warning, no OS/version leak /etc/issue, /etc/issue.net, /etc/motd

A scoped sudoers entry is the whole idea of least privilege — this user can restart one service and nothing else:

# /etc/sudoers.d/webops   (validate with: visudo -cf /etc/sudoers.d/webops)
%webops ALL=(root) /usr/bin/systemctl restart nginx, /usr/bin/systemctl reload nginx

Login banners serve two masters: a legal deterrent (many jurisdictions require an “authorized use only” notice for prosecution) and information hygiene (the default /etc/issue on some distros prints the OS and kernel version — free reconnaissance for an attacker):

File Shown Notes
/etc/issue Before local console login Remove \r \m \s \v escapes so it doesn’t leak OS/kernel version
/etc/issue.net Before remote login SSH shows it only if Banner /etc/issue.net is set in sshd_config
/etc/motd After successful login “Message of the day”; keep it free of sensitive info
# A compliant warning banner (no version info)
printf '%s\n' 'Authorized access only. All activity is monitored and logged.' \
  | sudo tee /etc/issue /etc/issue.net

SSH hardening recap and fail2ban

SSH is the front door of almost every Linux server, so it gets its own full lesson on OpenSSH keys, config and hardening. Here is the hardening baseline in one table, plus the tool that turns the failed-login noise into blocked IPs.

The SSH hardening baseline

Put these in a drop-in — /etc/ssh/sshd_config.d/10-hardening.conf on modern OpenSSH — rather than editing the main file, so distro upgrades don’t clobber you:

Directive Value Why
PermitRootLogin no Force named accounts + sudo; kills root brute-force
PasswordAuthentication no Keys only — passwords can be guessed
PubkeyAuthentication yes Enable key auth
KbdInteractiveAuthentication no Close the keyboard-interactive (PAM password) path too
PermitEmptyPasswords no Never allow blank passwords
AllowGroups sshusers Whitelist: only members of this group may SSH in
MaxAuthTries 3 Drop the connection after 3 bad attempts
LoginGraceTime 30 Close unauthenticated connections quickly
X11Forwarding no Not needed on a server
AllowTcpForwarding no Disable if you don’t tunnel through this host
ClientAliveInterval / ClientAliveCountMax 300 / 2 Reap dead sessions
Banner /etc/issue.net Show the legal banner pre-auth
# ALWAYS test the config before reloading — a syntax error can lock you out
sudo sshd -t && echo "config OK"
# Show the *effective* config sshd will actually use (resolves all drop-ins)
sudo sshd -T | grep -Ei 'permitrootlogin|passwordauth|allowgroups|maxauthtries'
sudo systemctl reload ssh    # Debian: 'ssh'   RHEL: 'sshd'

⚠️ The classic SSH lockout. Setting PasswordAuthentication no or AllowGroups sshusers while your key/group membership isn’t actually working will lock you out the instant you reload. The safe ritual, every time: (1) add your key and confirm a key-based login works in a second terminal; (2) if using AllowGroups, run id to confirm you’re in that group; (3) sshd -t; (4) reload; (5) open a brand-new session and log in — only when that succeeds do you close the old one. Never reload sshd with only one session open.

fail2ban: banning the brute-force

Even with key-only SSH, the internet will pound your port 22 with password attempts, filling logs and wasting cycles. fail2ban tails the auth log (via journald on systemd hosts), and after N failures from an IP within a window, inserts a firewall rule to ban it for a while. Config lives in /etc/fail2ban/jail.localnever edit jail.conf, which is replaced on upgrade.

# /etc/fail2ban/jail.local
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
# ⚠️ Whitelist your own admin network so you can't ban yourself:
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24
backend  = systemd          # read journald, not a log file path

[sshd]
enabled  = true
maxretry = 3
bantime  = 1d               # repeat SSH offenders get a longer ban
Parameter Meaning Typical value Notes
enabled Turn the jail on true Per-jail; [sshd] is the one that matters most
maxretry Failures before a ban 35 Lower = stricter
findtime Window those failures must occur in 10m maxretry within findtime triggers the ban
bantime How long the ban lasts 1h1d -1 = permanent ban
ignoreip Never-ban list your admin CIDR Set this to avoid self-lockout
backend Where to read logs systemd Or logpath = /var/log/auth.log on non-systemd
banaction How the ban is enforced nftables-multiport Also iptables-multiport, firewalld
bantime.increment Escalate repeat offenders true Each re-offense multiplies bantime
sudo systemctl enable --now fail2ban
sudo fail2ban-client status              # list active jails
sudo fail2ban-client status sshd         # banned IPs, counters for the sshd jail
sudo fail2ban-client set sshd unbanip 203.0.113.9   # release an IP (e.g. yourself)
Status for the jail: sshd
|- Filter
|  |- Currently failed: 2
|  `- Total failed:     417
`- Actions
   |- Currently banned: 6
   `- Banned IP list:   45.9.148.x 193.32.162.x ...

⚠️ fail2ban can ban you. A flaky VPN, a mistyped password a few times, or CGNAT sharing your IP with an attacker will get your address banned. Always set ignoreip to your admin subnet, know the unbanip command, and remember bans are enforced by the firewall — if you’re locked out, the console or a whitelisted jump host is your way back in.

Firewall default-deny and MAC enforcing

Two more shells, each with its own full lesson — this section is the hardening view of them.

Default-deny firewall

The principle is one sentence: deny everything inbound, then explicitly allow the handful of ports the host needs (usually SSH and the application). This is covered in depth in the firewalls lesson (firewalld, nftables, iptables); the hardening baseline:

Task firewalld (RHEL) nftables/ufw (Debian)
Default policy zones default to drop unlisted ufw default deny incoming
Allow SSH first firewall-cmd --permanent --add-service=ssh ufw allow 22/tcp
Allow the app --permanent --add-service=https ufw allow 443/tcp
Apply firewall-cmd --reload ufw enable
Verify firewall-cmd --list-all ufw status verbose / nft list ruleset

⚠️ Allow SSH before you enable a default-deny policy, or the very command that turns on the firewall drops your session. On ufw enable you’ll even get a prompt warning that the operation may disrupt existing connections — heed it. Enable the SSH allow rule, verify it’s present, then switch the policy.

SELinux / AppArmor in enforcing mode

DAC (the rwx bits) trusts each process with the user’s rights. MAC — Mandatory Access Control — adds a second, independent policy the kernel enforces regardless of file permissions, confining each service to only what it legitimately needs. Full treatment in the SELinux/AppArmor lesson. For hardening, one rule: it must be in enforcing mode. A MAC framework in permissive/complain mode logs violations but enforces nothing — a false sense of security.

Framework Distros Check mode Set enforcing Diagnose a denial
SELinux RHEL, Fedora, Rocky, CentOS getenforce (want Enforcing) setenforce 1; persist in /etc/selinux/config SELINUX=enforcing ausearch -m avc -ts recent, sealert
AppArmor Ubuntu, Debian, SUSE aa-status aa-enforce /etc/apparmor.d/<profile> journalctl -k | grep apparmor

⚠️ Don’t flip SELinux to enforcing blind on a box that’s been running permissive. Mislabeled files accumulate while it’s off; switching straight to enforcing can break services or, worst case, login. The safe path: fix the labels first (restorecon -R / or, if the box has been permissive a long time, touch /.autorelabel && reboot to relabel on boot), confirm services start, then set enforcing. Never edit /etc/selinux/config to disabled as a “fix” — you lose all confinement and a re-enable later requires a full relabel.

Kernel and sysctl hardening

The kernel exposes hundreds of tunables through sysctl; a dozen of them meaningfully harden the network stack and memory model. Set them as a drop-in under /etc/sysctl.d/ (not by editing /etc/sysctl.conf, which distros may own), so they’re versioned and survive upgrades:

# /etc/sysctl.d/60-hardening.conf   — apply with: sudo sysctl --system
# --- Network: this host is a server, not a router ---
net.ipv4.ip_forward = 0                    # don't forward packets (skip if it IS a router/NAT/k8s node)
net.ipv4.conf.all.rp_filter = 1            # reverse-path filter: drop spoofed source IPs
net.ipv4.conf.default.rp_filter = 1
net.ipv4.tcp_syncookies = 1                # survive SYN-flood DoS
net.ipv4.conf.all.accept_redirects = 0     # ignore ICMP redirects (MITM vector)
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.all.send_redirects = 0       # a server shouldn't send redirects either
net.ipv4.conf.all.accept_source_route = 0  # reject source-routed packets
net.ipv4.conf.all.log_martians = 1         # log impossible/spoofed addresses
net.ipv4.icmp_echo_ignore_broadcasts = 1   # don't answer broadcast pings (smurf)
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_ra = 0            # ignore router advertisements on a static server
# --- Memory / kernel exploit mitigation ---
kernel.randomize_va_space = 2              # full ASLR (stack, heap, mmap, brk)
kernel.dmesg_restrict = 1                  # hide the kernel ring buffer from non-root
kernel.kptr_restrict = 2                   # hide kernel pointers (defeats some exploits)
kernel.yama.ptrace_scope = 1               # restrict ptrace: no attaching to arbitrary procs
fs.suid_dumpable = 0                        # setuid programs never write core dumps (leak-proof)
fs.protected_hardlinks = 1                  # block hardlink-based TOCTOU attacks
fs.protected_symlinks = 1
sudo sysctl --system                    # load every /etc/sysctl.d/*.conf now
sysctl net.ipv4.ip_forward              # verify one setting took effect
# → net.ipv4.ip_forward = 0
sysctl key Value Protects against ⚠️ Caveat
net.ipv4.ip_forward 0 Turning your host into an unwitting router Leave 1 on real routers, NAT gateways, Docker/k8s nodes
net.ipv4.conf.all.rp_filter 1 IP source-address spoofing Can drop traffic in asymmetric-routing/multihomed setups
net.ipv4.tcp_syncookies 1 SYN-flood denial of service Safe on virtually all servers
net.ipv4.conf.all.accept_redirects 0 ICMP-redirect man-in-the-middle
net.ipv4.conf.all.accept_source_route 0 Source-routing bypass of firewalls
net.ipv4.conf.all.send_redirects 0 Leaking routing info Only matters if multihomed
kernel.randomize_va_space 2 Predictable memory layout → reliable exploits Full ASLR; standard on modern distros
kernel.dmesg_restrict 1 Kernel info leak via dmesg Non-root can’t read the ring buffer
kernel.kptr_restrict 2 Kernel-pointer leaks aiding exploits May hide info some monitoring wants
kernel.yama.ptrace_scope 1 One process debugging/reading another’s memory May break some debuggers/gdb -p
fs.suid_dumpable 0 Core dumps of setuid programs leaking secrets

Two of these are hardware/compiler mitigations the kernel coordinates rather than pure sysctls:

Mitigation What it does How it’s on
ASLR (Address Space Layout Randomization) Randomizes stack/heap/library addresses so exploits can’t hardcode targets kernel.randomize_va_space=2 (full)
NX / DEP (No-eXecute) Marks data pages non-executable so injected shellcode won’t run CPU NX bit + kernel; verify grep -o nx /proc/cpuinfo | head -1
Stack canaries / FORTIFY Compiler-inserted guards detect stack smashing Built into distro binaries

⚠️ ip_forward=0 breaks routers and container hosts. If this host does NAT, is a VPN gateway, or runs Docker/Kubernetes/libvirt, it must forward packets — setting ip_forward=0 will silently break networking for everything behind it. Know your host’s role before you copy a hardening file. Similarly, rp_filter=1 can drop legitimate traffic on multihomed/asymmetric-routing hosts; test it.

Filesystem hardening

An attacker who lands a shell wants to write a binary somewhere and run it, and escalate via a setuid program. Filesystem hardening removes both opportunities.

Separate partitions and mount options

Putting /tmp, /var, /var/log, /home, and /dev/shm on separate mounts does two things: it stops a full /var/log from taking down the whole system, and — crucially — it lets you apply restrictive mount options to each:

Option Meaning Effect
nodev Ignore device files on this mount A planted /tmp/hda device node can’t be used to read raw disk
nosuid Ignore setuid/setgid bits here A dropped setuid-root binary in /tmp gains no privilege
noexec Forbid executing binaries from this mount Malware written to /tmp simply won’t run

The standard hardening matrix (CIS-aligned) for the mount options each area should carry:

Mount nodev nosuid noexec Rationale
/tmp World-writable — the #1 malware drop zone
/var/tmp Same, persists across reboot
/dev/shm Shared memory; a common fileless-exec spot
/home Users may legitimately run scripts; noexec breaks that
/var Package post-scripts may exec here
/boot Only read at boot
# /etc/fstab — a hardened /tmp (or use the systemd tmp.mount unit)
tmpfs   /tmp        tmpfs   defaults,rw,nodev,nosuid,noexec,size=2G   0 0
tmpfs   /dev/shm    tmpfs   defaults,nodev,nosuid,noexec              0 0
# Verify what's actually mounted with which options
findmnt /tmp
# → TARGET SOURCE FSTYPE OPTIONS
#   /tmp   tmpfs  tmpfs  rw,nosuid,nodev,noexec,size=2097152k

⚠️ noexec on /tmp breaks some installers. Certain package managers, Java tooling, and build systems extract and execute helpers from /tmp (or /var/tmp). If a dnf/pip/.deb install suddenly fails with “permission denied” on a script it wrote to /tmp, noexec is the cause. Options: temporarily mount -o remount,exec /tmp during the install, or point the tool at a different tmpdir (TMPDIR=/var/lib/app-tmp). Add mount options on a box you can reach — a bad /etc/fstab entry can stop it booting.

Finding SUID/SGID, world-writable and sticky-bit issues

Three sweeps every hardened host should pass. Use -xdev so find stays on one filesystem and doesn’t wander into /proc, /sys, or network mounts:

# 1. All setuid and setgid files — audit this list; each is a privilege boundary
sudo find / -xdev \( -perm -4000 -o -perm -2000 \) -type f -printf '%m %u %p\n' 2>/dev/null

# 2. World-writable FILES — almost always a mistake; anyone can modify them
sudo find / -xdev -type f -perm -0002 -printf '%m %u %p\n' 2>/dev/null

# 3. World-writable DIRECTORIES missing the sticky bit — anyone can delete others' files
sudo find / -xdev -type d -perm -0002 ! -perm -1000 -printf '%m %u %p\n' 2>/dev/null
Finding Command Why it’s dangerous Fix
Unexpected setuid binary find / -xdev -perm -4000 Runs as its owner (often root) — a bug = instant root Remove the bit if not needed: chmod u-s <file>
setgid binary/dir find / -xdev -perm -2000 Runs as / inherits the group chmod g-s <file> if unjustified
World-writable file find / -xdev -type f -perm -0002 Anyone can alter it (config, script) chmod o-w <file>
World-writable dir, no sticky find / -xdev -type d -perm -0002 ! -perm -1000 Anyone can delete/rename others’ files chmod +t <dir> (like /tmp)
Unowned file (no user/group) find / -xdev -nouser -o -nogroup Leftover from a deleted user; may get reassigned chown to a real owner or remove

The sticky bit (+t, the t in drwxrwxrwt on /tmp) is the fix for the shared-directory problem: in a sticky world-writable directory, you can only delete files you own, even though everyone can write. That’s why /tmp is safe to share.

auditd — the tamper-evident trail

Everything so far prevents; auditd records. The Linux Audit daemon writes a kernel-level log of security-relevant events — file access, syscalls, logins, command execution — that is much harder to tamper with than application logs, and can be made immutable until reboot. When (not if) something gets past the outer layers, the audit trail is how you know what happened. This is the on-host half of the compliance lesson (auditd, OpenSCAP, STIG).

Rules live in /etc/audit/rules.d/*.rules (compiled into /etc/audit/audit.rules by augenrules), or are loaded live with auditctl. A hardening rule set watches the files and commands that matter:

# /etc/audit/rules.d/hardening.rules   (load: sudo augenrules --load)
## Watch the identity & auth files — -p wa = writes + attribute changes, -k = search key
-w /etc/passwd    -p wa -k identity
-w /etc/shadow    -p wa -k identity
-w /etc/group     -p wa -k identity
-w /etc/sudoers   -p wa -k scope
-w /etc/sudoers.d/ -p wa -k scope
-w /etc/ssh/sshd_config -p wa -k sshd
## Watch privilege escalation and logins
-w /var/log/lastlog -p wa -k logins
-w /var/run/faillock -p wa -k logins
## Record every command run as root (heavy — but gold during an incident)
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -F auid!=-1 -k rootcmd
## Watch kernel module load/unload (rootkit vector)
-a always,exit -F arch=b64 -S init_module -S finit_module -S delete_module -k modules
## Make the config immutable until reboot — MUST be the LAST line
-e 2
Rule syntax Meaning
-w <path> -p wa -k key Watch a file: read/write/execute/attribute; tag events with key
-a always,exit -F arch=b64 -S execve Syscall rule: audit every execve on 64-bit
-F euid=0 / -F auid>=1000 Field filters: effective UID root; real (login) UID a human
-k <key> Search key so ausearch -k key finds these events
-e 2 Immutable: no rule changes until reboot (tamper resistance)
-e 1 / -e 0 Enabled / disabled (mutable)

Analyse the trail with three tools:

Tool Purpose Example
auditctl Load/list live rules auditctl -l, auditctl -s (status)
ausearch Query the log by key/time/type/user ausearch -k identity -ts today
aureport Summarised reports aureport --auth, aureport -x (executables)
sudo auditctl -l                          # list active rules
sudo ausearch -k identity -ts recent      # who touched /etc/passwd or /etc/shadow lately
sudo aureport --auth --summary            # failed/successful auth attempt summary

⚠️ -e 2 locks the rules until reboot. Immutable mode is the point — an attacker can’t quietly disable auditing — but it also means you can’t edit rules without a reboot. Get the rule set right first, load and test it with -e 1, and only add -e 2 (as the final line) once you’re happy. Also: high-volume rules like “audit every execve” can generate huge logs and I/O — size /var/log/audit (a separate partition is ideal), tune /etc/audit/auditd.conf (max_log_file, space_left_action), and scope the rule with field filters (-F auid>=1000) so you’re not logging every system daemon’s activity.

Integrity and compliance scanning

Two automated backstops: one detects tampering after the fact (AIDE), one measures you against the whole benchmark at once (OpenSCAP).

AIDE — file-integrity baseline

AIDE (Advanced Intrusion Detection Environment) records a cryptographic fingerprint (hashes, size, perms, inode, mtime) of every important file into a baseline database, then later re-scans and reports what changed. If /usr/bin/sshd or /etc/passwd changed and you didn’t do it, that’s an incident.

sudo apt install aide     # Debian    |    sudo dnf install aide    # RHEL
sudo aideinit             # (Debian helper) or:  sudo aide --init
# Promote the new DB to the active baseline:
sudo mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
# ...later, on a schedule, check for changes:
sudo aide --check
Step Command Note
Configure what to watch /etc/aide/aide.conf Rules per path: which attributes to track
Build the baseline aide --init Do this on a known-good system, ideally right after install
Activate baseline mv aide.db.new.gz aide.db.gz The .new DB becomes the reference
Check aide --check Reports added/removed/changed files
Update baseline after legit changes aide --update Re-baseline after a patch window, or you’ll drown in noise

The subtle risk: if an attacker gets root, they can rewrite the AIDE database too. Best practice is to store the baseline DB (and the AIDE binary) offline or read-only — copy aide.db.gz to a separate host and diff there. AIDE catches the opportunistic attacker, not necessarily the one who owns the box.

OpenSCAP — automated CIS/STIG scanning

Hand-applying controls teaches you the model; OpenSCAP checks the other several hundred you didn’t. It evaluates the host against a machine-readable profile (from the SCAP Security Guide, package scap-security-guide/ssg-*) and produces a pass/fail report — the automated form of “are we CIS-compliant?” This is your bridge to the compliance lesson, which goes deep on SCAP.

sudo apt install libopenscap8 ssg-debderived   # Debian family
sudo dnf install openscap-scanner scap-security-guide   # RHEL family

# List the profiles bundled for this OS (cis, stig, pci-dss, ...)
oscap info /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml

# Scan against the CIS Level-1 Server profile → HTML report + machine results
sudo oscap xccdf eval \
  --profile xccdf_org.ssgproject.content_profile_cis_server_l1 \
  --results scan-results.xml --report scan-report.html \
  /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml
Command Purpose
oscap info <datastream> List available profiles and metadata
oscap xccdf eval --profile <id> ... Run the scan; --report x.html for a human report
oscap xccdf generate fix --profile <id> ... Generate a remediation script (bash/Ansible) — review before running
oscap xccdf eval --remediate ... Scan and auto-fix failing rules — ⚠️ test on a throwaway host first

The workflow is a loop: scan → read the report → remediate → re-scan. Never blindly --remediate a production box: an auto-fix that, say, sets noexec on /tmp or tightens PAM can break a workload or lock you out. Generate the fix, read it, apply changes through your config management, and re-scan to confirm the score climbed. Aim to codify passing controls in Ansible so new hosts are born compliant.

Patching and living with EOL

Every layer above is defeated by one unpatched CVE in a service you legitimately run. Patch cadence is a security control, not maintenance overhead.

# See what's outstanding
apt list --upgradable                 # Debian/Ubuntu
dnf updateinfo list security          # RHEL — just the security-flagged updates

# Apply security updates
sudo apt update && sudo apt upgrade                    # Debian
sudo dnf upgrade --security                            # RHEL: security-only

Automate the security updates so a forgotten box isn’t a soft target:

Distro Package Config Enable
Debian/Ubuntu unattended-upgrades /etc/apt/apt.conf.d/50unattended-upgrades (which origins), 20auto-upgrades (how often) sudo dpkg-reconfigure -plow unattended-upgrades
RHEL/Fedora/Rocky dnf-automatic /etc/dnf/automatic.conf — set apply_updates = yes, upgrade_type = security sudo systemctl enable --now dnf-automatic.timer
# /etc/dnf/automatic.conf (excerpt)
[commands]
upgrade_type = security     # only security errata, not every update
apply_updates = yes         # actually install, don't just download

⚠️ Automatic updates can restart or reboot into a broken service. Unattended upgrades that pull a new kernel or restart a daemon can cause an unplanned outage. Tune the reboot window (Unattended-Upgrade::Automatic-Reboot-Time "03:00"), exclude packages that need a controlled restart (a database, your app), and stage updates in a test tier first. Automation is right for security patches on stateless tiers; stateful services want a human in the loop.

And the hardest control of all — don’t run end-of-life software:

Reality Risk What to do
Distro past EOL (e.g. CentOS 7, EOL 2024) No more security patches — new CVEs stay open forever Plan the upgrade before EOL; it’s a project, not a dnf upgrade
App needs an ancient runtime You’re pinned to vulnerable libs Containerise/isolate it, restrict its network, front it with a WAF
Paid extended support (ELS/ESM) Buys time, not a cure Use as a bridge while you migrate, not a destination

Running EOL software is sometimes unavoidable, but it must be a known, compensated risk: network-isolate it, monitor it hardest, and have the migration on the roadmap. An unpatched EOL box exposed to the internet is the most common root cause in breach post-mortems.

Hands-on lab

Harden a throwaway Ubuntu or RHEL-family VM (or a --privileged container for the non-kernel parts). Do this on a machine you can reach via console/snapshot — you’re going to touch SSH and the firewall. Take a snapshot first.

Step 1 — Baseline the surface.

sudo ss -tulpn
systemctl list-units --type=service --state=running

What just happened: you now have the “before” picture — every listening port and running service. Note anything on 0.0.0.0 you don’t recognise.

Step 2 — Reduce surface. Pick a service you don’t need (e.g. cups) and mask it; remove a cleartext client.

sudo systemctl disable --now cups.service 2>/dev/null; sudo systemctl mask cups.service
sudo apt purge -y telnet 2>/dev/null || sudo dnf remove -y telnet 2>/dev/null

What just happened: CUPS can no longer start by any path, and the telnet client is gone from disk.

Step 3 — Blacklist a module. Block USB storage and verify it’s inert.

echo 'install usb-storage /bin/false' | sudo tee /etc/modprobe.d/hardening.conf
modprobe -n -v usb-storage        # → install /bin/false

What just happened: the kernel will refuse to auto-load USB mass storage.

Step 4 — sysctl hardening. Drop in the network/memory settings and load them.

sudo tee /etc/sysctl.d/60-hardening.conf >/dev/null <<'EOF'
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
kernel.randomize_va_space = 2
kernel.dmesg_restrict = 1
fs.suid_dumpable = 0
EOF
sudo sysctl --system
sysctl kernel.randomize_va_space   # → 2

What just happened: full ASLR, SYN-cookie protection, and no setuid core dumps — verified live.

Step 5 — Harden /tmp. Remount /tmp noexec (revert after the lab if it interferes).

sudo mount -o remount,nodev,nosuid,noexec /tmp 2>/dev/null || \
  { sudo mount -t tmpfs -o nodev,nosuid,noexec tmpfs /tmp; }
printf '#!/bin/sh\necho hi\n' > /tmp/t.sh; chmod +x /tmp/t.sh; /tmp/t.sh

What just happened: the last command fails with “Permission denied” — malware dropped in /tmp can’t execute.

Step 6 — SSH baseline (carefully). Add your key first, confirm it works in a second terminal, then harden.

sudo tee /etc/ssh/sshd_config.d/10-hardening.conf >/dev/null <<'EOF'
PermitRootLogin no
PasswordAuthentication no
MaxAuthTries 3
EOF
sudo sshd -t && sudo systemctl reload ssh   # 'sshd' on RHEL

What just happened: config validated then reloaded. Now open a brand-new SSH session to confirm you can still get in before closing this one.

Step 7 — fail2ban. Install, whitelist yourself, enable the sshd jail.

sudo apt install -y fail2ban || sudo dnf install -y fail2ban
sudo tee /etc/fail2ban/jail.local >/dev/null <<'EOF'
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1
bantime = 1h
[sshd]
enabled = true
maxretry = 3
EOF
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

What just happened: repeated SSH failures from any non-whitelisted IP now earn an hour-long firewall ban.

Step 8 — auditd. Watch the identity files and prove it fires.

sudo apt install -y auditd || sudo dnf install -y audit
echo '-w /etc/passwd -p wa -k identity' | sudo tee /etc/audit/rules.d/lab.rules
sudo augenrules --load
sudo useradd labtest                       # triggers a write to /etc/passwd
sudo ausearch -k identity -ts recent | tail

What just happened: the audit log shows exactly which process and user modified /etc/passwd.

Step 9 — Scan. If OpenSCAP is available, measure yourself.

sudo apt install -y libopenscap8 ssg-debderived || sudo dnf install -y openscap-scanner scap-security-guide
DS=$(ls /usr/share/xml/scap/ssg/content/ssg-*-ds.xml | head -1)
oscap info "$DS" | grep -i profile | head

What just happened: you can now run a full oscap xccdf eval against a CIS profile and get an HTML scorecard.

Cleanup: restore your snapshot, or userdel labtest, remove the drop-ins, and remount /tmp without noexec if the lab machine needs it.

Common mistakes and troubleshooting

Symptom Likely cause Fix
Locked out of SSH after hardening PasswordAuthentication no with no working key, or AllowGroups you’re not in Use console/serial; re-add key or fix group; test with sshd -T before reloading next time
Firewall enable dropped my session Turned on default-deny before allowing SSH Console in; add the SSH allow rule, verify, then enable policy
fail2ban banned my own IP No ignoreip, or flaky VPN/CGNAT fail2ban-client set sshd unbanip <ip>; add your CIDR to ignoreip
Account locked, can’t log in pam_faillock deny reached Console: faillock --user <u> --reset; sane unlock_time
Service won’t start after enforcing SELinux Mislabeled files / missing boolean ausearch -m avc -ts recent; restorecon -Rv <path>; setsebool as needed
dnf/pip install fails “permission denied” in /tmp noexec on /tmp mount -o remount,exec /tmp for the install, or set TMPDIR elsewhere
Networking broke on a Docker/router host net.ipv4.ip_forward = 0 Remove that line on forwarding hosts; sysctl --system
Can’t edit audit rules auditd in immutable mode (-e 2) Reboot to make rules mutable; then adjust
Box won’t boot after fstab edit Bad mount option / typo in /etc/fstab Boot rescue/single-user, fix /etc/fstab, remount
Blacklisting broke boot/containers Blacklisted vfat (EFI) or overlay (Docker) Remove those lines from /etc/modprobe.d/, rebuild initramfs if needed

The three that bite hardest, in prose:

Lockout is the number-one hardening incident. It is almost always self-inflicted and almost always from changing SSH, the firewall, PAM, or AllowGroups with only one session open. Adopt the ritual permanently: snapshot → change in a drop-in → sshd -t / visudo -c / firewall-cmd --list-all to validate → apply → open a new session and confirm → only then close the old one. Know your cloud provider’s serial-console path before you need it; on bare metal, keep IPMI/iLO/iDRAC access. A hardened box you can’t log into is not secure — it’s bricked.

Enforcing SELinux on a long-permissive box. Labels drift while SELinux is off. Flip straight to enforcing and services fail with cryptic AVC denials, or worse. Relabel first (touch /.autorelabel && reboot), watch ausearch -m avc, fix booleans/contexts, then enforce. And never “fix” a denial by setting SELinux to disabled — that throws away the entire MAC layer and requires a full relabel to re-enable safely.

noexec/blacklist breaking legitimate work. Level-2 controls like noexec on /tmp or aggressive module blacklists are correct and disruptive. A package manager that execs from /tmp, a container runtime that needs overlay, an EFI system that needs vfat — each turns a “hardening win” into an outage. The discipline: know your host’s role, apply Level 2 selectively, test on a staging host, and document every deviation. Security that breaks the workload gets ripped out by the next on-call engineer, which is worse than not hardening at all.

Cheat-sheet

Task Command
Listening sockets + process ss -tulpn
Running / enabled services systemctl list-units --type=service --state=running · list-unit-files --state=enabled
Kill a service for good systemctl mask --now <svc>
Remove a package apt purge <pkg> · dnf remove <pkg>
Test if a module can load modprobe -n -v <mod>
Apply all sysctl drop-ins sysctl --system · check: sysctl <key>
Find setuid/setgid files find / -xdev \( -perm -4000 -o -perm -2000 \) -type f
World-writable files find / -xdev -type f -perm -0002
Verify a mount’s options findmnt /tmp
Test sshd config sshd -t · show effective: sshd -T
Reload SSH systemctl reload ssh (Debian) / sshd (RHEL)
fail2ban jail status fail2ban-client status sshd
Unban an IP fail2ban-client set sshd unbanip <ip>
SELinux mode getenforce · set: setenforce 1
AppArmor status aa-status
Reset a locked account faillock --user <u> --reset
List / load audit rules auditctl -l · augenrules --load
Search / report audit log ausearch -k <key> -ts recent · aureport --auth
AIDE check aide --check
SCAP scan oscap xccdf eval --profile <id> --report r.html <datastream>
List SCAP profiles oscap info <ds.xml>
Pending security updates apt list --upgradable · dnf updateinfo list security

Interview and exam questions

Q: What is “defense in depth” and why does it matter? A: Layering multiple independent security controls so that no single failure exposes the system — an attacker must defeat every layer (surface reduction, network, auth, MAC, kernel/fs, monitoring). It matters because every individual control can fail or be bypassed; the combination provides containment even when one control is misconfigured or has a zero-day.

Q: Difference between systemctl disable and systemctl mask? A: disable stops a unit from starting at boot but another unit’s dependency (Wants=/Requires=) can still pull it up. mask symlinks the unit to /dev/null, so it cannot be started by any means until unmasked. Mask when you want a service forbidden, not merely off.

Q: You set PasswordAuthentication no and got locked out of a remote server. What went wrong and how do you avoid it? A: Key-based auth wasn’t actually working (missing/mispermissioned key, wrong user), so with passwords disabled there was no way in. Avoid it by confirming a key login works in a second session before reloading sshd, running sshd -t to validate config, and keeping console/serial access as a fallback.

Q: What does kernel.randomize_va_space = 2 do? A: Enables full ASLR — randomizes the stack, heap, mmap region, and executable base — so exploits can’t rely on hardcoded memory addresses. Combined with NX (non-executable data pages) it makes reliable code-execution exploits much harder.

Q: Why put /tmp on its own mount with nosuid,nodev,noexec? A: /tmp is world-writable — the natural place for an attacker to drop a payload. noexec stops it being run, nosuid neutralises any setuid bit on a dropped binary, and nodev prevents planted device nodes. A separate mount also stops a full /tmp from taking down the root filesystem.

Q: How does fail2ban work, and how can it lock you out? A: It tails auth logs (journald by default), and after maxretry failures from an IP within findtime, inserts a firewall rule banning that IP for bantime. It can ban you if your IP fluctuates (VPN/CGNAT) or you fumble a password — mitigate with ignoreip for your admin CIDR and know fail2ban-client set <jail> unbanip.

Q: What is the difference between CIS Level 1 and Level 2? A: Level 1 is practical hardening with minimal functional impact — safe as a default everywhere. Level 2 is defense-in-depth for high-security environments and can break workflows (e.g. noexec /tmp, disabled core dumps), so it’s applied where you can test and accept the impact.

Q: What does -e 2 do in an auditd rules file, and what’s the trade-off? A: It puts the audit system in immutable mode — no rule changes until reboot — so an attacker can’t silently disable auditing. The trade-off is that you also can’t modify rules without rebooting, so it must be the final line after the rule set is finalised.

Q: (RHCSA-style) Configure the host to lock an account after 4 failed logins for 10 minutes. A: In /etc/security/faillock.conf set deny = 4, unlock_time = 600, fail_interval = 900; ensure pam_faillock is in the PAM stack (via authselect on RHEL 8+). Verify with faillock --user <u> and reset with faillock --user <u> --reset.

Q: (LFCS-style) Find every setuid file on the root filesystem and explain the risk. A: find / -xdev -perm -4000 -type f. Each runs with its owner’s privileges (often root) regardless of who executes it, so a bug in any of them is a privilege-escalation path. Audit the list against a known-good baseline and strip the setuid bit from anything unjustified.

Q: What’s the practical role of OpenSCAP in hardening? A: It scans a host against a machine-readable CIS/STIG profile (from the SCAP Security Guide) and produces a pass/fail report — the automated check of hundreds of controls you didn’t apply by hand. Workflow: scan → report → remediate (review generated fixes) → re-scan; codify passing controls in config management.

Q: Why is patching considered a hardening control, and what’s the risk of automating it? A: Every other layer is defeated by one unpatched CVE in a service you run, so timely patching is essential containment. Automating security updates (unattended-upgrades/dnf-automatic) closes the window on forgotten hosts, but an unattended restart/reboot can cause an outage — tune the reboot window, exclude stateful services, and stage in a test tier.

Key takeaways

linuxhardeningsecuritycis-benchmarksstigsshfail2banauditdsysctlselinuxopenscapkernelattack-surface
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments