Harden frr_anycast against unattended-upgrade-triggered route flush

Prod-инцидент: apt-daily-upgrade обновил libc6 -> postinst вызвал
рестарт зависимых юнитов -> перезапустился systemd-networkd -> он
вычистил из ядра BGP-маршруты zebra (KeepConfiguration не был задан,
networkd по умолчанию удаляет "чужие" маршруты при старте). Zebra не
знала о вычистке, трафик встал до ручного restart frr. Повторилось
10 сентября с обновлением systemd, но было замаскировано плейбуком.

Роль frr_anycast теперь защищает от этого класса сбоя безусловно:
- отключены apt-daily/apt-daily-upgrade (20auto-upgrades + маскирование
  таймеров и сервисов) - обновления пакетов только вручную;
- systemd-networkd получает drop-in KeepConfiguration=yes - устраняет
  сам механизм вычистки маршрутов при любом будущем рестарте networkd,
  не только от конкретного пакета-триггера;
- новый handler Restart systemd-networkd между Apply netplan и Restart
  frr (networkd должен переустановить владение интерфейсами до того,
  как zebra переустановит маршруты).

Шаблоны FRR/nftables не затронуты: render-check.sh 6/6,
render-check-cloud.sh 84/84, без изменений.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bc3HkMiDwooSyLBLoMKNyF
This commit is contained in:
ayurishchevandClaude Sonnet 5 committed 2026-09-11 13:09:17 +03:00
1 parent 804fee4ffa
commit 72367682b5
5 files changed
+122 -1

No files matched your search

+9
View File
@@ -165,3 +165,12 @@ ansible/tests/render-check-cloud.sh # 4 соседа, prepend только н
`render-check-cloud.sh` проверяет: у каждого из 4 узлов ровно 4 `neighbor ... remote-as`, outbound `route-map` навешан на всех — `RM-OUT-PREPEND` (фильтр + `set as-path prepend 65200 65200 65200`) на 2×65103, `RM-OUT` (только фильтр) на 2×65104, `nftables.conf` содержит DNAT (`dnat to 95.163.53.117`) и `masquerade` на заданном внешнем интерфейсе, а также (если `nft` доступен на control node) валидирует синтаксис каждого `nftables.conf` через `nft -c -f`. `render-check-cloud.sh` проверяет: у каждого из 4 узлов ровно 4 `neighbor ... remote-as`, outbound `route-map` навешан на всех — `RM-OUT-PREPEND` (фильтр + `set as-path prepend 65200 65200 65200`) на 2×65103, `RM-OUT` (только фильтр) на 2×65104, `nftables.conf` содержит DNAT (`dnat to 95.163.53.117`) и `masquerade` на заданном внешнем интерфейсе, а также (если `nft` доступен на control node) валидирует синтаксис каждого `nftables.conf` через `nft -c -f`.
Детали и разбор диаграммы: [docs/2026-09-09-ansible-site-cloud-plan.md](docs/2026-09-09-ansible-site-cloud-plan.md) / [docs/2026-09-09-ansible-site-cloud-summary.md](docs/2026-09-09-ansible-site-cloud-summary.md). Детали и разбор диаграммы: [docs/2026-09-09-ansible-site-cloud-plan.md](docs/2026-09-09-ansible-site-cloud-plan.md) / [docs/2026-09-09-ansible-site-cloud-summary.md](docs/2026-09-09-ansible-site-cloud-summary.md).
## Защита от auto-update-триггеров и networkd route-flush
Прод-инцидент: `apt-daily-upgrade` обновил `libc6` → postinst вызвал рестарт зависимых юнитов → перезапустился `systemd-networkd` → он вычистил из ядра BGP-маршруты zebra (не «свои» для него) → трафик встал до ручного `systemctl restart frr`. Роль `frr_anycast` теперь защищает от этого класса сбоя безусловно, на всех узлах обеих сред:
- **Обновления только по требованию администратора**: `/etc/apt/apt.conf.d/20auto-upgrades` отключает периодические `apt update`/unattended-upgrade, дополнительно замаскированы `apt-daily.timer`/`apt-daily-upgrade.timer` и их `.service`. `apt upgrade` по-прежнему доступен вручную.
- **`systemd-networkd` не вычищает чужие маршруты**: drop-in `/etc/systemd/networkd.conf.d/99-keep-configuration.conf` (`KeepConfiguration=yes`) — защита не от конкретного пакета-триггера, а от самого механизма, на случай **любого** будущего рестарта networkd.
Не влияет на рендер `frr.conf`/`daemons`/`nftables.conf` — `render-check.sh`/`render-check-cloud.sh` проходят без изменений. Детали: [docs/2026-09-11-system-hardening-plan.md](docs/2026-09-11-system-hardening-plan.md) / [docs/2026-09-11-system-hardening-summary.md](docs/2026-09-11-system-hardening-summary.md).
+9 -1
View File
@@ -2,11 +2,19 @@
# Порядок важен: Ansible исполняет handlers в порядке ИХ ОБЪЯВЛЕНИЯ здесь, # Порядок важен: Ansible исполняет handlers в порядке ИХ ОБЪЯВЛЕНИЯ здесь,
# а не в порядке notify. "Apply netplan" должен отработать раньше "Restart # а не в порядке notify. "Apply netplan" должен отработать раньше "Restart
# frr", иначе dummy0 (и anycast-адрес на нём) ещё не будет существовать к # frr", иначе dummy0 (и anycast-адрес на нём) ещё не будет существовать к
# моменту старта zebra/bgpd. # моменту старта zebra/bgpd. "Restart systemd-networkd" — аналогично раньше
# "Restart frr": networkd должен перечитать KeepConfiguration=yes и (пере)стать
# владельцем интерфейсов ДО того, как zebra переустановит BGP-маршруты в ядро
# — иначе есть краткое окно, где свежий networkd ещё может вычистить их.
- name: Apply netplan - name: Apply netplan
ansible.builtin.command: netplan apply ansible.builtin.command: netplan apply
changed_when: true changed_when: true
- name: Restart systemd-networkd
ansible.builtin.systemd:
name: systemd-networkd
state: restarted
- name: Restart frr - name: Restart frr
ansible.builtin.systemd: ansible.builtin.systemd:
name: frr name: frr
+43
View File
@@ -34,6 +34,49 @@
success_msg: "DNAT на {{ dnat_target_ip }} будет применён (enable_snat: true)." success_msg: "DNAT на {{ dnat_target_ip }} будет применён (enable_snat: true)."
when: dnat_target_ip | length > 0 when: dnat_target_ip | length > 0
- name: Auto-update защита | отключить периодический apt (apt-daily/unattended-upgrades)
ansible.builtin.copy:
dest: /etc/apt/apt.conf.d/20auto-upgrades
content: |
APT::Periodic::Update-Package-Lists "0";
APT::Periodic::Unattended-Upgrade "0";
owner: root
group: root
mode: "0644"
- name: Auto-update защита | замаскировать apt-daily таймеры/сервисы
ansible.builtin.systemd:
name: "{{ item }}"
masked: true
state: stopped
enabled: false
loop:
- apt-daily.timer
- apt-daily-upgrade.timer
- apt-daily.service
- apt-daily-upgrade.service
- name: Сеть | каталог для networkd drop-in существует
ansible.builtin.file:
path: /etc/systemd/networkd.conf.d
state: directory
owner: root
group: root
mode: "0755"
- name: Сеть | systemd-networkd не должен вычищать маршруты, поставленные не им (zebra)
ansible.builtin.copy:
dest: /etc/systemd/networkd.conf.d/99-keep-configuration.conf
content: |
[Network]
KeepConfiguration=yes
owner: root
group: root
mode: "0644"
notify:
- Restart systemd-networkd
- Restart frr
- name: FRR APT-репозиторий | добавить GPG-ключ - name: FRR APT-репозиторий | добавить GPG-ключ
ansible.builtin.get_url: ansible.builtin.get_url:
url: https://deb.frrouting.org/frr/keys.gpg url: https://deb.frrouting.org/frr/keys.gpg
+25
View File
@@ -0,0 +1,25 @@
# План внедрения: защита от auto-update-триггеров и networkd route-flush
**Дата:** 2026-09-11
## Инцидент
`apt-daily-upgrade.service` обновил `libc6` → postinst вызвал `daemon-reexec`/рестарт зависимых юнитов → перезапустился `systemd-networkd`. Networkd по умолчанию (`KeepConfiguration` не задан) при старте вычищает на управляемых интерфейсах маршруты, которые не совпадают с его конфигом — «чужие» для него BGP-маршруты zebra. Zebra не знала о вычистке (её RIB не менялся), кернел-FIB был пуст до ручного `systemctl restart frr`. Аналогичный триггер сработал повторно 10 сентября (обновление `systemd`), но был замаскирован плейбуком, перезапускавшим FRR следом.
Вывод пользователя: триггером может стать обновление **любого** пакета, тянущего рестарт networkd — защита должна быть не от конкретного пакета, а структурная.
## Решение (роль `frr_anycast`, применяется безусловно на всех узлах)
1. **Отключить автоматические/unattended обновления**:
- `/etc/apt/apt.conf.d/20auto-upgrades` → `Update-Package-Lists "0"`, `Unattended-Upgrade "0"`.
- Замаскировать `apt-daily.timer`, `apt-daily-upgrade.timer` и их `.service` — двойная защита, даже если конфиг-файл будет вручную изменён.
- Обновления пакетов остаются возможны, но только явным `apt update && apt upgrade`, инициированным администратором.
2. **`systemd-networkd` не должен вычищать маршруты, поставленные не им** — drop-in `/etc/systemd/networkd.conf.d/99-keep-configuration.conf` с `KeepConfiguration=yes`. Защищает от той же категории сбоя при **любом** будущем рестарте networkd, независимо от причины.
3. Handler-цепочка при изменении networkd-конфига: `Restart systemd-networkd` → `Restart frr` (та же логика, что и ручной remediation 08:57 — зебра переустанавливает маршруты после того, как networkd перечитал конфиг).
## Definition of done
- Задачи добавлены в `roles/frr_anycast/tasks/main.yml`, handler — в `handlers/main.yml` (порядок: после `Apply netplan`, перед `Restart frr`).
- `frr.conf.j2`/`daemons.j2`/`nftables.conf.j2` не затронуты — `render-check.sh`/`render-check-cloud.sh` проходят без изменений (регресс-проверка).
- `--syntax-check` чист для обоих playbook'ов.
- README и summary обновлены.
@@ -0,0 +1,36 @@
# Summary: защита от auto-update-триггеров и networkd route-flush
**Дата:** 2026-09-11
**План:** [2026-09-11-system-hardening-plan.md](2026-09-11-system-hardening-plan.md)
## Разбор инцидента
`apt-daily-upgrade.service` обновил `libc6` → postinst-скрипт вызвал `daemon-reexec`/рестарт зависимых юнитов → перезапустился `systemd-networkd`. Networkd по умолчанию (`KeepConfiguration` не задан) при старте вычищает на управляемых интерфейсах маршруты, которые не совпадают с его собственным конфигом — «чужие», включая BGP-маршруты, установленные zebra через netlink. Zebra о вычистке не узнавала (её внутренняя RIB не менялась) — маршруты в кернел-FIB отсутствовали до ручного `systemctl restart frr` в 08:57. Повторный триггер 10 сентября (обновление `systemd`) был замаскирован тем, что следом отработал плейбук и перезапустил FRR сам. Ключевой вывод (подтверждён): триггером может стать обновление любого пакета, тянущего рестарт networkd — защита нужна структурная, не «дождаться конкретного пакета».
## Что сделано
В `roles/frr_anycast/tasks/main.yml` добавлены безусловные задачи (применяются на всех узлах обеих сред):
1. Отключение периодического apt (`/etc/apt/apt.conf.d/20auto-upgrades` → `Update-Package-Lists "0"`, `Unattended-Upgrade "0"`) + маскирование `apt-daily.timer`/`apt-daily-upgrade.timer`/их `.service` — двойная защита от повторного включения.
2. `systemd-networkd` drop-in `/etc/systemd/networkd.conf.d/99-keep-configuration.conf` с `KeepConfiguration=yes` — устраняет сам механизм вычистки маршрутов при рестарте networkd, независимо от причины рестарта.
3. Новый handler `Restart systemd-networkd` в `handlers/main.yml`, размещён между `Apply netplan` и `Restart frr` (порядок важен — networkd должен перечитать `KeepConfiguration` и переустановить владение интерфейсами до того, как zebra переустанавливает маршруты).
Обновления пакетов остаются возможны, но только явным ручным `apt update && apt upgrade` — автоматический/unattended путь полностью исключён.
## Результаты проверки
- `--syntax-check` — OK для `site-a.yml` и `site-cloud.yml`.
- `render-check.sh` (площадка A) — 6/6, без изменений.
- `render-check-cloud.sh` (site-cloud) — 84/84, без изменений.
Новые задачи не трогают шаблоны FRR/nftables, поэтому регресс-тесты рендера не затронуты — это дополнение к системной защите ОС, а не к BGP-конфигурации.
## Известное ограничение
Реальный apply на живых ВМ не выполнялся (ВМ не подняты, как и в предыдущих изменениях этой сессии) — эффект `KeepConfiguration=yes` и маскирования таймеров проверен только синтаксически/по документации модулей, не на реальном перезапуске networkd.
## Изменённые файлы
- `ansible/roles/frr_anycast/tasks/main.yml`, `ansible/roles/frr_anycast/handlers/main.yml`.
- `README.md` — новый раздел «Защита от auto-update-триггеров и networkd route-flush».
- `docs/2026-09-11-system-hardening-plan.md`, `docs/2026-09-11-system-hardening-summary.md`.