Security Notes
Hardening

Linux System Hardening

Each setting below includes an explanation of why it exists — what attack it blocks or what threat model it addresses. Good hardening is not cargo-culting configuration; it's understanding the threat and applying the right countermeasure.

24 min read 12 sections 8 model answers

Production Linux Philosophy

Before individual settings, understand the mental model for production Linux hardening.

Minimal Footprint

A production server should run only what is needed for its one job. Every installed package is:

  • An attack surface (every binary is a potential LOLBin or exploit target)
  • A patching obligation
  • A source of unexpected side-effects

In practice

bash
# After minimal OS install, audit what's running
systemctl list-units --type=service --state=running
# Disable anything not required for the application
systemctl disable --now avahi-daemon cups bluetooth

# Audit installed packages
rpm -qa --qf "%{NAME}\n" | sort      # RHEL/CentOS
dpkg --get-selections | grep -v deinstall  # Debian/Ubuntu

# Remove unnecessary packages
apt autoremove --purge telnet ftp rsh-client

Single Purpose

One server = one function. A web server is not also a database, a build server, and an NTP source. This limits blast radius: compromise of one function cannot directly pivot to others.

Immutable Infrastructure

Modern production environments treat servers as disposable and rebuilt from code rather than patched in place:

  • Use container images (Docker, Podman) or golden AMI/GCE image baking
  • On finding a vulnerability: rebuild the image, redeploy — never SSH in and manually patch
  • This means your hardening configuration lives in code (Ansible, Terraform, Packer), not in manual steps

Patch Strategy

RiskApproach
Critical/High CVEPatch within 24–72 hours; immutable infra means redeploy
Container base imagesRebuild weekly minimum; use distroless or minimal images
Kernelunattended-upgrades (Debian) or dnf-automatic (RHEL) for security-only updates
Regular patchingMonthly in a maintenance window; staggered rollout
bash
# Ubuntu/Debian: enable automatic security-only updates
apt install unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
# /etc/apt/apt.conf.d/50unattended-upgrades — verify security origin is uncommented

# RHEL/CentOS: automatic security updates
dnf install dnf-automatic
# /etc/dnf/automatic.conf: apply_updates = yes  upgrade_type = security
systemctl enable --now dnf-automatic.timer

Minimal Privilege for Services

Every service runs as its own non-root user, in its own systemd unit with hardened unit options:

ini
# /etc/systemd/system/myapp.service
[Service]
User=myapp
Group=myapp
NoNewPrivileges=true          # cannot gain new privileges via setuid/capabilities
CapabilityBoundingSet=        # drop all capabilities (or keep only what's needed, e.g. CAP_NET_BIND_SERVICE)
PrivateTmp=true               # isolated /tmp view
ProtectSystem=strict          # / and /usr read-only; /etc read-only
ProtectHome=true              # /home, /root, /run/user are invisible
ReadWritePaths=/var/lib/myapp # only this path is writable

WhyIf the app is compromised, it cannot read /etc/shadow, write to /var/log as root, or access other users' data.


Kernel Hardening (sysctl)

bash
# /etc/sysctl.d/99-hardening.conf

Network Settings

net.ipv4.ip_forward = 0
net.ipv6.conf.all.forwarding = 0

WhyKernel packet forwarding turns your server into a router. Unless this is actually a NAT gateway or router, this should be off. If enabled on a compromised host, it can be used to route traffic between network segments the attacker otherwise can't reach.

net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0

WhyICMP redirect messages tell a host to use a different gateway for a route. An attacker on the same LAN can send spoofed ICMP redirects to poison your routing table and MITM traffic. Disable both accepting and sending unless you're a router.

net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0

WhySource routing lets the packet's sender specify the path through the network. This can be used to bypass ACLs and firewall rules by routing through hosts that wouldn't normally be allowed. No production server should accept these packets.

net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1

WhyReverse Path Filtering (RFC 3704). The kernel checks: "does the reply route for this packet's source IP go back out the interface it arrived on?" If not, the source address is spoofed and the packet is dropped. Blocks many DDoS reflection and IP spoofing attacks.

net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 4096
net.ipv4.tcp_synack_retries = 2

WhySYN flood protection. An attacker sends thousands of TCP SYN packets with spoofed source IPs; the server allocates state for each half-open connection until the backlog fills up and legitimate connections are refused (DoS). SYN cookies avoid storing state until the 3-way handshake completes.

net.ipv4.icmp_echo_ignore_broadcasts = 1

WhySmurf attack defense. An attacker sends an ICMP echo request to a broadcast address with your server's IP as the source. Every host on the network replies to your server, overwhelming it.

net.ipv4.icmp_ignore_bogus_error_responses = 1

WhySome routers send malformed ICMP error responses that don't conform to RFC 1122. Logging these creates noise and can be used to generate log spam. Silently discard them.

net.ipv4.conf.all.log_martians = 1

Why"Martian packets" have impossible source addresses (RFC 1918 private addresses arriving on a public interface, loopback addresses from external sources). Logging them helps detect network misconfiguration and spoofing attempts.

Memory and Exploit Mitigations

kernel.randomize_va_space = 2

WhyASLR (Address Space Layout Randomization). Randomizes where the stack, heap, and library mappings appear in memory. Exploits that need to jump to a fixed address (e.g., system() in libc for ret2libc attacks) have to brute-force or leak the address first. 2 = full randomization (stack + mmap + vDSO).

Does NOT protect againstinformation disclosure vulnerabilities that leak addresses; heap spraying; JIT spraying in JavaScript engines.

fs.suid_dumpable = 0

WhyBy default, setuid programs don't produce core dumps (which could contain sensitive memory, including credentials). Setting to 0 ensures no core dump for setuid processes even if ulimit -c unlimited is set globally. Core dumps from privileged programs could expose /etc/shadow contents, private keys, or session tokens in memory.

kernel.dumpable = 0

WhyPrevents processes from having their memory readable via ptrace or /proc/pid/mem by other non-privileged processes. Without this, a process could read another process's memory in the same user session.

kernel.kptr_restrict = 2

WhyKernel pointers (addresses of kernel functions, data structures) shown in /proc/kallsyms and similar interfaces are used by exploit developers to defeat KASLR (Kernel ASLR). kptr_restrict=2 hides them even from root unless the process has CAP_SYSLOG. Without this, an attacker who already has code execution can trivially find commit_creds and prepare_kernel_cred addresses needed for kernel privilege escalation.

kernel.dmesg_restrict = 1

Whydmesg output often contains physical addresses, PCI device info, and kernel version details that aid exploit development. Restricts reading to root only.

kernel.unprivileged_bpf_disabled = 1

WhyUnprivileged eBPF allows any local user to load eBPF programs. The eBPF verifier has had multiple exploitable bugs (CVE-2021-3490, CVE-2021-31440, etc.) that let local users escalate to root. Disabling unprivileged eBPF closes this attack surface. Only processes with CAP_BPF (or CAP_SYS_ADMIN) can load eBPF programs.

kernel.perf_event_paranoid = 3

Whyperf can expose CPU performance counters and kernel call stacks, which have been used as side channels to defeat ASLR (e.g., via PEBS sampling revealing kernel addresses). Level 3 restricts all perf access to root.

kernel.unprivileged_userns_clone = 0   # Debian/Ubuntu

WhyUser namespaces allow unprivileged processes to create sandbox environments with a new UID/GID mapping. While useful for containers (rootless Docker, Podman), they have been exploited repeatedly for local privilege escalation (CVE-2022-0492, CVE-2023-32233 via nested namespaces). If you're not running rootless containers, disable them.

Filesystem Protections

fs.protected_hardlinks = 1
fs.protected_symlinks = 1

WhyClassic TOCTOU (time-of-check-to-time-of-use) attacks in world-writable directories like /tmp. An attacker creates a hardlink to /etc/shadow or a symlink to a privileged file, waits for a privileged process to follow the link during a check-and-act window. These settings prevent non-owner hardlinks and restrict symlink following in world-writable sticky directories.

fs.protected_fifos = 2
fs.protected_regular = 2

WhyPrevent writing to FIFOs and regular files in world-writable sticky directories when the file is owned by someone else. Closes a class of privilege escalation where an attacker pre-creates a file a privileged process will then write to.


Boot Loader (GRUB) Security

bash
# /etc/default/grub — GRUB_CMDLINE_LINUX_DEFAULT
lockdown=confidentiality

WhyKernel lockdown mode (since Linux 5.4) restricts features that could allow user-space to tamper with the running kernel — even as root. confidentiality mode disables: kexec, /dev/mem and /dev/kmem access, hibernation (avoids unencrypted memory dump to disk), loading unsigned kernel modules, and several other kernel-tampering interfaces. An attacker with root who can't load a kernel module or use kexec has very limited rootkit options.

bash
intel_iommu=on iommu=force   # or amd_iommu=on

WhyIOMMU (Input-Output Memory Management Unit) controls which memory regions PCIe devices can access via DMA. Without IOMMU, a compromised or malicious PCIe device (thunderbolt device, NIC firmware) can perform DMA attacks — reading/writing any physical memory address, bypassing all software security. IOMMU enforces that each device can only access its assigned memory regions.

bash
nosmt=force

WhySMT (Simultaneous Multi-Threading / Hyper-Threading) shares L1/L2 caches and execution resources between sibling threads. This sharing is exploited by attacks like Spectre, MDS (Microarchitectural Data Sampling), L1TF (L1 Terminal Fault). In high-security environments (where a co-tenant VM or process could read another's cache), disabling SMT eliminates these attacks entirely at a ~30% CPU throughput cost.

bash
apparmor=1 security=apparmor    # or enforcing=1 for SELinux

WhyActivates the LSM (Linux Security Module) at boot. Without this, the MAC (Mandatory Access Control) framework doesn't confine processes even if profiles are written.

GRUB Password

bash
grub-mkpasswd-pbkdf2
# Add to /etc/grub.d/40_custom:
# set superusers="admin"
# password_pbkdf2 admin <hash>
update-grub

WhyWithout a GRUB password, anyone with physical access can boot into single-user/recovery mode and gain a root shell without authentication — bypassing all OS-level security controls. GRUB password forces authentication before editing boot parameters or selecting recovery mode.


User and Authentication Hardening

Password Policy

bash
# /etc/login.defs
PASS_MAX_DAYS   90     # force rotation every 90 days
PASS_MIN_DAYS   1      # prevent immediate re-use after change
PASS_WARN_AGE   14     # warn 14 days before expiry
PASS_MIN_LEN    14     # minimum length
INACTIVE        30     # lock account if no login in 30 days

WhyLimits the window of opportunity if a credential is stolen. An attacker who captures a hash from an old breach has at most 90 days before it's invalid. Inactive accounts (service accounts, former employees) are a common initial access vector — lock them.

bash
# Enforce strong passwords — /etc/pam.d/common-password
password required pam_pwquality.so retry=3 minlen=14 dcredit=-1 ucredit=-1 ocredit=-1 lcredit=-1

Whydcredit=-1 requires at least 1 digit, ucredit=-1 at least 1 uppercase, ocredit=-1 at least 1 special character, lcredit=-1 at least 1 lowercase. Complexity + length makes dictionary and brute-force attacks impractical.

bash
# Account lockout after failed attempts
# /etc/pam.d/common-auth:
auth required pam_tally2.so onerr=fail audit silent deny=5 unlock_time=900

WhyWithout lockout, attackers can brute-force passwords with no consequence. deny=5 locks after 5 failures; unlock_time=900 (15 min) limits automation without permanently locking legitimate users. onerr=fail — if the module fails for any reason, deny access (fail-secure).

bash
passwd -l root     # disable root login

WhyRoot is the highest-value account on any Linux system. Disabling direct root login forces all administrative actions through sudo (which is logged). Audit trail: you can see which user ran which command as root. With direct root login, there's no attribution.


SSH Hardening

SSH is the most commonly targeted service on internet-facing Linux servers.

bash
# /etc/ssh/sshd_config

Protocol 2

WhySSH Protocol 1 has known cryptographic weaknesses (weak MAC, session key vulnerabilities). It was deprecated in 2001. No reason to support it.

PermitRootLogin no

WhyRoot login via SSH means: (a) no attribution of who logged in if multiple people share the key, (b) a successful brute force immediately gives full system control, (c) root's authorized_keys is a single point of compromise. Use sudo instead.

PasswordAuthentication no
PermitEmptyPasswords no

WhyPassword auth is vulnerable to brute force and credential stuffing. Disabling it forces key-based auth, which requires possession of the private key — cannot be guessed, stolen from a database breach, or phished in the same way.

ChallengeResponseAuthentication no

WhyDisables keyboard-interactive authentication methods (like PAM password challenges). If PasswordAuthentication no is set but this is enabled, some configurations allow fallback to password auth.

AllowGroups sshusers
AllowUsers alice bob

WhyEven if a user account is created on the system, it cannot SSH in unless it's in the allowed list. Prevents service accounts, test accounts, or compromised accounts from being used for remote access.

KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com

WhyRemoves weak algorithms. diffie-hellman-group1-sha1 (Logjam attack), RC4/3DES ciphers, hmac-md5 and hmac-sha1 are all weak. Modern algos: Curve25519 for key exchange (immune to small subgroup attacks), AES-GCM/ChaCha20 AEAD for encryption+integrity together.

X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no

WhyThese forwarding features are powerful and dangerous. TCP forwarding turns your server into a port forwarder through the firewall, potentially allowing tunneled access to internal services. Agent forwarding exposes your SSH agent socket on the remote server — a compromised server can use your agent to SSH into other hosts without ever touching your key. X11 forwarding enables screen scraping and keylogging of remote GUI sessions.

ClientAliveInterval 300
ClientAliveCountMax 2

WhyTerminates idle sessions after ~10 minutes (300s × 2). Abandoned sessions represent an open window — an unattended terminal, a laptop left unlocked. Zombie sessions also consume resources.

MaxAuthTries 3

WhyLimits how many authentication attempts per connection. Combined with fail2ban, this drastically slows brute force attempts. An attacker gets 3 guesses per TCP connection before being disconnected.

LogLevel VERBOSE

WhyLogs fingerprints of keys used for login. If you're investigating a breach and need to know which key was used to log in, the default INFO level doesn't record fingerprints — VERBOSE does.


Filesystem and Mount Hardening

tmpfs /tmp tmpfs defaults,rw,nosuid,nodev,noexec,relatime 0 0

Why

  • noexec: prevents running compiled binaries or scripts dropped in /tmp. A common post-exploitation step is to wget a binary to /tmp and execute it. noexec breaks this without affecting bash scripts run via bash /tmp/script.sh (the interpreter is what runs).
  • nosuid: any SUID/SGID bits on files in /tmp are ignored. Prevents privilege escalation via a setuid binary planted in /tmp.
  • nodev: device files (block/char special files) in /tmp are ignored. Prevents creating /tmp/null, /tmp/zero etc. that map to real devices.
tmpfs /dev/shm tmpfs defaults,nodev,nosuid,noexec 0 0

Why/dev/shm is shared memory — commonly used for IPC but also abused as an execution sandbox to avoid /tmp restrictions (some attack tools check if /tmp is noexec and fall back to /dev/shm).

bash
chmod +t /tmp /var/tmp   # sticky bit

WhyThe sticky bit on world-writable directories means only the file's owner (or root) can delete or rename it. Without it, any user can delete other users' files in /tmp, enabling race-condition attacks (TOCTOU) where the attacker deletes and replaces a legitimate file before a privileged process uses it.


SELinux and AppArmor

Why Mandatory Access Control?

Discretionary Access Control (DAC) — standard Unix rwx permissions — is "discretionary" because the owner can grant permissions to anyone. If a process running as www-data is compromised, it can read any file www-data can read, write to any path www-data can write, and connect to any address.

MAC (Mandatory Access Control via SELinux/AppArmor) enforces policy set by an administrator regardless of what the process owner decides. The web server process gets a label/profile that says: "may read /var/www/html, may listen on port 80/443, may write to /var/log/nginx — nothing else." Even running as root, policy is enforced.

SELinux (RHEL/CentOS/Fedora)

bash
sestatus; getenforce          # check status
setenforce 1                  # enforce immediately
# /etc/selinux/config: SELINUX=enforcing  (persistent)

ausearch -m avc -ts recent    # review denials
audit2why < /var/log/audit/audit.log  # explain a denial in plain language
audit2allow                   # generate a policy module to allow a denial (use sparingly)

# Label a directory for Apache
semanage fcontext -a -t httpd_sys_content_t "/var/www/html(/.*)?"
restorecon -Rv /var/www/html

# Check what context a process is running with
ps auxZ | grep nginx

Enforcing vs PermissiveEnforcing blocks and logs violations. Permissive only logs them — use Permissive to tune policy before switching to Enforcing, not in production.

AppArmor (Debian/Ubuntu)

bash
aa-status                            # show loaded profiles and modes
aa-enforce /etc/apparmor.d/usr.sbin.nginx   # enforce mode for nginx
aa-genprof /usr/local/bin/myapp      # interactively build a profile
journalctl -t kernel | grep apparmor # violations

Auditd

Whyauditd is the kernel-level audit subsystem. When an incident happens and you need to answer "who executed what, when, from which process" — auditd is what provides the forensic trail. Without it, you have no syscall-level audit. syslog only captures what programs choose to log; auditd captures what the kernel sees.

bash
# /etc/audit/rules.d/hardening.rules

# Watch for changes to sensitive files
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k sudo
-w /etc/ssh/sshd_config -p wa -k sshconfig
-w /etc/cron.d -p wa -k cron

WhyThese files are the most valuable targets for persistence and privilege escalation. Any write to /etc/sudoers or addition to /etc/passwd should generate an audit event.

bash
# Monitor privilege escalation
-a always,exit -F arch=b64 -S setuid -S setgid -F a0=0 -F exe=/usr/bin/sudo -k priv_esc
-a always,exit -F arch=b64 -S execve -F euid=0 -F uid>=1000 -k priv_exec

WhyCaptures every time a non-root user executes something that runs as root. Essential for detecting SUID abuse and sudo escalation.

bash
# Monitor /tmp execution
-a always,exit -F arch=b64 -S execve -F path=/tmp -k tmp_exec

WhyEven with noexec on /tmp, an attacker may copy the binary to an executable location first. Logging exec events from /tmp (or attempts to use path) gives visibility.

bash
# Network connections
-a always,exit -F arch=b64 -S connect -k net_connect

WhyUnexpected outbound connections (reverse shells, C2 beacons) are a key indicator of compromise. Every connect() syscall is logged with the process that made it.

bash
# Kernel module loading
-w /sbin/insmod -p x -k modules
-w /sbin/rmmod -p x -k modules
-a always,exit -F arch=b64 -S init_module -S delete_module -k modules

WhyRootkits are typically loaded as kernel modules. Auditing module load/unload provides a trail and triggers SIEM alerts.

bash
-e 2    # make audit config immutable until reboot

WhyAn attacker with root could auditctl -D to delete all rules and then proceed undetected. -e 2 makes the audit configuration read-only until reboot. Any attempt to modify rules requires a reboot (which itself is auditable).

bash
# Query audit logs
ausearch -k priv_esc             # show privilege escalation events
ausearch -m execve -sv no -i     # failed exec attempts
ausearch -ts today -c sudo       # sudo usage today
aureport --summary               # executive summary
aureport --failed                # failed events

AIDE (File Integrity Monitoring)

WhyAIDE (Advanced Intrusion Detection Environment) creates a cryptographic baseline of your filesystem (hashes, permissions, ownership, timestamps). After a compromise, comparing the current state against the baseline reveals what was modified — even if the attacker cleaned log files. This is why AIDE databases should be stored off-host (write once, on a separate system).

bash
apt install aide
aideinit                                                    # build initial database
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db         # activate
aide --check                                                # compare current vs baseline
aide --update                                               # update baseline after intentional change

# Cron: daily check, report to security team
0 3 * * * /usr/bin/aide --check 2>&1 | mail -s "AIDE $(hostname)" security@example.com

Key pointThe AIDE database itself must be protected. If an attacker can write to the database file, they can update the baseline to cover their tracks. Store the database on read-only media or a separate host, or use immutable storage (chattr +i).


fail2ban

Whyfail2ban watches log files for patterns indicating brute-force or scanning activity and dynamically bans IPs via iptables/nftables. SSH brute force attempts against internet-exposed servers happen constantly — without fail2ban (or equivalent), your logs fill with noise and eventually a weak password may be guessed. fail2ban reduces attacker throughput to a crawl.

bash
# /etc/fail2ban/jail.local
[DEFAULT]
bantime  = 3600      # ban for 1 hour
findtime = 600       # within a 10-minute window
maxretry = 5         # if 5 failures
banaction = iptables-multiport

[sshd]
enabled = true
port    = ssh
logpath = %(sshd_log)s
maxretry = 3         # stricter for SSH: 3 attempts
bantime = 86400      # ban SSH failures for 24 hours

[nginx-http-auth]
enabled = true

# Operations
fail2ban-client status             # show all jails
fail2ban-client status sshd        # show SSH jail stats
fail2ban-client set sshd unbanip 1.2.3.4   # unban legitimate IP

Production Linux Hardening Checklist

Minimal Install

[ ] Start from minimal/server ISO — no GUI, no desktop environment
[ ] Remove all unnecessary packages (telnet, ftp, rsh, nfs-client if not needed)
[ ] Disable unused services: systemctl disable --now avahi cups bluetooth ModemManager
[ ] Audit enabled services: systemctl list-unit-files --state=enabled
[ ] Remove setuid bits from binaries that don't need them:
    find / -perm -4000 -type f 2>/dev/null     # SUID binaries
    find / -perm -2000 -type f 2>/dev/null     # SGID binaries
[ ] Audit world-writable files: find / -perm -002 -type f 2>/dev/null

Kernel & Boot

[ ] ASLR: kernel.randomize_va_space = 2
[ ] Unprivileged eBPF disabled: kernel.unprivileged_bpf_disabled = 1
[ ] kptr_restrict = 2, dmesg_restrict = 1
[ ] SYN cookies: net.ipv4.tcp_syncookies = 1
[ ] Source routing disabled (accept_source_route = 0)
[ ] ICMP redirects disabled
[ ] Reverse path filtering enabled (rp_filter = 1)
[ ] Protected hardlinks/symlinks (fs.protected_hardlinks/symlinks = 1)
[ ] GRUB password set
[ ] Kernel lockdown=confidentiality
[ ] IOMMU enabled (intel_iommu=on or amd_iommu=on)

Authentication & Access

[ ] Root login disabled (passwd -l root)
[ ] SSH: PasswordAuthentication no
[ ] SSH: PermitRootLogin no
[ ] SSH: AllowUsers / AllowGroups configured
[ ] SSH: Modern KexAlgorithms, Ciphers, MACs only
[ ] SSH: MaxAuthTries 3, ClientAliveInterval 300
[ ] PAM: pam_pwquality for password complexity
[ ] PAM: pam_tally2 / pam_faillock for account lockout
[ ] MFA for SSH on high-value hosts (pam_google_authenticator or Duo)

Filesystem

[ ] /tmp: noexec,nosuid,nodev
[ ] /dev/shm: noexec,nosuid,nodev
[ ] /home: nosuid,nodev
[ ] /var/tmp: noexec,nosuid
[ ] Sticky bit on /tmp and /var/tmp
[ ] Application runs in restricted systemd unit (NoNewPrivileges, PrivateTmp, ProtectSystem=strict)

Monitoring & Integrity

[ ] SELinux enforcing OR AppArmor profiles enforced
[ ] auditd running with identity/priv_esc/module rules
[ ] Audit config immutable (-e 2)
[ ] AIDE initialised, database off-host, daily cron
[ ] fail2ban configured for SSH (and web services if exposed)
[ ] Firewall: default-deny inbound, only required ports open
[ ] Automatic security-only updates enabled (unattended-upgrades / dnf-automatic)
[ ] Logs shipped to remote SIEM / syslog server (local-only logs can be wiped)

Network

[ ] Firewall default-deny: iptables/nftables/ufw with explicit allow rules
[ ] No unnecessary ports listening: ss -tulpn | grep LISTEN
[ ] No root-owned world-writable network sockets
[ ] Internal services bound to 127.0.0.1 only if not needed externally
[ ] SSH access restricted to management network / VPN / bastion host
[ ] Consider port knocking or fail2ban for internet-facing SSH

Memory hook

hardening assumes "the attacker already got a foothold," and removes their next moves. Most sysctl/mount/lockdown settings aren't about keeping attackers out — they're about making life miserable after initial access: strip the info leaks they need (kptr_restrict, randomize_va_space for ASLR), close the privesc paths (nosuid/noexec mounts, disabled unprivileged eBPF/user namespaces), and protect the evidence and the kernel even from root (lockdown mode, auditd immutable -e 2). The mindset that ties it together: assume breach, then deny the escalation and the cover-up. That's why "even if the attacker has root" appears in several questions — good hardening still bites after root.

Interview Questions: Linux Hardening

Q
What does kernel.randomize_va_space = 2 protect against, and what does it NOT?
Model answer

It enables full ASLR — randomizing the stack, heap, libraries, and the mmap base — so an attacker exploiting a memory-corruption bug can't rely on fixed addresses for shellcode, gadgets, or libc functions. That defeats naive, hardcoded-address exploits. What it does not protect against is an exploit that includes an information leak: a single leaked pointer lets the attacker recover the randomized base and compute every other address, defeating ASLR. It also does nothing against logic bugs, and it's weaker on 32-bit where the entropy is small enough to brute-force. So ASLR raises the bar by forcing the attacker to also find a leak, but it's a probabilistic mitigation, not a wall.

Q
Why set kernel.kptr_restrict = 2? What attack does it address?
Model answer

kptr_restrict controls whether kernel pointers are exposed through interfaces like /proc/kallsyms. Set to 2, kernel addresses are hidden from all users including root. It addresses the information-leak half of kernel exploitation: kernel exploits need to defeat KASLR, and the easiest way is to read a leaked kernel symbol address from a /proc or sysfs interface. By denying that, you force the attacker to find a separate, harder info-leak primitive before their privesc exploit can compute kernel addresses. It's a defense-in-depth measure that meaningfully raises the difficulty of turning a kernel bug into a working exploit, which is why it pairs with KASLR.

Q
Why is disabling unprivileged eBPF important?
Model answer

Unprivileged eBPF lets a normal user load programs into the kernel that the verifier must prove safe — but the verifier is enormously complex, and bugs in it have repeatedly become local privilege-escalation CVEs, like the ALU bounds-tracking flaw in CVE-2021-3490. Allowing unprivileged users to reach that attack surface means any verifier bug is directly exploitable for root by any local user, including a compromised low-privilege service. Disabling it with kernel.unprivileged_bpf_disabled=1 removes that whole class of attack from unprivileged contexts while still letting privileged tooling use eBPF. Given how many eBPF privesc bugs have appeared, it's now standard hardening — you give up nothing in most production environments and close a high-value escalation path.

Q
What's the difference between nosuid and noexec mount options?
Model answer

Both are mount options that restrict what can happen on a filesystem. nosuid causes the kernel to ignore the setuid and setgid bits on binaries on that mount, so even a setuid-root binary placed there runs with the caller's privileges, not root — it prevents an attacker who can write to that mount from dropping a setuid-root shell for privilege escalation. noexec prevents executing any binary from that mount at all. You apply them to writable, data-only locations like /tmp, /var/tmp, /dev/shm, and removable media, where there's no legitimate reason to run setuid binaries or execute code — which blocks the common attacker pattern of dropping a payload or setuid binary in /tmp and running it. They address different steps: nosuid stops privilege gain via setuid, noexec stops execution entirely.

Q
How does rp_filter prevent IP spoofing, and why isn't it always strict?
Model answer

Reverse-path filtering checks incoming packets against the routing table: when a packet arrives, the kernel asks "would I route a reply to this source IP back out the interface it came in on?" In strict mode (2... actually 1 is strict per RFC 3704), if the reverse path doesn't match the incoming interface, the packet is dropped — which blocks spoofed source addresses that couldn't legitimately arrive on that interface. The reason you don't always use strict mode is asymmetric routing: in multi-homed setups where traffic legitimately comes in one interface and leaves another, strict reverse-path filtering would drop valid traffic. So loose mode exists to allow asymmetric paths while still filtering truly unroutable sources. You pick strict where the topology is symmetric and loose where it isn't.

Q
Walk me through a minimal production Linux build for a containerized microservice.
Model answer

The goal is minimal attack surface, so I start from the smallest viable base — distroless or Alpine, or a from-scratch image with just the static binary. I remove everything not needed at runtime: no shell, no package manager, no compilers or interpreters, no debugging tools, no setuid binaries. The container runs as a non-root user with a read-only root filesystem, all capabilities dropped, no new privileges, and a seccomp profile. On the host side, mounts for writable data are nosuid and noexec, the kernel is hardened with the sysctls we discussed, and unnecessary kernel modules are blacklisted. I'd scan the image for CVEs in CI and sign it, and ship logs off-box. The principle is that everything I remove is something an attacker can't use to live off the land — a service should contain exactly its one binary and its runtime dependencies, nothing more.

Q
What is kernel lockdown mode and why does it matter even against root?
Model answer

Kernel lockdown is a mode that restricts even root from operations that would let userspace modify or read the running kernel — things like loading unsigned modules, writing to /dev/mem, using kexec, or certain debugging interfaces. It matters because the traditional Linux model treats root as all-powerful, but lockdown enforces a boundary between root and the kernel itself: the point is to prevent an attacker who gains root from tampering with the kernel to install a rootkit, hide their presence, or disable security mechanisms. In integrity mode it blocks modification; in confidentiality mode it also blocks reading kernel memory. It's especially meaningful with Secure Boot, completing the chain so that a compromised root can't undermine the verified kernel. So it's defense that assumes the attacker already won root and still constrains them.

Q
How does auditd's immutable mode (-e 2) protect against an attacker with root?
Model answer

Setting auditd to -e 2 makes the audit configuration immutable until the next reboot — rules can't be changed or deleted, and auditd can't be stopped, without rebooting the machine. This matters because the first thing a competent attacker with root does is try to disable logging and delete their tracks, and normally root can just stop auditd or flush its rules. Immutable mode takes that off the table: the attacker can't quietly turn off the cameras, and forcing a reboot to do so is itself a loud, suspicious event that generates evidence. It pairs with shipping logs off-box in real time, so even within the pre-reboot window the records are already gone from the attacker's reach. It's the same assume-breach philosophy — protect the evidence even from root.

  • What's the difference between SELinux Enforcing and Permissive, and when would you run Permissive in production?
  • Why is it not enough to have noexec on /tmp to prevent execution from that directory?
  • How does AIDE help in post-incident forensics, and why must the AIDE database be stored off-host?