Linux OS and kernel hardening
A freshly installed Linux box is built to be convenient, which means it ships with services you did not ask for, kernel features you will never use and defaults chosen so that nothing surprises a beginner. Hardening it is mostly subtraction: remove what is listening, switch off the kernel surfaces you do not need, tighten the handful of sysctl knobs that matter and lock down the accounts and the module loader. The individual steps are small and well known. The discipline is doing them deliberately rather than inheriting whatever the installer decided and keeping the box patched, because the kernel exploits that matter (PwnKit, Dirty Pipe and their successors) are patched long before most machines apply the fix.
Reduce the surface before configuring it
The first pass is an inventory, because you cannot harden what you have not noticed. What is listening, what is enabled and what is installed that need not be:
ss -tlnp # listening sockets and their processes
systemctl list-unit-files --state=enabled
Every enabled service that does not earn its place is removed, not merely stopped, because a disabled package is a package that an update or a careless admin re-enables. A minimal install that never had the service is better than a full install with it switched off. This is the cheapest hardening there is and the one most often skipped, because the box already works and nobody wants to touch it.
Kernel knobs that are worth setting
The sysctl surface is large and most of it is noise; a focused set does real work. These are the ones I set on every server, grouped by what they defend:
# /etc/sysctl.d/hardening.conf
# restrict information and tracing that aid local escalation
kernel.dmesg_restrict = 1 # unprivileged users cannot read the kernel ring buffer
kernel.kptr_restrict = 2 # hide kernel pointers from /proc
kernel.yama.ptrace_scope = 1 # a process may only ptrace its own descendants
kernel.unprivileged_bpf_disabled = 1
kernel.kexec_load_disabled = 1 # no loading a new kernel at runtime
# link and fifo protections against /tmp race attacks
fs.protected_symlinks = 1
fs.protected_hardlinks = 1
fs.protected_fifos = 2
fs.protected_regular = 2
fs.suid_dumpable = 0 # no core dumps from setuid binaries
# network: ignore the redirects and source routing nobody legitimately uses
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.rp_filter = 1 # reverse-path filtering, anti-spoof
net.ipv4.tcp_syncookies = 1
ptrace_scope, kptr_restrict and dmesg_restrict are the group that matters most, because they close the information and debugging paths a local exploit leans on: one unprivileged process attaching to another to steal a credential, kernel addresses leaking to defeat KASLR, the ring buffer handing an attacker the layout they need. None of these costs a normal workload anything and together they make a local privilege-escalation attempt noticeably harder.
There is one step sysctl does not touch: the safest kernel surface is the one you never compiled. Disabling a feature beats leaving it, but never building a subsystem beats disabling it and that is a subject of its own, which I cover in recompiling your kernel .
The module loader
A loadable kernel module runs in ring 0, which is why an LKM rootkit is the most powerful kind and why the module loader is a surface worth closing once the box has booted and loaded the modules it needs. Enforce signatures so an unsigned module is refused and on a machine whose hardware does not change, disable module loading entirely after boot:
# refuse unsigned modules
echo 'module.sig_enforce=1' >> kernel cmdline # via your bootloader config
# once the needed modules are loaded, slam the door for the session
sysctl -w kernel.modules_disabled=1
modules_disabled=1 is a one-way switch until reboot: nothing more can be loaded, which defeats the LKM rootkit entirely at the cost of not being able to add a driver without a reboot, a trade that is almost always right on a server.
Accounts, authentication and sudo
The account surface is the boring half and the one attackers actually use. No shared logins, named accounts only and the authentication and privilege decisions made deliberately: password quality enforced through PAM where passwords exist at all, SSH moved to keys-only (its own subject) and sudo scoped rather than handed out as blanket root. The sudo detail that catches people is covered at length elsewhere and it reduces to one line here: a NOPASSWD grant on an interpreter, editor or pager is passwordless root, so the only safe sudo rules name commands that cannot be turned into a shell.
Audit the setuid inventory as part of this, because it is where local escalation shops for tools:
find / -xdev -perm -4000 -type f 2>/dev/null # the list should be short and known
Anything surprising in that list is a finding and a setuid bit that nothing needs is removed.
Mandatory access control and audit
AppArmor or SELinux is the layer that confines a service to what it is supposed to touch, so that a compromised daemon cannot wander the filesystem even with the rights its user would otherwise have. Running it in enforcing mode for the exposed services (the web server, the mail daemon) is a large reduction in what a single service compromise is worth and distributions ship usable profiles for the common ones. Leaving it in permissive or disabled “because it was in the way once” is giving up a real boundary to save an afternoon of profile-tuning.
The audit daemon is the record underneath all of it: auditd with rules watching the sensitive files and the privilege-changing syscalls turns “something happened” into a timeline. It does not prevent anything; it is how you find out what was done, which is the difference between an incident you understand and one you only suspect.
Patch, because the exploit was fixed before it was used
The kernel and userspace exploits that cause real damage are, with striking regularity, ones that were patched upstream weeks or months before the breach. PwnKit sat in pkexec for over a decade, Dirty Pipe was fixed in a point release and in both cases the machines that fell were the ones that had not applied the update. Unattended security updates on a server are not reckless, they are the opposite: the risk of an unexpected reboot from an upgrade is small and schedulable and the risk of running a known-exploitable kernel for months is neither. Automate the security channel, reboot on a schedule for kernel updates and treat a long uptime as a warning rather than a trophy.
Where to land
Strip the box to the services it needs and remove the rest rather than disabling them. Set the focused sysctl group, with ptrace_scope, kptr_restrict, dmesg_restrict and the link protections as the core. Enforce module signing and disable the loader after boot on static hardware. Keep accounts named, authentication key-based and sudo scoped to commands that cannot become a shell, with a known setuid inventory. Run AppArmor or SELinux in enforcing for the exposed services and auditd underneath for the record. And patch on a schedule, because nearly every kernel exploit that matters is one someone could have closed before it was used. The pattern is subtraction and deliberateness: a hardened host is not one with more installed, it is one with less, chosen on purpose and kept current.
[edit: 2026] The Ansible implementation of all this: the baseline and unattended-upgrades roles.