Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

sysctl: harden the Linux kernel and network stack

By updated

Improve this page

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).

ActionCommandPersistence
Read a valuesysctl -n net.ipv4.ip_forwardNot applicable
Change on the flysysctl -w net.ipv4.ip_forward=0Lost at reboot
Make persistentFile in /etc/sysctl.d/ + sysctl --systemSurvives 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.

Terminal window
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)"
done

On 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 = 1
net.ipv4.conf.all.send_redirects = 1
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.all.log_martians = 0
kernel.kptr_restrict = 0
kernel.yama.ptrace_scope = 0

These 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.

  1. Create the hardening file

    /etc/sysctl.d/99-hardening.conf
    # --- Network: routing and redirects (all + default pairs) ---
    net.ipv4.ip_forward = 0
    net.ipv4.conf.all.accept_redirects = 0
    net.ipv4.conf.default.accept_redirects = 0
    net.ipv6.conf.all.accept_redirects = 0
    net.ipv6.conf.default.accept_redirects = 0
    net.ipv4.conf.all.secure_redirects = 0
    net.ipv4.conf.default.secure_redirects = 0
    net.ipv4.conf.all.send_redirects = 0
    net.ipv4.conf.default.send_redirects = 0
    net.ipv4.conf.all.accept_source_route = 0
    net.ipv4.conf.default.accept_source_route = 0
    net.ipv6.conf.all.accept_source_route = 0
    # --- Network: anti-spoofing and logging ---
    net.ipv4.conf.all.rp_filter = 1
    net.ipv4.conf.default.rp_filter = 1
    net.ipv4.conf.all.log_martians = 1
    net.ipv4.conf.default.log_martians = 1
    net.ipv4.icmp_echo_ignore_broadcasts = 1
    net.ipv4.icmp_ignore_bogus_error_responses = 1
    net.ipv4.tcp_syncookies = 1
    net.ipv4.tcp_rfc1337 = 1
    net.ipv6.conf.all.accept_ra = 0
    net.ipv6.conf.default.accept_ra = 0
    # --- Kernel: information exposure and eBPF ---
    kernel.kptr_restrict = 2
    kernel.dmesg_restrict = 1
    kernel.randomize_va_space = 2
    kernel.yama.ptrace_scope = 1
    kernel.unprivileged_bpf_disabled = 2
    net.core.bpf_jit_harden = 2
    kernel.kexec_load_disabled = 1
    # --- File system ---
    fs.protected_hardlinks = 1
    fs.protected_symlinks = 1
    fs.protected_fifos = 2
    fs.protected_regular = 2
    fs.suid_dumpable = 0
  2. Apply the configuration

    Terminal window
    sudo sysctl --system
    * Applying /etc/sysctl.d/99-hardening.conf ...

    sysctl --system reloads all the sysctl.d files 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:

Terminal window
sudo sysctl -p /etc/sysctl.d/99-hardening.conf
sysctl: cannot stat /proc/sys/kernel/parametre_inexistant: No such file or directory

So 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:

Terminal window
sysctl -n kernel.kptr_restrict

The 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_restrict and dmesg_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_disabled and bpf_jit_harden: you close eBPF to unprivileged accounts and harden the JIT, a notorious kernel surface. The value 2, already the default on Debian 12, stays reversible; 1 locks 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 disables kdump, 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.

Terminal window
sysctl net.ipv4.conf.all.rp_filter kernel.kptr_restrict kernel.yama.ptrace_scope
net.ipv4.conf.all.rp_filter = 1
kernel.kptr_restrict = 2
kernel.yama.ptrace_scope = 1

For concrete proof, look at the effect of kptr_restrict = 2 on the addresses of the kernel symbols:

Terminal window
head -2 /proc/kallsyms
0000000000000000 A fixed_percpu_data
0000000000000000 A __per_cpu_start

The 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:

Terminal window
# Inside a Docker container
sysctl -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
FamilyUnprivileged Docker containerVM / physical
kernel.* (except IPC), fs.*Refused, shared with the hostModifiable
IPC kernel.* (shm*, msg*, sem)Isolated by IPC ns, set with --sysctlModifiable
net.*Isolated by netns, set with --sysctlModifiable

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 = 0 cuts routing. A router, a NAT gateway, a Kubernetes node or a Docker host need ip_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 value 2 (loose mode). Subtlety: the kernel applies max(conf.all, conf.<interface>), so setting all = 0 does not disable filtering if an interface has a higher value; and default only applies to interfaces created afterwards. CIS and ANSSI want 1, where systemd/Debian often keep 2 by default, on purpose.
  • accept_ra = 0 assumes 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 (plan AddressFamily inet / inet_protocols = ipv4). The radical form is ipv6.disable=1 on 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_scope to exist: check with cat /sys/kernel/security/lsm (otherwise, add yama to lsm= 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.

SymptomProbable causeSolution
permission denied on key kernel.*Running inside a containerApply on the host or a VM
The setting reverts to its old value after rebootChanged with -w instead of a sysctl.d fileWrite to /etc/sysctl.d/ then sysctl --system
A sysctl.d value is ignoredA file with a higher prefix overwrites itCheck the order with sysctl --system; name it 99-
Loss of connectivity after applyingip_forward=0 or rp_filter=1 on a router/multi-homed hostRestore 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/ (the 99- 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 = 2 masks 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_forward and rp_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

6 questions
6 min
70% to pass

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

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn