Network sysctls: measure, then change

Forwarding, redirects, rp_filter and SYN cookies.

Intermediate14 min · lesson 18 of 24

A few dozen kernel switches decide how a host treats packets it did not ask for: whether it forwards traffic between interfaces, whether it obeys a router that tells it to change path, and whether it accepts a packet whose source address could not have arrived where it did. Hardening guides list many of them, but most are already the default. In this lesson you read what Ubuntu 26.04 and RHEL 10 ship, test the few keys that differ on a lab network of network namespaces, and write one file that changes only those keys, with a reason for each and a rollback that restores the old values.

How sysctl settings are stored and applied

Every tunable lives as a file under /proc/sys: net.ipv4.conf.all.rp_filter is the file /proc/sys/net/ipv4/conf/all/rp_filter. sysctl reads and writes these with dotted names (sysctl -w key=value), and a value written that way is gone at the next boot. Persistent values live in files that systemd-sysctl.service applies at boot.

Four directories hold them: /etc/sysctl.d/ (yours), /run/sysctl.d/, /usr/local/lib/sysctl.d/ and /usr/lib/sysctl.d/ (the distribution's). All their .conf files are sorted together by name, and for a key set twice the file that sorts last wins. A file in /etc with the same name as one in /usr/lib replaces it entirely. sysctl.d(5) recommends names from 60 to 90 for your own files. Ubuntu 26.04 has no /etc/sysctl.conf at all; its settings are in /usr/lib/sysctl.d.

deploy@web01 · Ubuntu 26.04 LTS
$ ls /usr/lib/sysctl.d/ /etc/sysctl.d/ ls /etc/sysctl.conf
/etc/sysctl.d/: 99-cloudimg-ipv6.conf 99-lima.conf README.sysctl /usr/lib/sysctl.d/: 10-apparmor.conf 10-coredump-debian.conf 50-default.conf 50-pid-max.conf 55-bufferbloat.conf 55-console-messages.conf 55-ipv6-privacy.conf 55-kernel-hardening.conf 55-magic-sysrq.conf 55-map-count.conf 55-network-security.conf 55-ptrace.conf 55-zeropage.conf ls: cannot access '/etc/sysctl.conf': No such file or directory

99-cloudimg-ipv6.conf comes from the cloud image, and 99-lima.conf is added by Lima, the tool that runs this lab VM; neither is on an installed Ubuntu Server. Two format rules catch people out. A comment must be a whole line starting with # or ;; text after a value becomes part of the value. And a key may contain a glob (net.ipv4.conf.*.rp_filter) that writes every matching interface, with -key excluding one key from the glob. grep shows who sets the network keys this lesson cares about:

deploy@web01 · Ubuntu 26.04 LTS
$ grep -rE 'rp_filter|redirects|source_route|martians|forward|syncookies|broadcasts' /usr/lib/sysctl.d/ /etc/sysctl.d/
/usr/lib/sysctl.d/55-network-security.conf:net.ipv4.conf.default.rp_filter=2 /usr/lib/sysctl.d/55-network-security.conf:net.ipv4.conf.all.rp_filter=2 /usr/lib/sysctl.d/50-default.conf:net.ipv4.conf.default.rp_filter = 2 /usr/lib/sysctl.d/50-default.conf:net.ipv4.conf.*.rp_filter = 2 /usr/lib/sysctl.d/50-default.conf:-net.ipv4.conf.all.rp_filter /usr/lib/sysctl.d/50-default.conf:net.ipv4.conf.default.accept_source_route = 0 /usr/lib/sysctl.d/50-default.conf:net.ipv4.conf.*.accept_source_route = 0 /usr/lib/sysctl.d/50-default.conf:-net.ipv4.conf.all.accept_source_route

50-default.conf is systemd's own file: it sets rp_filter = 2 on every interface through the glob, excludes all, and turns off source routing. Ubuntu's 55-network-security.conf then sets all and default to 2 as well. Nothing sets redirects, martian logging, forwarding or SYN cookies, so those keys run at the kernel's built-in values.

Measure the defaults on both platforms

Per-interface network keys come in three flavours. net.ipv4.conf.eth0.X is the value for one interface. net.ipv4.conf.default.X is the value a new interface gets when it appears. net.ipv4.conf.all.X is combined with each interface's own value by a rule that depends on the key. (eth0 is this VM's network card; physical servers usually have names such as enp1s0.)

How "all" combines with an interface (ip-sysctl documentation)
rp_filter: the higher of the two values wins. accept_redirects (on a host that does not forward), send_redirects and log_martians: on when either value is on. accept_source_route: on only when both are on. So a single line in all can switch on martian logging or switch off source routing, but it cannot switch off redirects or make one interface strict. The experiments below show the first two rules at work.
deploy@web01 · Ubuntu 26.04 LTS
$ sysctl -a -r 'net\.ipv4\.conf\.(all|default|eth0)\.(rp_filter|accept_redirects|secure_redirects|send_redirects|shared_media|accept_source_route|log_martians)$'
net.ipv4.conf.all.accept_redirects = 1 net.ipv4.conf.all.accept_source_route = 0 net.ipv4.conf.all.log_martians = 0 net.ipv4.conf.all.rp_filter = 2 net.ipv4.conf.all.secure_redirects = 1 net.ipv4.conf.all.send_redirects = 1 net.ipv4.conf.all.shared_media = 1 net.ipv4.conf.default.accept_redirects = 1 net.ipv4.conf.default.accept_source_route = 0 net.ipv4.conf.default.log_martians = 0 net.ipv4.conf.default.rp_filter = 2 net.ipv4.conf.default.secure_redirects = 1 net.ipv4.conf.default.send_redirects = 1 net.ipv4.conf.default.shared_media = 1 net.ipv4.conf.eth0.accept_redirects = 1 net.ipv4.conf.eth0.accept_source_route = 0 net.ipv4.conf.eth0.log_martians = 0 net.ipv4.conf.eth0.rp_filter = 2 net.ipv4.conf.eth0.secure_redirects = 1 net.ipv4.conf.eth0.send_redirects = 1 net.ipv4.conf.eth0.shared_media = 1
$ sysctl net.ipv4.ip_forward net.ipv6.conf.all.forwarding net.ipv4.tcp_syncookies net.ipv4.icmp_echo_ignore_broadcasts net.ipv4.icmp_ignore_bogus_error_responses net.ipv6.conf.eth0.accept_redirects
net.ipv4.ip_forward = 0 net.ipv6.conf.all.forwarding = 0 net.ipv4.tcp_syncookies = 1 net.ipv4.icmp_echo_ignore_broadcasts = 1 net.ipv4.icmp_ignore_bogus_error_responses = 1 net.ipv6.conf.eth0.accept_redirects = 1

On Ubuntu the host accepts ICMP redirects (accept_redirects = 1) and would send them, logs nothing about impossible source addresses (log_martians = 0), and runs reverse-path filtering in loose mode (2). shared_media = 1 is the kernel default too; it matters for redirects below. Forwarding is off, source routing is refused, SYN cookies are on, and broadcast pings and bogus ICMP errors are ignored. RHEL 10 ships the same values except for reverse-path filtering, where its 50-redhat.conf sets default and every interface to 1 (strict) and leaves all at the kernel default of 0.

deploy@rocky10 · Rocky Linux 10.2
$ grep -rE 'rp_filter|redirects|source_route|martians|forward|syncookies|broadcasts' /usr/lib/sysctl.d/ /etc/sysctl.d/ /etc/sysctl.conf
… /usr/lib/sysctl.d/50-redhat.conf:net.ipv4.conf.default.rp_filter = 1 /usr/lib/sysctl.d/50-redhat.conf:net.ipv4.conf.*.rp_filter = 1 /usr/lib/sysctl.d/50-redhat.conf:-net.ipv4.conf.all.rp_filter
$ sysctl -a -r 'net\.ipv4\.conf\.(all|default|eth0)\.(rp_filter|accept_redirects|secure_redirects|send_redirects|shared_media|accept_source_route|log_martians)$'
… net.ipv4.conf.all.rp_filter = 0 … net.ipv4.conf.default.rp_filter = 1 … net.ipv4.conf.eth0.rp_filter = 1 …

The keys worth changing are the redirect keys and log_martians on both platforms, and rp_filter on Ubuntu. The rest are defaults you can pin, so a later change (a package, a container runtime, a colleague's quick fix) is put back at boot and shows up as drift in a scan. The list of keys follows the CIS Benchmark (the CIS RHEL 10 Level 1 profile in scap-security-guide 0.1.82 checks every key in this lesson); the split into "change" and "pin" is SecOpsLog advice.

What the baseline file does on each platform
Changes on both
accept_redirects = 0
and secure_redirects = 0
send_redirects = 0
only routers send them
log_martians = 1
spoofed sources get logged
Changes on Ubuntu
rp_filter 2 to 1
RHEL: strict on eth0 already
Pins a default
ip_forward = 0
IPv4 and IPv6
accept_source_route = 0
already refused
syncookies, broadcasts = 1
already on
Measure first: only the left and middle columns change the running kernel. The right column keeps defaults from drifting.

Spoofed sources: rp_filter and martians

Nothing in a packet proves its source address. Reverse-path filtering checks it against the route the kernel would use to reply. In strict mode (1) the packet is dropped unless that reply would leave by the interface the packet came in on. In loose mode (2) it is dropped only if the source is unreachable through any interface. Loose mode stops little on a host with a default route, because almost every address is reachable somehow. A "martian" is a packet with an impossible source address, and log_martians = 1 writes one to the kernel log when it is dropped.

The lab tests this on a small network of network namespaces, away from the VM's own interface. A network namespace is a separate copy of the kernel's network stack with its own interfaces, routes and net.* sysctls; ip netns exec NAME command runs a command inside one. The server web has a public side (eth0, 192.0.2.10) and an internal network on eth1 (10.20.0.0/24). Another host on the public segment, out, can send packets that claim to come from the internal address 10.20.0.99.

/var/tmp/hard-sysctlnet-lab.sh
#!/bin/sh
# A small lab network in network namespaces: a server (web), its default
# router (gw), a second router (gw2), another host (out), and a switch (lan).
set -e
for n in web gw gw2 out lan; do ip netns add hard-sn-$n; ip -n hard-sn-$n link set lo up; done
ip -n hard-sn-lan link add br0 type bridge
ip -n hard-sn-lan link set br0 up
for n in web gw gw2 out; do
ip link add eth0 netns hard-sn-$n type veth peer name p-$n netns hard-sn-lan
ip -n hard-sn-lan link set p-$n master br0 up
ip -n hard-sn-$n link set eth0 up
done
# web: the server, with an internal network on eth1
ip -n hard-sn-web addr add 192.0.2.10/24 dev eth0
ip -n hard-sn-web link add eth1 type dummy
ip -n hard-sn-web addr add 10.20.0.10/24 dev eth1
ip -n hard-sn-web link set eth1 up
ip -n hard-sn-web route add default via 192.0.2.1
ip -n hard-sn-web route add 198.51.100.0/24 via 192.0.2.2
# gw: the default router; it knows 203.0.113.0/24 is behind gw2
ip -n hard-sn-gw addr add 192.0.2.1/24 dev eth0
ip netns exec hard-sn-gw sysctl -q -w net.ipv4.ip_forward=1
ip -n hard-sn-gw route add 203.0.113.0/24 via 192.0.2.2
# gw2: a second router that owns the partner and remote networks
ip -n hard-sn-gw2 addr add 192.0.2.2/24 dev eth0
ip -n hard-sn-gw2 link add dummy0 type dummy
ip -n hard-sn-gw2 link set dummy0 up
ip -n hard-sn-gw2 addr add 198.51.100.1/24 dev dummy0
for a in 1 2 3; do ip -n hard-sn-gw2 addr add 203.0.113.$a/32 dev dummy0; done
# out: another host on the segment, able to send from 10.20.0.99
ip -n hard-sn-out addr add 192.0.2.66/24 dev eth0
ip -n hard-sn-out addr add 10.20.0.99/32 dev lo
deploy@web01 · Ubuntu 26.04 LTS
$ sudo /var/tmp/hard-sysctlnet-lab.sh ip netns list
hard-sn-lan hard-sn-out hard-sn-gw2 hard-sn-gw hard-sn-web
$ sudo ip netns exec hard-sn-web sysctl -a -r 'net\.ipv4\.conf\.(all|default|eth0)\.rp_filter$'
net.ipv4.conf.all.rp_filter = 2 net.ipv4.conf.default.rp_filter = 2 net.ipv4.conf.eth0.rp_filter = 2

A new namespace inherits the host's IPv4 all and default values, so web starts with Ubuntu's loose mode everywhere. Now out pings the server's public address from the spoofed internal source. The pings get no reply (the server answers towards its internal network), so read the result on the server with nstat, which prints the kernel's protocol counters: IcmpInEchos counts pings accepted, TcpExtIPReversePathFilter counts packets dropped by reverse-path filtering.

deploy@web01 · Ubuntu 26.04 LTS
$ sudo ip netns exec hard-sn-out ping -c 2 -W 1 -I 10.20.0.99 192.0.2.10 | tail -n 2 sudo ip netns exec hard-sn-web nstat -asz IcmpInEchos TcpExtIPReversePathFilter
2 packets transmitted, 0 received, 100% packet loss, time 1006ms #kernel IcmpInEchos 2 0.0 TcpExtIPReversePathFilter 0 0.0

Both spoofed pings were accepted: 10.20.0.99 is reachable through eth1, and loose mode asks no more. Setting strict mode on the interface alone looks like the obvious fix. It changes nothing:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo ip netns exec hard-sn-web sysctl -w net.ipv4.conf.eth0.rp_filter=1 sudo ip netns exec hard-sn-out ping -c 2 -W 1 -I 10.20.0.99 192.0.2.10 | tail -n 2 sudo ip netns exec hard-sn-web nstat -asz IcmpInEchos TcpExtIPReversePathFilter
net.ipv4.conf.eth0.rp_filter = 1 2 packets transmitted, 0 received, 100% packet loss, time 1056ms #kernel IcmpInEchos 4 0.0 TcpExtIPReversePathFilter 0 0.0

The kernel uses the higher of conf.all.rp_filter and the interface's value, and on Ubuntu all is 2, so eth0 stays loose whatever you write to it. You cannot make any interface strict while all is 2. Set all to 1 as well (with martian logging on) and the next two spoofed pings are dropped, while a genuine ping from out's real address still works:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo ip netns exec hard-sn-web sysctl -w net.ipv4.conf.all.rp_filter=1 net.ipv4.conf.all.log_martians=1 sudo ip netns exec hard-sn-out ping -c 2 -W 1 -I 10.20.0.99 192.0.2.10 | tail -n 2 sudo ip netns exec hard-sn-web nstat -asz IcmpInEchos TcpExtIPReversePathFilter
net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.all.log_martians = 1 2 packets transmitted, 0 received, 100% packet loss, time 1049ms #kernel IcmpInEchos 4 0.0 TcpExtIPReversePathFilter 2 0.0
$ sudo ip netns exec hard-sn-out ping -c 2 -W 1 192.0.2.10 | tail -n 2
2 packets transmitted, 2 received, 0% packet loss, time 1008ms rtt min/avg/max/mdev = 0.139/0.590/1.042/0.451 ms

The drops are silent to the sender. log_martians makes them visible to you, in the kernel log that journalctl -k reads:

deploy@web01 · Ubuntu 26.04 LTS
$ journalctl -k --since -2min | grep 10.20.0.99
Sep 27 10:03:49 web01 kernel: IPv4: martian source (src=10.20.0.99, dst=192.0.2.10, dev=eth0) Sep 27 10:03:50 web01 kernel: IPv4: martian source (src=10.20.0.99, dst=192.0.2.10, dev=eth0)

The operational risk of strict mode is asymmetric routing: a host with two uplinks whose reply to some source legitimately leaves by the other interface. Strict mode drops it with no application error. The max rule tells you how to exempt one interface: with all = 1 you can set that interface to 2, and the higher value (loose) applies to it alone. RHEL's layout (all = 0, each interface 1) gets strict mode from the interface values; CIS still asks for all = 1, which changes nothing on eth0 there. Martian logging is rate-limited, but a noisy segment can still add steady journal lines.

Redirects, forwarding and the pinned defaults

An ICMP redirect is a message from a router saying "for that destination, use this other gateway on your network". On a server it is an instruction any host that can fake the router's address may try to send, and a host that obeys can have its traffic steered through a machine of the attacker's choosing. secure_redirects = 1 looks like a limit (accept only redirects to gateways the host already uses), but ip-sysctl documents it as overridden by shared_media, which is 1 by default, as the values above show, so on a default host it narrows nothing. accept_redirects = 0 is the control that refuses them; the file below also sets secure_redirects = 0 because the CIS profile checks it.

The lab has a real redirect to test with. The server's default router gw knows that 203.0.113.0/24 is behind the second router gw2 on the same segment, so when web sends there through gw, gw forwards the packet and sends web a redirect. ip route get shows whether web took it (cache <redirected> and via 192.0.2.2 mean it did), and nstat counts the redirects that arrived.

deploy@web01 · Ubuntu 26.04 LTS
$ sudo ip netns exec hard-sn-web sysctl net.ipv4.conf.all.accept_redirects net.ipv4.conf.eth0.accept_redirects sudo ip netns exec hard-sn-web ping -c 2 -i 0.5 -W 1 203.0.113.1 | tail -n 2 sudo ip netns exec hard-sn-web ip route get 203.0.113.1 sudo ip netns exec hard-sn-web nstat -asz IcmpInRedirects
net.ipv4.conf.all.accept_redirects = 1 net.ipv4.conf.eth0.accept_redirects = 1 2 packets transmitted, 2 received, 0% packet loss, time 515ms rtt min/avg/max/mdev = 0.326/0.515/0.704/0.189 ms 203.0.113.1 via 192.0.2.2 dev eth0 src 192.0.2.10 uid 0 cache <redirected> expires 299sec #kernel IcmpInRedirects 1 0.0

For a host that does not forward, the kernel accepts redirects on an interface when either all or the interface's own value is 1. Setting only all to 0, as many hardening lists do, leaves eth0 at 1, and the next redirect is still taken:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo ip netns exec hard-sn-web sysctl -w net.ipv4.conf.all.accept_redirects=0 sudo ip netns exec hard-sn-web ping -c 2 -i 0.5 -W 1 203.0.113.2 | tail -n 2 sudo ip netns exec hard-sn-web ip route get 203.0.113.2 sudo ip netns exec hard-sn-web nstat -asz IcmpInRedirects
net.ipv4.conf.all.accept_redirects = 0 2 packets transmitted, 2 received, 0% packet loss, time 503ms rtt min/avg/max/mdev = 0.050/0.274/0.498/0.224 ms 203.0.113.2 via 192.0.2.2 dev eth0 src 192.0.2.10 uid 0 cache <redirected> expires 299sec #kernel IcmpInRedirects 2 0.0

Only when the interface value is 0 too is the redirect ignored. Redirects still arrive (the counter keeps rising, because both pings now go through gw), but the route stays on the default gateway:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo ip netns exec hard-sn-web sysctl -w net.ipv4.conf.eth0.accept_redirects=0 sudo ip netns exec hard-sn-web ping -c 2 -i 0.5 -W 1 203.0.113.3 | tail -n 2 sudo ip netns exec hard-sn-web ip route get 203.0.113.3 sudo ip netns exec hard-sn-web nstat -asz IcmpInRedirects
net.ipv4.conf.eth0.accept_redirects = 0 2 packets transmitted, 2 received, 0% packet loss, time 510ms rtt min/avg/max/mdev = 0.164/0.264/0.364/0.100 ms 203.0.113.3 via 192.0.2.1 dev eth0 src 192.0.2.10 uid 0 cache #kernel IcmpInRedirects 4 0.0

That is why the file below sets all, default and a glob over every existing interface; log_martians, an either-or key being turned on, needs only all. A host sends redirects only while it forwards, so send_redirects = 0 matters once something turns forwarding on. For IPv6, the kernel reads each interface's accept_redirects, which is 1 on both platforms, so the glob covers it too. Impact: none on an ordinary network; a host that relied on redirects needs a correct route instead.

Forwarding is the pivot control. ip_forward = 0 means a packet arriving on one interface is never sent out of another, so a compromised web server cannot become a router into the internal network behind it. Container runtimes and VPN servers set it to 1 because they route for their containers or clients; do not install this file on a container host, a router or a VPN gateway. The ip-sysctl documentation also notes that changing ip_forward resets the per-interface settings to host or router defaults, so check the redirect keys again after anything toggles it.

The pinned keys need only a sentence each. accept_source_route = 0 refuses packets that carry their own route, and the default all = 0 already refuses them everywhere (an AND key). tcp_syncookies = 1 lets the kernel answer a SYN flood without keeping state for each half-open connection; the kernel documentation calls it a fallback, not a way to carry legitimate load. icmp_echo_ignore_broadcasts = 1 stops the host answering pings sent to a broadcast address, which would make it an amplifier. None of this needs IPv6 disabled. The CIS profile also asks for accept_ra = 0; on a host that takes its IPv6 address from router advertisements that breaks IPv6, and on Ubuntu systemd-networkd already handles them itself (the kernel's value on eth0 is 0).

Persist, verify and roll back

Save the current values before changing anything. sysctl prints key = value lines, which is also the format sysctl -p reads, so the snapshot is the rollback.

deploy@web01 · Ubuntu 26.04 LTS
$ sysctl -a -r 'net\.ipv4\.conf\.(all|default|eth0|lo)\.(rp_filter|accept_redirects|secure_redirects|send_redirects|log_martians)$|net\.ipv6\.conf\.(all|default|eth0|lo)\.accept_redirects$' > ~/net-sysctl-before.conf wc -l ~/net-sysctl-before.conf
24 /home/deploy/net-sysctl-before.conf

Then write the file. The name 60-secopslog-network.conf sorts after Ubuntu's 55-network-security.conf and RHEL's 50-redhat.conf, so its values win. Each key is marked as changing or pinning a default, and every comment is on its own line.

/etc/sysctl.d/60-secopslog-network.conf
# SecOpsLog network baseline for a host that does not route packets.
# CHANGES marks a key that differs from the Ubuntu 26.04 / RHEL 10 default;
# PINS marks a default that is kept here so drift is caught and undone.
# Comments must stay on their own lines: sysctl.d has no trailing comments.
# PINS: no forwarding between interfaces (IPv4 and IPv6).
net.ipv4.ip_forward = 0
net.ipv6.conf.all.forwarding = 0
# CHANGES: ignore ICMP redirects. A host accepts one when either "all" or
# the interface allows it, so the glob sets every interface as well.
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.*.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv4.conf.*.secure_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
net.ipv6.conf.*.accept_redirects = 0
# CHANGES: never send redirects (only a router has a reason to).
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.*.send_redirects = 0
# PINS: refuse source-routed packets.
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
# CHANGES on Ubuntu (2, loose): strict reverse-path filtering. The kernel
# uses the higher of "all" and the interface, so all three are set.
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.*.rp_filter = 1
# CHANGES: log packets with impossible source addresses.
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
# PINS: SYN cookies, and no replies to broadcast pings or bogus ICMP errors.
net.ipv4.tcp_syncookies = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1

sudo sysctl --system applies every file in the same order boot does and prints each file name, then each value it writes. The order is the point: your file is applied after 55-network-security.conf sets all.rp_filter = 2, so 1 is what stays. The glob lines expand to all, default, eth0 and lo.

deploy@web01 · Ubuntu 26.04 LTS
$ sudo sysctl --system
… * Applying /usr/lib/sysctl.d/55-network-security.conf ... * Applying /usr/lib/sysctl.d/55-ptrace.conf ... * Applying /usr/lib/sysctl.d/55-zeropage.conf ... * Applying /etc/sysctl.d/60-secopslog-network.conf ... * Applying /etc/sysctl.d/99-cloudimg-ipv6.conf ... * Applying /etc/sysctl.d/99-lima.conf ... … kernel.sysrq = 176 … net.ipv4.conf.default.rp_filter = 2 net.ipv4.conf.all.rp_filter = 2 … net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.default.accept_redirects = 0 net.ipv4.conf.eth0.accept_redirects = 0 net.ipv4.conf.lo.accept_redirects = 0 … net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.default.rp_filter = 1 net.ipv4.conf.eth0.rp_filter = 1 net.ipv4.conf.lo.rp_filter = 1 …

Verify by reading the keys back, including an interface, since the interface value is what redirect handling uses. eth0.log_martians still reads 0, which is fine: for that key, all = 1 is enough.

deploy@web01 · Ubuntu 26.04 LTS
$ sysctl -a -r 'net\.ipv4\.conf\.(all|default|eth0)\.(rp_filter|accept_redirects|send_redirects|log_martians)$'
net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.all.log_martians = 1 net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.all.send_redirects = 0 net.ipv4.conf.default.accept_redirects = 0 net.ipv4.conf.default.log_martians = 1 net.ipv4.conf.default.rp_filter = 1 net.ipv4.conf.default.send_redirects = 0 net.ipv4.conf.eth0.accept_redirects = 0 net.ipv4.conf.eth0.log_martians = 0 net.ipv4.conf.eth0.rp_filter = 1 net.ipv4.conf.eth0.send_redirects = 0

sysctl --system is the procps tool; at boot the same files are applied by systemd-sysctl, which also gives an explicit key priority over a glob and reapplies the interface keys to every interface that appears later. systemd-analyze cat-config sysctl.d prints the files in the order boot reads them, and restarting systemd-sysctl runs the boot path now, as the next reboot will.

deploy@web01 · Ubuntu 26.04 LTS
$ systemd-analyze cat-config sysctl.d | grep -E "^# /|rp_filter"
… # /usr/lib/sysctl.d/50-default.conf net.ipv4.conf.default.rp_filter = 2 net.ipv4.conf.*.rp_filter = 2 -net.ipv4.conf.all.rp_filter … # /usr/lib/sysctl.d/55-network-security.conf net.ipv4.conf.default.rp_filter=2 net.ipv4.conf.all.rp_filter=2 … # /etc/sysctl.d/60-secopslog-network.conf net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.default.rp_filter = 1 net.ipv4.conf.*.rp_filter = 1 …
$ sudo systemctl restart systemd-sysctl sysctl net.ipv4.conf.all.rp_filter net.ipv4.conf.eth0.rp_filter net.ipv4.conf.eth0.accept_redirects
net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.eth0.rp_filter = 1 net.ipv4.conf.eth0.accept_redirects = 0

Rollback needs care. Deleting the file and re-running sysctl --system restores only the keys some other file still sets. rp_filter goes back to 2 because Ubuntu's files set it; redirects and martian logging stay as your file left them, because no file sets them and the kernel keeps its running values until reboot.

deploy@web01 · Ubuntu 26.04 LTS
$ sudo rm /etc/sysctl.d/60-secopslog-network.conf sudo sysctl --system >/dev/null sysctl net.ipv4.conf.eth0.rp_filter net.ipv4.conf.eth0.accept_redirects net.ipv4.conf.all.log_martians
net.ipv4.conf.eth0.rp_filter = 2 net.ipv4.conf.eth0.accept_redirects = 0 net.ipv4.conf.all.log_martians = 1

Load the snapshot to put every value back:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo sysctl -p ~/net-sysctl-before.conf | head -n 3 sysctl net.ipv4.conf.eth0.accept_redirects net.ipv4.conf.all.log_martians
net.ipv4.conf.all.accept_redirects = 1 net.ipv4.conf.all.log_martians = 0 net.ipv4.conf.all.rp_filter = 2 net.ipv4.conf.eth0.accept_redirects = 1 net.ipv4.conf.all.log_martians = 0

On RHEL 10 the method is identical: sysctl --system and systemd-sysctl read the same directories, and /etc/sysctl.d/99-sysctl.conf is a symlink to /etc/sysctl.conf, which is still read there. The file above changes redirects and martian logging, and sets all.rp_filter to 1, which RHEL's interfaces already use.

Try this

Build the lab network with the script above, then make web strict with sudo ip netns exec hard-sn-web sysctl -w net.ipv4.conf.all.rp_filter=1 net.ipv4.conf.eth0.rp_filter=1. Break it by adding an asymmetric route: sudo ip -n hard-sn-web route add 192.0.2.66/32 dev eth1, so replies to out's real address leave by the wrong interface. Ping web from out (sudo ip netns exec hard-sn-out ping -c 2 -W 1 192.0.2.10) and read nstat -asz IcmpInEchos TcpExtIPReversePathFilter inside web: the reverse-path counter rises by 2 and IcmpInEchos does not. Repair it for that interface only with net.ipv4.conf.eth0.rp_filter=2, ping again, and confirm IcmpInEchos rises while the drop counter stays put (the pings still get no reply, because eth1 leads nowhere in the lab). Remove the namespaces with sudo ip netns del for each hard-sn-* name when you finish.

Takeaway

Read the defaults before you write a sysctl file, change only the keys that differ, and set per-interface keys through all, default and a glob because the kernel combines them differently for each key. Snapshot the running values first, since deleting the file does not undo what it set.

Quick check
01On Ubuntu 26.04 you add net.ipv4.conf.eth0.rp_filter = 1 in a sysctl.d file to make the public interface strict, and a scan still shows spoofed packets being accepted on eth0. What went wrong?
Incorrect — Sorting earlier would make it lose: the file that sorts last wins. The problem is the combining rule, not the order.
Incorrect — The interface value was written; sysctl reads back 1. It is ignored because of the value in all, not because eth0 existed first.
Correct — With conf.all.rp_filter at 2, max(2, 1) is 2 on every interface; all must be 1 (or 0) before an interface can be strict.
Incorrect — log_martians only adds a log line. Filtering happens without it; here nothing is being filtered at all.
02A hardening list sets net.ipv4.conf.all.accept_redirects = 0 and default = 0 on a running server. An hour later the server still follows an ICMP redirect on eth0. Why?
Incorrect — secure_redirects never forces redirects on, and with shared_media at its default of 1 it is not even consulted. The interface's own accept_redirects is what remains.
Correct — default only affects interfaces created later; eth0 keeps 1, and the OR rule means all = 0 alone refuses nothing.
Incorrect — The kernel processes ICMP redirects itself; the lab shows the change taking effect immediately once eth0 is 0.
Incorrect — Possible in general, but the lab shows an IPv4 redirect still accepted with all = 0; the IPv4 interface value is the cause.
03You remove /etc/sysctl.d/60-secopslog-network.conf and run sudo sysctl --system to roll back. rp_filter returns to 2, but accept_redirects on eth0 is still 0. What is the correct rollback?
Incorrect — systemd-sysctl only writes what the remaining files say; like sysctl --system, it cannot know a value no file mentions.
Incorrect — Running values persist until they are written again or the host reboots; there is no timer.
Incorrect — That pins the old value forever and hides the rollback in a new file; restoring the measured values is the rollback.
Correct — Only keys another file still sets are restored by sysctl --system; the snapshot restores the rest, and a reboot returns to the files' values.

Related