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:
1 parent
804fee4ffa
commit
72367682b5
5 files changed
+122
-1
No files matched your search
@@ -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).
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`.
|
||||||
Reference in new issue
Block a user