Fix BGP route leak: mandatory outbound prefix-filter on every neighbor
Prod-инцидент: FRR реэкспортировал входящий 10.0.0.0/8 обратно
eBGP-соседям. Причина — ни у одного соседа не было outbound-фильтра
(no bgp ebgp-requires-policy отключал защиту FRR, единственный
route-map RM-PREPEND-OUT permit-all фильтрации не делал).
frr.conf.j2: добавлен ip prefix-list PL-ANYCAST-OUT (permit только
anycast_prefix) и route-map RM-OUT / RM-OUT-PREPEND (фильтр, либо
фильтр+prepend), оба с явным deny в конце. Назначены outbound на
ВСЕХ соседей без исключения — наружу теперь уходит исключительно
анонсируемый anycast-префикс, что бы ни попало в локальную
BGP-таблицу от других соседей.
Уязвимость структурная (в общей роли), поэтому фикс задел и
площадку A — golden-файлы frr/a{1,2,3}/frr.conf синхронизированы
с новым рендером (намеренно, не регресс). no bgp ebgp-requires-policy
сознательно не тронут — его включение требует ещё inbound-policy на
каждого соседа, отдельная задача.
render-check.sh 6/6, render-check-cloud.sh 84/84 (включая nft -c).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bc3HkMiDwooSyLBLoMKNyF
This commit is contained in:
1 parent
1e287ee81c
commit
804fee4ffa
8 files changed
+113
-9
No files matched your search
@@ -91,6 +91,8 @@ docker compose exec -T c1 curl -s http://10.200.200.1/
|
||||
|
||||
Роль `frr_anycast` обобщена: список BGP-соседей узла — не 2 жёстко именованных (`neighbor_b1`/`neighbor_b2`), а `bgp_neighbors` произвольной длины, каждый с флагом `prepend` (as-path prepend на исходящих анонсах). Опционально (`enable_snat: true`) роль также поднимает sNAT через `nftables` (masquerade на внешнем интерфейсе) — оба узла ниже используют эту одну и ту же роль с разными `group_vars`.
|
||||
|
||||
**Outbound-фильтр обязателен у всех соседей.** Каждому соседу назначен outbound route-map (`RM-OUT` или, если `prepend: true`, `RM-OUT-PREPEND`), матчащий `ip prefix-list PL-ANYCAST-OUT` (permit только `anycast_prefix`) с явным `deny` в конце — наружу уходит исключительно анонсируемый anycast-префикс, что бы ни попало в локальную BGP-таблицу от других соседей. Это фикс реального прод-инцидента: без фильтра FRR реэкспортировал случайно полученный `10.0.0.0/8` обратно в сторону других eBGP-соседей (см. [docs/2026-09-10-bgp-outbound-filter-summary.md](docs/2026-09-10-bgp-outbound-filter-summary.md)).
|
||||
|
||||
Детали: [docs/2026-09-03-ansible-site-a-plan.md](docs/2026-09-03-ansible-site-a-plan.md) / [docs/2026-09-03-ansible-site-a-summary.md](docs/2026-09-03-ansible-site-a-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) — доработка под второе окружение (см. ниже).
|
||||
|
||||
### Структура
|
||||
@@ -160,6 +162,6 @@ ansible/tests/render-check.sh # регресс: площадка A ре
|
||||
ansible/tests/render-check-cloud.sh # 4 соседа, prepend только на резервном канале, sNAT
|
||||
```
|
||||
|
||||
`render-check-cloud.sh` проверяет: у каждого из 4 узлов ровно 4 `neighbor ... remote-as` (2×65104 без route-map, 2×65103 с `route-map RM-PREPEND-OUT out`), `route-map` содержит `set as-path prepend 65200 65200 65200`, `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).
|
||||
@@ -4,10 +4,20 @@ hostname {{ inventory_hostname }}
|
||||
log stdout informational
|
||||
no ipv6 forwarding
|
||||
!
|
||||
ip prefix-list PL-ANYCAST-OUT seq 5 permit {{ anycast_prefix }}
|
||||
!
|
||||
route-map RM-OUT permit 10
|
||||
match ip address prefix-list PL-ANYCAST-OUT
|
||||
!
|
||||
route-map RM-OUT deny 20
|
||||
!
|
||||
{% if bgp_neighbors | selectattr('prepend') | list | length > 0 %}
|
||||
route-map RM-PREPEND-OUT permit 10
|
||||
route-map RM-OUT-PREPEND permit 10
|
||||
match ip address prefix-list PL-ANYCAST-OUT
|
||||
set as-path prepend {{ ((local_asn ~ ' ') * (as_path_prepend_count | default(0) | int)) | trim }}
|
||||
!
|
||||
route-map RM-OUT-PREPEND deny 20
|
||||
!
|
||||
{% endif %}
|
||||
router bgp {{ local_asn }}
|
||||
bgp router-id {{ router_id }}
|
||||
@@ -21,8 +31,8 @@ router bgp {{ local_asn }}
|
||||
{% for n in bgp_neighbors %}
|
||||
neighbor {{ n.ip }} activate
|
||||
{% endfor %}
|
||||
{% for n in bgp_neighbors if n.prepend %}
|
||||
neighbor {{ n.ip }} route-map RM-PREPEND-OUT out
|
||||
{% for n in bgp_neighbors %}
|
||||
neighbor {{ n.ip }} route-map {{ 'RM-OUT-PREPEND' if n.prepend else 'RM-OUT' }} out
|
||||
{% endfor %}
|
||||
exit-address-family
|
||||
!
|
||||
|
||||
@@ -43,12 +43,17 @@ for n in vr01 vr02 vr03 vr04; do
|
||||
check "$n neighbor main2" "$f" " neighbor $MAIN2_NEIGHBOR_IP remote-as 65104" 1
|
||||
check "$n neighbor backup1" "$f" " neighbor $BACKUP1_NEIGHBOR_IP remote-as 65103" 1
|
||||
check "$n neighbor backup2" "$f" " neighbor $BACKUP2_NEIGHBOR_IP remote-as 65103" 1
|
||||
check "$n route-map defined" "$f" "route-map RM-PREPEND-OUT permit 10" 1
|
||||
check "$n prefix-list anycast only" "$f" "ip prefix-list PL-ANYCAST-OUT seq 5 permit 172.31.255.10/32" 1
|
||||
check "$n RM-OUT filters to anycast" "$f" "match ip address prefix-list PL-ANYCAST-OUT" 2 # RM-OUT + RM-OUT-PREPEND
|
||||
check "$n RM-OUT implicit deny" "$f" "route-map RM-OUT deny 20" 1
|
||||
check "$n RM-OUT-PREPEND defined" "$f" "route-map RM-OUT-PREPEND permit 10" 1
|
||||
check "$n prepend value" "$f" "set as-path prepend 65200 65200 65200" 1
|
||||
check "$n route-map on backup1 only" "$f" "neighbor $BACKUP1_NEIGHBOR_IP route-map RM-PREPEND-OUT out" 1
|
||||
check "$n route-map on backup2 only" "$f" "neighbor $BACKUP2_NEIGHBOR_IP route-map RM-PREPEND-OUT out" 1
|
||||
check "$n NO route-map on main1" "$f" "neighbor $MAIN1_NEIGHBOR_IP route-map RM-PREPEND-OUT out" 0
|
||||
check "$n NO route-map on main2" "$f" "neighbor $MAIN2_NEIGHBOR_IP route-map RM-PREPEND-OUT out" 0
|
||||
check "$n filter+prepend on backup1" "$f" "neighbor $BACKUP1_NEIGHBOR_IP route-map RM-OUT-PREPEND out" 1
|
||||
check "$n filter+prepend on backup2" "$f" "neighbor $BACKUP2_NEIGHBOR_IP route-map RM-OUT-PREPEND out" 1
|
||||
check "$n filter-only on main1" "$f" "neighbor $MAIN1_NEIGHBOR_IP route-map RM-OUT out" 1
|
||||
check "$n filter-only on main2" "$f" "neighbor $MAIN2_NEIGHBOR_IP route-map RM-OUT out" 1
|
||||
check "$n NO prepend route-map on main1" "$f" "neighbor $MAIN1_NEIGHBOR_IP route-map RM-OUT-PREPEND out" 0
|
||||
check "$n NO prepend route-map on main2" "$f" "neighbor $MAIN2_NEIGHBOR_IP route-map RM-OUT-PREPEND out" 0
|
||||
|
||||
nft="$OUT_DIR/$n/nftables.conf"
|
||||
check "$n nftables masquerade" "$nft" "masquerade" 1
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# План внедрения: outbound-фильтр BGP (устранение утечки 10.0.0.0/8)
|
||||
|
||||
**Дата:** 2026-09-10
|
||||
|
||||
## Проблема
|
||||
|
||||
В PROD (окружение `site-cloud`) FRR реэкспортировал входящий маршрут `10.0.0.0/8` обратно соседям. Причина: ни у одного BGP-соседа не было outbound-фильтра — `no bgp ebgp-requires-policy` отключает защитный механизм FRR, а единственный существующий route-map (`RM-PREPEND-OUT`, только на резервном канале) фильтрации не делал (`permit 10` без `match`). Роутеры многодомны в 2 разных внешних AS без изоляции — любой маршрут в BGP-таблице реэкспортировался всем eBGP-соседям (классический непреднамеренный transit/route-leak).
|
||||
|
||||
## Решение
|
||||
|
||||
`frr.conf.j2` (общий для всех окружений, включая площадку A — та же уязвимость структурно применима и там):
|
||||
- `ip prefix-list PL-ANYCAST-OUT permit <anycast_prefix>`.
|
||||
- `route-map RM-OUT` (filter-only) и `route-map RM-OUT-PREPEND` (filter + prepend, рендерится только если есть соседи с `prepend: true`) — оба матчат `PL-ANYCAST-OUT`, оба заканчиваются явным `deny 20`.
|
||||
- `neighbor ... route-map out` теперь навешан на **всех** соседей без исключения: `RM-OUT-PREPEND` для `prepend: true`, `RM-OUT` для остальных.
|
||||
- `no bgp ebgp-requires-policy` сознательно **не убран** — иначе FRR перестанет принимать вообще все inbound-маршруты без явной inbound-policy (отдельная задача, не входит в текущий фикс).
|
||||
|
||||
Golden-файлы площадки A (`frr/a{1,2,3}/frr.conf`) обновлены под новый рендер — это намеренное изменение (тот же класс уязвимости актуален и для Docker-лабы), а не регресс.
|
||||
|
||||
## Definition of done
|
||||
|
||||
- `render-check.sh` (площадка A) — совпадает с обновлёнными golden-файлами.
|
||||
- `render-check-cloud.sh` — фильтр применён на всех 4 узлах, prepend — только на резервных соседях, `RM-OUT`/`RM-OUT-PREPEND` синтаксически корректны.
|
||||
- README и summary обновлены.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Summary: outbound-фильтр BGP (устранение утечки 10.0.0.0/8)
|
||||
|
||||
**Дата:** 2026-09-10
|
||||
**План:** [2026-09-10-bgp-outbound-filter-plan.md](2026-09-10-bgp-outbound-filter-plan.md)
|
||||
|
||||
## Инцидент и причина
|
||||
|
||||
В PROD (`site-cloud`) FRR реэкспортировал входящий маршрут `10.0.0.0/8` обратно eBGP-соседям. Причина — структурная: ни у одного соседа (ни в `site-a`, ни в `site-cloud`) не было outbound-фильтра. `no bgp ebgp-requires-policy` отключал защитный механизм FRR (RFC 8212), который иначе не дал бы поднять eBGP-сессию без явной policy; единственный существующий route-map (`RM-PREPEND-OUT`, только на резервном канале site-cloud) не фильтровал — `permit 10` без `match` пропускал всё. Роутеры многодомны в разные внешние AS без изоляции по префиксам → любой маршрут в локальной BGP-таблице реэкспортировался всем eBGP-соседям.
|
||||
|
||||
## Что сделано
|
||||
|
||||
`ansible/roles/frr_anycast/templates/frr.conf.j2`:
|
||||
- Добавлен `ip prefix-list PL-ANYCAST-OUT seq 5 permit <anycast_prefix>`.
|
||||
- Добавлены `route-map RM-OUT` (только фильтр) и `route-map RM-OUT-PREPEND` (фильтр + prepend, рендерится только при наличии соседей с `prepend: true`) — оба матчат `PL-ANYCAST-OUT`, оба с явным `deny 20` в конце.
|
||||
- Каждому соседу в `bgp_neighbors` теперь назначен outbound route-map: `RM-OUT-PREPEND`, если `prepend: true`, иначе `RM-OUT` — раньше route-map вообще не навешивался на не-prepend соседей (0% фильтрации на основном канале site-cloud и на обоих соседях site-a).
|
||||
- `no bgp ebgp-requires-policy` **сознательно оставлен** — включение потребовало бы также inbound-policy на каждого соседа (иначе FRR перестанет принимать вообще все входящие маршруты), это отдельная, не запрошенная сейчас задача.
|
||||
|
||||
**Golden-файлы площадки A обновлены** (`frr/a{1,2,3}/frr.conf`) — добавлен тот же prefix-list/route-map-блок. Это намеренное изменение: та же уязвимость структурно присутствовала и в Docker-лабе (просто менее заметна, т.к. там оба BGP-соседа логически «свои»), фикс применён единообразно везде, где используется роль.
|
||||
|
||||
## Результаты проверки
|
||||
|
||||
- `render-check.sh` (площадка A) — 6/6 OK против обновлённых golden-файлов.
|
||||
- `render-check-cloud.sh` — 80/80 проверок: у всех 4 узлов ровно один outbound route-map на каждого из 4 соседей (по правильному типу), `nft -c -f` подтверждает синтаксическую валидность nftables (без изменений в этой части).
|
||||
- `--syntax-check` — OK для обоих playbook'ов.
|
||||
|
||||
## Не входит в этот фикс (сознательно)
|
||||
|
||||
- Inbound-фильтрация (RFC1918/bogon-фильтр на приём) — рекомендована как defense-in-depth, но не реализована.
|
||||
- Включение `bgp ebgp-requires-policy` обратно — требует отдельной проработки inbound-policy на каждого соседа.
|
||||
|
||||
## Изменённые файлы
|
||||
|
||||
- `ansible/roles/frr_anycast/templates/frr.conf.j2`.
|
||||
- `frr/a1/frr.conf`, `frr/a2/frr.conf`, `frr/a3/frr.conf` (golden, Docker-лаба).
|
||||
- `ansible/tests/render-check-cloud.sh` — обновлены проверки под новые имена route-map/prefix-list.
|
||||
- `README.md` — пояснение про обязательный outbound-фильтр.
|
||||
- `docs/2026-09-10-bgp-outbound-filter-plan.md`, `docs/2026-09-10-bgp-outbound-filter-summary.md`.
|
||||
@@ -7,6 +7,13 @@ no ipv6 forwarding
|
||||
interface dummy0
|
||||
ip address 10.200.200.1/32
|
||||
!
|
||||
ip prefix-list PL-ANYCAST-OUT seq 5 permit 10.200.200.1/32
|
||||
!
|
||||
route-map RM-OUT permit 10
|
||||
match ip address prefix-list PL-ANYCAST-OUT
|
||||
!
|
||||
route-map RM-OUT deny 20
|
||||
!
|
||||
router bgp 65001
|
||||
bgp router-id 10.0.0.1
|
||||
no bgp ebgp-requires-policy
|
||||
@@ -17,6 +24,8 @@ router bgp 65001
|
||||
network 10.200.200.1/32
|
||||
neighbor 10.0.1.2 activate
|
||||
neighbor 10.0.2.2 activate
|
||||
neighbor 10.0.1.2 route-map RM-OUT out
|
||||
neighbor 10.0.2.2 route-map RM-OUT out
|
||||
exit-address-family
|
||||
!
|
||||
line vty
|
||||
|
||||
@@ -7,6 +7,13 @@ no ipv6 forwarding
|
||||
interface dummy0
|
||||
ip address 10.200.200.1/32
|
||||
!
|
||||
ip prefix-list PL-ANYCAST-OUT seq 5 permit 10.200.200.1/32
|
||||
!
|
||||
route-map RM-OUT permit 10
|
||||
match ip address prefix-list PL-ANYCAST-OUT
|
||||
!
|
||||
route-map RM-OUT deny 20
|
||||
!
|
||||
router bgp 65001
|
||||
bgp router-id 10.0.0.2
|
||||
no bgp ebgp-requires-policy
|
||||
@@ -17,6 +24,8 @@ router bgp 65001
|
||||
network 10.200.200.1/32
|
||||
neighbor 10.0.3.2 activate
|
||||
neighbor 10.0.4.2 activate
|
||||
neighbor 10.0.3.2 route-map RM-OUT out
|
||||
neighbor 10.0.4.2 route-map RM-OUT out
|
||||
exit-address-family
|
||||
!
|
||||
line vty
|
||||
|
||||
@@ -7,6 +7,13 @@ no ipv6 forwarding
|
||||
interface dummy0
|
||||
ip address 10.200.200.1/32
|
||||
!
|
||||
ip prefix-list PL-ANYCAST-OUT seq 5 permit 10.200.200.1/32
|
||||
!
|
||||
route-map RM-OUT permit 10
|
||||
match ip address prefix-list PL-ANYCAST-OUT
|
||||
!
|
||||
route-map RM-OUT deny 20
|
||||
!
|
||||
router bgp 65001
|
||||
bgp router-id 10.0.0.3
|
||||
no bgp ebgp-requires-policy
|
||||
@@ -17,6 +24,8 @@ router bgp 65001
|
||||
network 10.200.200.1/32
|
||||
neighbor 10.0.5.2 activate
|
||||
neighbor 10.0.6.2 activate
|
||||
neighbor 10.0.5.2 route-map RM-OUT out
|
||||
neighbor 10.0.6.2 route-map RM-OUT out
|
||||
exit-address-family
|
||||
!
|
||||
line vty
|
||||
|
||||
Reference in new issue
Block a user