Самая частая жалоба после раскатки hardening-конфига — «в файле 1, а в ядре 0». Почти всегда причина в одном из трёх мест, и я проверяю их именно в таком порядке.
Первое — очередность файлов. При загрузке параметры выставляет systemd-sysctl.service, он читает *.conf из /etc/sysctl.d/, /run/sysctl.d/, /usr/local/lib/sysctl.d/ и /usr/lib/sysctl.d/. Файл в /etc/ с тем же именем целиком заменяет вендорский. Все файлы сортируются по имени лексикографически, независимо от каталога, и при конфликте побеждает значение из файла, который идёт позже. Документация systemd советует для своих файлов в /etc/ префиксы 60–90, для пакетов в /usr/ — 10–40. Ручная команда sysctl --system проходит те же каталоги, но /etc/sysctl.conf читает последним: старое значение, забытое в нём, перекроет любой файл из sysctl.d.
# где ещё задан параметр
grep -rn "rp_filter" /etc/sysctl.conf /etc/sysctl.d/ /run/sysctl.d/ \
/usr/local/lib/sysctl.d/ /usr/lib/sysctl.d/ 2>/dev/null
# перечитать все файлы в том же порядке, что при загрузке
sysctl --system
# что реально стоит в ядре
sysctl net.ipv4.conf.all.rp_filter net.ipv4.conf.eth0.rp_filter
Второе — модули ядра. Часть ключей появляется только после загрузки модуля: например, net.bridge.* — после br_netfilter. Если модуль грузится позже systemd-sysctl, ключа на момент применения просто нет, а запись в отсутствующий ключ логируется на уровне debug и сервис не падает. Справка sysctl.d(5) предлагает два решения: udev-правило на появление модуля или файл в /etc/modules-load.d/, чтобы модуль загружался раньше. Параметры интерфейсов (net.ipv4.conf.*, net.ipv6.conf.*) systemd применяет отдельно для каждого интерфейса по мере его появления.
Третье — путаница all, default и имени интерфейса. Ветка default задаёт значения для интерфейсов, которые будут созданы после, а на уже поднятые не влияет. Правила объединения у каждого ключа свои: для rp_filter ядро берёт максимум из conf/all и conf/<интерфейс>; log_martians включён, если единица стоит хотя бы в одном из них; accept_redirects на хосте без форвардинга остаётся включённым, если он TRUE хотя бы в одном месте. Поэтому редиректы я выключаю и в all, и в default, а проверяю на конкретном интерфейсе, а не на all.
Отдельная ловушка — Docker. Он при старте сам включает net.ipv4.ip_forward и net.ipv6.conf.all.forwarding, если они выключены, и ставит политику DROP для пересылаемых пакетов. На хосте с контейнерами строка net.ipv4.ip_forward = 0 из общего конфига бессмысленна: я убираю её из шаблона для таких серверов, чтобы проверка не показывала ложное расхождение.