Linux kernel hardening with sysctl
sysctl configurations used for bastion, load-balancer and vault roles. It keeps security hardening separate from performance tuning. Test every setting on the exact distribution, kernel and application stack before production rollout.The recommended design is to keep one common hardening file and add a small role-specific file afterward:
output 2 lines
/etc/sysctl.d/50-kernel-hardening.conf /etc/sysctl.d/60-server-role.conf
This is easier to audit than maintaining several almost-identical sysctl.conf files.
Apply and verify with:
$ sysctl --system $ sysctl -a
For an individual setting:
$ sysctl kernel.kptr_restrictNot every kernel exposes every setting. A missing key should be investigated.
Common security baseline
Create /etc/sysctl.d/50-kernel-hardening.conf:
# ----------------------------------------------------------------------------- # Linux kernel hardening baseline # Target: modern Linux kernels, 2026 # ----------------------------------------------------------------------------- # ----------------------------------------------------------------------------- # Memory / process information exposure # ----------------------------------------------------------------------------- # Full address-space layout randomization. kernel.randomize_va_space = 2 # Restrict exposure of kernel pointers, including to privileged readers. kernel.kptr_restrict = 2 # Prevent unprivileged users from reading the kernel log buffer. kernel.dmesg_restrict = 1 # Restrict performance monitoring for unprivileged users. kernel.perf_event_paranoid = 2 # Disable unprivileged BPF while keeping the setting reversible by an # administrator. Value 1 is stronger but cannot be reverted until reboot. kernel.unprivileged_bpf_disabled = 2 # Restrict userfaultfd handling of kernel-mode faults. vm.unprivileged_userfaultfd = 0 # Restrict ptrace to parent/child relationships on kernels with the Yama LSM. # Debuggers can still be used for normal child processes. kernel.yama.ptrace_scope = 1 # ----------------------------------------------------------------------------- # Filesystem protections # ----------------------------------------------------------------------------- # Do not generate core dumps for setuid/setgid or otherwise protected binaries. fs.suid_dumpable = 0 # Protect against common link-based attacks in world-writable directories. fs.protected_hardlinks = 1 fs.protected_symlinks = 1 # Extend protections to FIFOs and regular files in world/group-writable # sticky directories. fs.protected_fifos = 2 fs.protected_regular = 2 # ----------------------------------------------------------------------------- # IPv4 / IPv6 network hardening # ----------------------------------------------------------------------------- # Reject IPv4 source-routed packets. net.ipv4.conf.all.accept_source_route = 0 net.ipv4.conf.default.accept_source_route = 0 # For IPv6, -1 fully disables acceptance of source-routing headers. net.ipv6.conf.all.accept_source_route = -1 net.ipv6.conf.default.accept_source_route = -1 # Do not accept ICMP redirects. net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.default.accept_redirects = 0 net.ipv4.conf.all.secure_redirects = 0 net.ipv4.conf.default.secure_redirects = 0 net.ipv6.conf.all.accept_redirects = 0 net.ipv6.conf.default.accept_redirects = 0 # Hosts should not send IPv4 ICMP redirects. net.ipv4.conf.all.send_redirects = 0 net.ipv4.conf.default.send_redirects = 0 # Basic protections against malformed or broadcast ICMP traffic. net.ipv4.icmp_echo_ignore_broadcasts = 1 net.ipv4.icmp_ignore_bogus_error_responses = 1 # SYN cookies remain a useful fallback when the SYN backlog overflows. net.ipv4.tcp_syncookies = 1 # Protect against TIME-WAIT assassination described by RFC 1337. net.ipv4.tcp_rfc1337 = 1 # Log packets with impossible/spoofed-looking source addresses. # Consider log-rate limiting on Internet-facing systems. net.ipv4.conf.all.log_martians = 1 net.ipv4.conf.default.log_martians = 1
Role: bastion / jump host
A bastion should normally behave as an endpoint, not as a router.
Create /etc/sysctl.d/60-server-role.conf:
# Do not route traffic between interfaces. net.ipv4.ip_forward = 0 net.ipv6.conf.all.forwarding = 0 # Strict reverse-path validation is appropriate when routing is symmetric. net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.default.rp_filter = 1
For a high-assurance bastion, consider the following only after validating that troubleshooting and management tools do not need them:
# Only privileged processes may attach with ptrace. kernel.yama.ptrace_scope = 2 # Disable creation of new io_uring instances completely. # This reduces kernel attack surface but can break applications that use io_uring. kernel.io_uring_disabled = 2
kernel.io_uring_disabled = 2 is not part of the common baseline because some applications legitimately depend on io_uring.
Role: vault / secrets server
A secrets server has the same network posture as a bastion: it should not route traffic.
net.ipv4.ip_forward = 0 net.ipv6.conf.all.forwarding = 0 net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.default.rp_filter = 1
For a dedicated secrets appliance with a tightly controlled software stack, the stronger process-isolation options are also reasonable after testing:
kernel.yama.ptrace_scope = 2 # Optional: only if the application stack does not require io_uring. kernel.io_uring_disabled = 2
Kernel hardening is only one layer for a vault system. Memory locking, service sandboxing, filesystem permissions, encrypted storage, authentication policy and secret lifecycle controls must be handled by the application and service manager as well.
Role: load balancer
Load balancers are different because some designs intentionally route packets, use virtual IP addresses, asymmetric paths or direct-server-return.
The old load-balancer configuration enabled IPv4 forwarding. Keep that only when the architecture requires routing.
Typical L3/L4 load-balancer overlay:
# Required only for designs that forward packets at the IP layer. net.ipv4.ip_forward = 1 # Use loose reverse-path validation when legitimate asymmetric routing is # possible. Some DSR/policy-routing designs may require per-interface tuning # or rp_filter=0 instead. net.ipv4.conf.all.rp_filter = 2 net.ipv4.conf.default.rp_filter = 2 # Allow services to bind a virtual IPv4 address before it is present locally. # Useful for HA/failover VIP designs. net.ipv4.ip_nonlocal_bind = 1
If the load balancer forwards IPv6 as well:
net.ipv6.conf.all.forwarding = 1
If it is only a userspace reverse proxy such as HAProxy or NGINX, IP forwarding is normally unnecessary:
net.ipv4.ip_forward = 0 net.ipv6.conf.all.forwarding = 0
Do not use strict rp_filter=1 on a load balancer with asymmetric routing. The Linux kernel documentation recommends loose mode for complicated/asymmetric routing.
Additional high-assurance options
These controls can reduce kernel attack surface further, but they are operationally significant and should not be enabled blindly.
Disable io_uring
Linux provides:
kernel.io_uring_disabled = 1
to block unprivileged io_uring creation while still allowing privileged processes, or:
kernel.io_uring_disabled = 2
to block creation for all processes.
This is suitable only when application compatibility has been tested.
Disable further kernel module loading
After all required modules are loaded, Linux can permanently disable module load/unload operations until reboot:
kernel.modules_disabled = 1
This is an irreversible runtime setting. It can break hot-plugging, filesystems, drivers, monitoring and recovery workflows. It is therefore not included in the baseline.
Disable kexec loading
Linux also supports:
kernel.kexec_load_disabled = 1
which permanently disables new kexec image loading until reboot. This can be valuable on locked-down appliances but can interfere with kdump or operational recovery procedures.
Validation
Check that all intended values were applied:
$ sysctl --system $ $ sysctl kernel.randomize_va_space $ sysctl kernel.kptr_restrict $ sysctl kernel.dmesg_restrict $ sysctl kernel.unprivileged_bpf_disabled $ sysctl vm.unprivileged_userfaultfd $ $ sysctl fs.suid_dumpable $ sysctl fs.protected_hardlinks $ sysctl fs.protected_symlinks $ sysctl fs.protected_fifos $ sysctl fs.protected_regular $ $ sysctl net.ipv4.ip_forward $ sysctl net.ipv4.conf.all.rp_filter $ sysctl net.ipv4.conf.all.accept_redirects $ sysctl net.ipv6.conf.all.accept_redirects
Also verify application behavior, routing, failover and monitoring. A syntactically valid sysctl profile can still be operationally wrong for a particular network design.
References
- Linux kernel - /proc/sys/kernel
- Linux kernel - /proc/sys/fs
- Linux kernel - /proc/sys/vm
- Linux kernel - IP sysctl
- Linux kernel - Yama LSM
- Linux kernel - perf events security
- RFC 3704 - Ingress Filtering for Multihomed Networks
- RFC 1337 - TIME-WAIT Assassination Hazards in TCP
Current practice (checked 2026-10)
- Still valid: every key in the baseline and in the role files is documented in the current kernel sysctl documentation with the values used above. None of them has been removed or renamed.
- unprivileged_bpf_disabled: distributions already set this key; Oracle Linux 10, for example, ships
kernel.unprivileged_bpf_disabled=1in/usr/lib/sysctl.d/01-unprivileged-bpf.conf. Value 1 cannot be changed until reboot, so the later= 2line fails on such a host. Check the running value first and drop the line when it is already 1. - Already the default:
kernel.perf_event_paranoid = 2andvm.unprivileged_userfaultfd = 0are the kernel defaults. Keeping them explicit is harmless and documents the intent. - rp_filter: the kernel uses the maximum of
conf/all/rp_filterand the per-interface value. systemd and distribution defaults write per-interface values with anet.ipv4.conf.*.rp_filterglob (seen here as 2 on one interface, 1 on the others), so setting onlyallanddefaultto 1 does not guarantee strict mode on existing interfaces. Verify per interface. - io_uring: with
kernel.io_uring_disabled = 1the companion keykernel.io_uring_groupnames one group that may still create io_uring instances; this is a middle path between 1 and the full block of 2. - kexec:
kernel.kexec_load_limit_panicandkernel.kexec_load_limit_rebootlimit the number of remaining kexec loads per image type. They allow loading the kdump crash kernel once and then locking kexec, wherekexec_load_disabled = 1would block kdump too. - BPF JIT: the baseline disables unprivileged BPF but does not set
net.core.bpf_jit_harden. It takes 0 (off), 1 (harden for unprivileged users) or 2 (all users) and fits the same section.
$ sysctl kernel.unprivileged_bpf_disabled $ sysctl -a -r 'rp_filter$'
Sources: