sysctl sets the Linux kernel parameters on the fly: how the
network stack handles packets, what the kernel exposes to processes, how
the file system protects itself. By default, several of these settings
favour compatibility over security. This guide shows how to establish a
baseline, apply a hardening set aligned with the CIS Benchmarks and
ANSSI-BP-028 v2.0 in a persistent way, check its effect, and avoid the
pitfalls (a badly set parameter can cut the network). The proposed set covers
several ANSSI recommendations: R9 (kernel, including ASLR), R11
(Yama/ptrace LSM), R12 (IPv4 network), R13 (IPv6) and R14 (file
systems). R8 (memory options) is set on the kernel command line, not
with sysctl. Audience: administrators hardening an
exposed server.
What you will learn
- Read and change a kernel parameter (
sysctl -n,-w,sysctl.d) - Capture a baseline before touching anything
- Apply a network, kernel and file hardening set that persists across reboots
- Check the real effect (and why some settings require a VM)
- Avoid settings that break routing or a Kubernetes node
What is sysctl?
sysctl is the interface that reads and writes the kernel variables
exposed under /proc/sys/. Each variable has a hierarchical name
(net.ipv4.ip_forward) that maps to a file
(/proc/sys/net/ipv4/ip_forward).
| Action | Command | Persistence |
|---|---|---|
| Read a value | sysctl -n net.ipv4.ip_forward | Not applicable |
| Change on the fly | sysctl -w net.ipv4.ip_forward=0 | Lost at reboot |
| Make persistent | File in /etc/sysctl.d/ + sysctl --system | Survives reboot |
Some settings do not go through sysctl at all: they must be set before
the kernel starts, on its command line. This is the case of audit=1 or
of several memory security options, covered in
Pass parameters to the kernel at boot.
For hardening you can drive on the fly, you always write to a file in
/etc/sysctl.d/: on-the-fly changes (-w) are for testing but vanish at
reboot.
Prerequisites
- A Linux server (Debian, Ubuntu, RHEL, AlmaLinux, Rocky) or a VM
- root access (or
sudo) - Ideally a test VM: some network settings can cut a connection you did not anticipate
Establish a baseline
Before hardening, measure the existing state. Default values vary by distribution and version, so assume nothing.
for k in net.ipv4.conf.all.accept_redirects net.ipv4.conf.all.send_redirects \ net.ipv4.conf.all.rp_filter net.ipv4.conf.all.log_martians \ kernel.kptr_restrict kernel.yama.ptrace_scope; do printf "%-42s = %s\n" "$k" "$(sysctl -n $k)"doneOn a fresh Debian 12, several sensitive settings are unsafe by default (values of the Debian kernel, which the distribution does not change):
net.ipv4.conf.all.accept_redirects = 1net.ipv4.conf.all.send_redirects = 1net.ipv4.conf.all.rp_filter = 0net.ipv4.conf.all.log_martians = 0kernel.kptr_restrict = 0kernel.yama.ptrace_scope = 0These are precisely the values the hardening will fix. Others (such as
kernel.randomize_va_space = 2 or fs.protected_symlinks = 1) are often
already correct, but you freeze them anyway to avoid any drift. On
Debian 13, the /usr/lib/sysctl.d/50-default.conf file shipped by systemd
sets rp_filter = 2 on each interface but explicitly excludes conf.all,
which stays at the kernel value.
Deploy a hardening set
Put the configuration in /etc/sysctl.d/99-hardening.conf. Two precedence
rules to know: with an identical file name, the directory wins
(/etc > /run > /usr/lib); with different names, the name that
sorts last lexically wins. The 99- prefix therefore guarantees that
your file is read last and takes precedence over the values set by the
distribution; sysctl --system finally reads /etc/sysctl.conf at the very
end.
-
Create the hardening file
/etc/sysctl.d/99-hardening.conf # --- Network: routing and redirects (all + default pairs) ---net.ipv4.ip_forward = 0net.ipv4.conf.all.accept_redirects = 0net.ipv4.conf.default.accept_redirects = 0net.ipv6.conf.all.accept_redirects = 0net.ipv6.conf.default.accept_redirects = 0net.ipv4.conf.all.secure_redirects = 0net.ipv4.conf.default.secure_redirects = 0net.ipv4.conf.all.send_redirects = 0net.ipv4.conf.default.send_redirects = 0net.ipv4.conf.all.accept_source_route = 0net.ipv4.conf.default.accept_source_route = 0net.ipv6.conf.all.accept_source_route = 0# --- Network: anti-spoofing and logging ---net.ipv4.conf.all.rp_filter = 1net.ipv4.conf.default.rp_filter = 1net.ipv4.conf.all.log_martians = 1net.ipv4.conf.default.log_martians = 1net.ipv4.icmp_echo_ignore_broadcasts = 1net.ipv4.icmp_ignore_bogus_error_responses = 1net.ipv4.tcp_syncookies = 1net.ipv4.tcp_rfc1337 = 1net.ipv6.conf.all.accept_ra = 0net.ipv6.conf.default.accept_ra = 0# --- Kernel: information exposure and eBPF ---kernel.kptr_restrict = 2kernel.dmesg_restrict = 1kernel.randomize_va_space = 2kernel.yama.ptrace_scope = 1kernel.unprivileged_bpf_disabled = 2net.core.bpf_jit_harden = 2kernel.kexec_load_disabled = 1# --- File system ---fs.protected_hardlinks = 1fs.protected_symlinks = 1fs.protected_fifos = 2fs.protected_regular = 2fs.suid_dumpable = 0 -
Apply the configuration
Terminal window sudo sysctl --system* Applying /etc/sysctl.d/99-hardening.conf ...sysctl --systemreloads all thesysctl.dfiles in order.
An unknown key goes completely unnoticed
This is the pitfall that ruins a hardening without anyone noticing.
Contrary to what you would expect, sysctl --system does not report a
non-existent parameter: it implies the -e option, which suppresses
precisely these errors.
Measured on AlmaLinux 10 then on Debian 13 (procps-ng 4.0.4) with a
deliberately wrong key: sysctl --system prints no message about it and
exits with code 0. You believe you hardened the machine, and you did not.
To see the error, you must target the file explicitly:
sudo sysctl -p /etc/sysctl.d/99-hardening.confsysctl: cannot stat /proc/sys/kernel/parametre_inexistant: No such file or directorySo get into the habit of validating each file with
sysctl -p <file> after writing it, then checking the value actually
applied, the only one that counts:
sysctl -n kernel.kptr_restrictThe role of each family of settings
The file above stacks some thirty lines, and applying them without understanding them amounts to copying a recipe. The six families below each state what they protect against, and at what cost.
- redirects and source route: an attacker on the network can no longer divert your traffic through ICMP redirects or forced routing.
rp_filter: strict reverse path filtering drops packets whose source address is inconsistent with the route, a basic anti-spoofing.kptr_restrictanddmesg_restrict: you hide kernel addresses and kernel logs, which help make an exploit reliable.ptrace_scope: a process can no longer spy on another outside its descendants, through the Yama LSM, which must be loaded.unprivileged_bpf_disabledandbpf_jit_harden: you close eBPF to unprivileged accounts and harden the JIT, a notorious kernel surface. The value2, already the default on Debian 12, stays reversible;1locks until the next reboot.kexec_load_disabled = 1, ANSSI R9 rule, forbids loading a replacement kernel on the fly. The trade-off is heavy: it disableskdump, which relies on kexec, so do not set it if you capture memory dumps.
Check the effect
Read the targeted values again: they must have switched. Each one shows the value set in the file, which proves the kernel took it.
sysctl net.ipv4.conf.all.rp_filter kernel.kptr_restrict kernel.yama.ptrace_scopenet.ipv4.conf.all.rp_filter = 1kernel.kptr_restrict = 2kernel.yama.ptrace_scope = 1For concrete proof, look at the effect of kptr_restrict = 2 on the
addresses of the kernel symbols:
head -2 /proc/kallsyms0000000000000000 A fixed_percpu_data0000000000000000 A __per_cpu_startThe real addresses are masked (replaced by zeros), even for root: a local
exploit can no longer read them to bypass the kernel ASLR. The three
levels: 0 exposes them depending on the context, 1 hides them from
unprivileged processes, 2 hides them from everyone, root included.
The container versus VM pitfall
This is the limit to know before thinking you can harden "everywhere". In
an unprivileged Docker container, /proc/sys is mounted read-only:
even the net.*, isolated by the kernel, are refused from the inside.
Measured with Docker 29.1.3 on a Debian 13 image:
# Inside a Docker containersysctl -w kernel.kptr_restrict=2# sysctl: permission denied on key "kernel.kptr_restrict"
sysctl -w net.ipv4.conf.all.rp_filter=1# sysctl: permission denied on key "net.ipv4.conf.all.rp_filter"
# From the host, at launch: accepted for net.*docker run --rm --sysctl net.ipv4.conf.all.rp_filter=1 \ debian:13-slim@sha256:020c0d20b9880058cbe785a9db107156c3c75c2ac944a6aa7ab59f2add76a7bd \ cat /proc/sys/net/ipv4/conf/all/rp_filter# 1| Family | Unprivileged Docker container | VM / physical |
|---|---|---|
kernel.* (except IPC), fs.* | Refused, shared with the host | Modifiable |
IPC kernel.* (shm*, msg*, sem) | Isolated by IPC ns, set with --sysctl | Modifiable |
net.* | Isolated by netns, set with --sysctl | Modifiable |
In practice: a full kernel hardening only applies at host level (or a VM), not from an application container. Containers inherit the host's hardened kernel.
Security and points of attention
Some settings break legitimate cases. Adapt them before applying them in bulk:
ip_forward = 0cuts routing. A router, a NAT gateway, a Kubernetes node or a Docker host needip_forward = 1. Only harden this parameter on a simple application server.rp_filter = 1(strict) breaks asymmetric routing and multi-homed hosts. When in doubt, use the value2(loose mode). Subtlety: the kernel appliesmax(conf.all, conf.<interface>), so settingall = 0does not disable filtering if an interface has a higher value; anddefaultonly applies to interfaces created afterwards. CIS and ANSSI want1, where systemd/Debian often keep2by default, on purpose.accept_ra = 0assumes a statically configured IPv6. On a network that distributes prefixes through Router Advertisement, keep it enabled.- Disabling IPv6 (ANSSI R13,
net.ipv6.conf.all.disable_ipv6 = 1) is a choice, not a default: if sshd or Postfix listen on::1, they can refuse to start (planAddressFamily inet/inet_protocols = ipv4). The radical form isipv6.disable=1on the GRUB command line. - Beware of network managers: NetworkManager and systemd-networkd
set their per-interface values again after
systemd-sysctl. If a per-interface setting does not hold, fix it on the manager side (IPv4ReversePathFilter=for networkd) rather than stacking sysctl files. - Yama must be loaded for
kernel.yama.ptrace_scopeto exist: check withcat /sys/kernel/security/lsm(otherwise, addyamatolsm=at boot). - Always test on a VM or through a fallback console (KVM/IPMI): a bad network setting applied remotely can cut off your access.
To roll these settings out across a fleet, the
ansible.posix.sysctl
(in French) module applies them and makes them persistent idempotently.
Troubleshooting
These five symptoms share one trait: none of them points to the faulty
file. A permission denied, a value that reverts or a key silently ignored
only say that the setting did not take, never why. The first three come
from the mechanism, container, -w or read order, the last two from the
content of the file.
| Symptom | Probable cause | Solution |
|---|---|---|
permission denied on key kernel.* | Running inside a container | Apply on the host or a VM |
| The setting reverts to its old value after reboot | Changed with -w instead of a sysctl.d file | Write to /etc/sysctl.d/ then sysctl --system |
A sysctl.d value is ignored | A file with a higher prefix overwrites it | Check the order with sysctl --system; name it 99- |
| Loss of connectivity after applying | ip_forward=0 or rp_filter=1 on a router/multi-homed host | Restore through the console, adjust (ip_forward=1, rp_filter=2) |
cannot stat /proc/sys/... with sysctl -p <file> | Parameter missing on this kernel (which sysctl --system keeps quiet about) | Remove the line, or load the module that creates the key |
Put it into practice
A sysctl hardening is only worth something if it holds after a reboot. This lab has you write a set of parameters in /etc/sysctl.d/, apply it with sysctl --system and prove that the values survive a reboot of the VM.
Key points
- sysctl sets the kernel on the fly; to harden, you write to
/etc/sysctl.d/(the99-wins) - Capture a baseline first: defaults vary by distribution
- Key targets: redirects, rp_filter, kptr_restrict, ptrace_scope (Yama), eBPF, fs protections (ANSSI R9/R11/R12/R14)
kptr_restrict = 2masks kernel addresses from everyone, root included (checkable in/proc/kallsyms)- In a container, only
net.*and the namespaced IPC are isolated, and Docker sets them at launch (--sysctl): kernel hardening happens on the host/VM - Watch out for
ip_forwardandrp_filter(max(all, iface)semantics): they break routers and multi-homed hosts
Knowledge check
Check that the essentials of this guide are acquired. The questions only cover what has just been explained here.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Protect GRUB with a password (boot loader hardening)
A new full quiz with random questions
Work again only on the questions you missed