Kernel & memory hardening (sysctl)
ptrace, dmesg, kptr, and friends.
A hardened host is not a host nobody can break into. That bar is impossible. A useful bar sits lower and meaner: when someone does get a toehold in one process, they find almost nothing worth having and no easy path to the rest of the machine. An office building after hours works the same way. A burglar who jimmies a ground-floor window still runs into locked stairwell doors, a floor plan that has been shredded, and a visitor log they cannot open. The Linux kernel (the core of the operating system that manages memory, hardware, and every running process) ships with a row of these interior locks either switched off or left half-closed. This lesson switches them on, and shows you how to prove they caught.
What these knobs actually control
Every one of these settings is a switch on the kernel's wall, and sysctl (system control, the interface for reading and writing kernel settings while the machine is running) is how you reach it. There are thousands of them. Each lives as a plain file under /proc/sys, so writing kernel.kptr_restrict is the exact same act as writing the file /proc/sys/kernel/kptr_restrict. You have two ways to flip a switch. sysctl -w flips it right now and forgets by the next reboot. A file dropped in /etc/sysctl.d/ flips it and flips it again on every boot, which is what you want for anything that matters. Start by reading where you stand.
On a current Ubuntu those three already read 1, because Ubuntu turns the first two on for you in its own /etc/sysctl.d files and builds the kernel to restrict the log by default. Debian is looser out of the box: it leaves kernel.yama.ptrace_scope at 0 and does not tighten kptr_restrict, so on Debian you are not confirming these so much as enabling them. Either way the plan is the same. Confirm what is already set, ratchet kptr_restrict up one more notch, add the two or three knobs the distribution leaves open, and write the whole policy into a single file you own so a package update cannot quietly walk it back.
Stop one process from reading another's memory
Every running program keeps live secrets in memory: decrypted config, session tokens, the private key it is using this second. ptrace (process trace, the system call a debugger uses to reach inside another running process and control it) is the master key to all of that. A debugger uses the key for good reasons. An attacker who has already landed on your box uses the same key to step sideways into a neighbouring process and read its secrets straight out of memory.
Most services run more than one process under a single account. A web app might run eight worker processes, every one of them as the user www-data. Break into one worker, and if ptrace is wide open you can attach to the other seven and lift whatever they are holding. Yama (a Linux Security Module, an optional kernel component that bolts on extra access checks) decides how far ptrace is allowed to reach, through the setting kernel.yama.ptrace_scope. Watch it turn an attach down.
With the scope set to 1, a process may trace only its own descendants: the children it started, their children, and on down. The shell in Session B never started that sleep, so the kernel says no. Flip the value to 0 and the same strace would have sailed through, printing every system call the victim makes and laying its memory open to a debugger. There are four settings, from loosest to tightest. 0 is the classic Unix behaviour, where any process can trace another running under the same user. 1 narrows tracing to descendants. 2 allows it only for a process carrying the CAP_SYS_PTRACE capability (one of the fine-grained slices that root's total power is chopped into). 3 shuts ptrace off completely until the next reboot.
Hide the map and lock the logbook
A memory-corruption exploit is a burglar working blind. To turn a crash into a root shell, it needs real addresses: where a specific kernel function or structure sits in memory this boot. Two files hand that map over. /proc/kallsyms lists kernel symbols next to their addresses. The kernel log carries them too, sprinkled through crash traces and driver chatter.
kernel.kptr_restrict controls the map. At 0, kernel pointers print in full to anybody who looks. At 1, they come out as zeros for any user without the CAP_SYSLOG capability. At 2, they are zeros for everyone, root included. kernel.dmesg_restrict controls the logbook: set to 1, only a process holding CAP_SYSLOG may read the kernel ring buffer (a fixed-size in-memory log that keeps overwriting its own oldest lines). Look at both from an ordinary, unprivileged shell.
Zeros where the addresses should be, and a flat refusal on the log. The exploit now has to find its targets the hard way, and for a lot of off-the-shelf attack code the hard way means not at all. Pushing kptr_restrict up to 2 is the aggressive setting and it is safe on servers. About the only thing that trips over it is the odd monitoring tool that genuinely needs live kernel addresses.
Shrink the surface: BPF and perf
BPF and perf are two power tools the kernel lends out to ordinary programs. Both are useful. Both are sharp enough to cut you. BPF (Berkeley Packet Filter, a small programming sandbox that runs your code inside the kernel itself) drives modern tracing and packet filtering. perf (the kernel's performance-monitoring subsystem) reads deep hardware and kernel counters. Each has a long history of bugs that a local user has bent into a root shell. If the unprivileged programs on this host have no need for them, take them away.
kernel.unprivileged_bpf_disabled set to 1 blocks the bpf() system call for anyone without privilege, and closes a whole family of local-escalation bugs in one move. kernel.perf_event_paranoid set to 3 refuses unprivileged programs any access to perf. One honest footnote on that 3: it is a Debian and Ubuntu extension. On a stock upstream kernel the strictest value the code defines is 2, and writing 3 there behaves like 2 rather than adding anything more.
The config file
# Process isolationkernel.yama.ptrace_scope = 1 # trace only your own descendants# Information hidingkernel.kptr_restrict = 2 # zero out kernel pointers for everyonekernel.dmesg_restrict = 1 # only CAP_SYSLOG reads the kernel ring buffer# Attack-surface reductionkernel.perf_event_paranoid = 3 # no unprivileged perf (Debian/Ubuntu value)kernel.unprivileged_bpf_disabled = 1 # no unprivileged BPF program loading
The filename is doing real work here. When sysctl --system runs, it gathers every file from /etc/sysctl.d/ and the system directories, sorts them by name, and applies them in that order, and for any single key the last file that sets it wins. The 90- prefix lands your file well after the distribution's own 50-default.conf, so your stricter values overwrite its gentler baseline. A file numbered higher than yours would beat you in turn, which is the reason to keep all your hardening keys together in this one file: a reviewer, or you in six months, can read the whole policy at a glance instead of hunting for a stray override buried in the 99s.
Apply and verify
sysctl --system rereads every file and echoes each key as it sets it, so that stream of key = value lines is your receipt that the file parsed and took. Notice the order: the 90- file is applied after 50-default.conf but before the 99- files, exactly as the name-sort predicts. Read any value straight back out of /proc/sys whenever you want to check the live state. Hold on to the difference between the two ways in: this file is the part that survives a reboot, while sysctl -w only ever touches the kernel running right now.
Randomize the layout, close the link races
ASLR (Address Space Layout Randomization) reshuffles where a program's stack, libraries, and heap land in memory on every single launch, so an exploit that leans on a fixed address is guessing in the dark. kernel.randomize_va_space = 2 is the full-strength setting, and it is the modern default. Confirm it anyway. A stray tuning guide or an ancient container image will sometimes dial it down to shave a few cycles off something.
The link protections shut down a classic bait-and-switch. A privileged program sets out to create a file in a shared, world-writable directory like /tmp. An attacker slips a symbolic link into that directory pointing at /etc/passwd, gets the timing right, and the privileged program follows the link and writes straight into a file it never meant to touch. fs.protected_symlinks and fs.protected_hardlinks tell the kernel to refuse attacker-owned links in sticky directories. fs.protected_fifos and fs.protected_regular stretch the same rule over named pipes and ordinary files. fs.suid_dumpable = 0 stops a SUID program (Set User ID, a program that runs with its owner's privileges instead of yours) from writing a core dump (the snapshot of memory saved when a program crashes), because that snapshot would spill exactly the secrets the program was holding.
# ...appended to the same file# Address-space randomizationkernel.randomize_va_space = 2 # full ASLR (stack, mmap, heap/brk)# Filesystem race protectionsfs.protected_symlinks = 1 # don't follow planted symlinks in sticky dirsfs.protected_hardlinks = 1 # don't hardlink to files you can't readfs.protected_fifos = 2 # don't open others' FIFOs in sticky dirsfs.protected_regular = 2 # don't write others' regular files in sticky dirsfs.suid_dumpable = 0 # never core-dump a SUID program
Capture a before and an after. Dump every value with sysctl -a into a file now, apply your config, dump it again, and diff the two. That one-page diff is what you staple to a change ticket, hand to a CIS benchmark (Center for Internet Security, which publishes hardening checklists) audit, and pull back up the next time a service starts misbehaving and someone asks what actually changed on this host.
Try this
Work through “Randomize the layout, close the link races” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
The trap worth remembering here: some of these are one-way doors. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.