Всем привет, меня зовут Антон Баранов, ведущий разработчик платформы контейнеризации «Боцман» в Группе Астра.
eBPF в Kubernetes перестал быть экзотикой и стал базовым механизмом для реализации L3/L4/L7‑политик, продвинутой телеметрии и runtime‑security на уровне ядра. Для инженера это значит, что без понимания eBPF‑хуков, map’ов и того, как Cilium/Calico и observability‑агенты встраиваются в сетевой стек уже невозможно эффективно дебажить и тюнить современный кластер kubernetes. В этой статье я расскажу, как мы расширили политики фильтрации трафика в Cilium, чтобы они учитывали классификационные метки, встроенные в сетевые пакеты Astra Linux, и что это дало с точки зрения безопасности и наблюдаемости.
1. Зачем трогать Cilium, если он и так мощный

На первый взгляд кажется, что Cilium уже умеет всё, что нужно для фильтрации трафика в Kubernetes‑кластере. Однако в Astra Linux все исходящие IPv4 пакеты получают дополнительную классификационную метку в поле Options, и использовать эту метку для усиления безопасности нам показалось хорошей идеей.

Идея была простой:
Научить Cilium «понимать» эту метку на уровне BPF
Поддержать нововведения будто они всегда были
В итоге получилось решение, которое добавляет в политики фильтрации трафика ещё один слой контроля, встроенный в саму ОС.
2. Что такое классификационная метка в Astra Linux
В Astra Linux в поле Options IPv4‑пакета всегда присутствует поле типа Security с фиксированным базовым значением Unclassified. Полезная нагрузка метки содержится в поле PROTECTION AUTHORITY FLAGS и кодирует:
Уровень (8 бит);
Битовую маску категорий (до 64 бит)
Последний бит каждого байта в поле указывает, есть ли следующий байт.
На эту метку существует ГОСТ Р 58256- 2018:
TYPE (8 бит) | LENGTH (8 бит) | CLASSIFICATION LEVEL (8 бит) | PROTECTION AUTHORITY FLAGS (11+ байт) |
10000010 (Dec 130) Security | ХХХХХХХХ | 10101011 Unclassified | AAAAAAA[1|0] AAAAAAA0 |
Общий вид заполнения поля PROTECTION AUTHORITY FLAGS
L - биты значения уровня
С - битовая маска категорий
LLLLLLL1 CCCCCCL1 ССССССС1 ССССССС1 ССССССС1 ССССССС1 ССССССС1 ССССССС1 ССССССС1 ССССССС1 00000СС0
Таким образом, у нас есть два ключевых атрибута:
уровень доступа;
категории — битовая маска, отражающая принадлежность к наборам доменов/классов.
Именно по ним мы и захотели строить политики фильтрации, дополняя классические L3/L4‑правила.
3. Почему именно Cilium и BPF
Коротко:
Cilium — это CNI‑плагин для Kubernetes, который использует BPF (eBPF) в ядре Linux для обработки сетевого трафика, мониторинга и реализации политик безопасности.
BPF позволяет безопасно выполнять пользовательский код в ядре, проходящий статическую проверку верификатором, что защищает систему от зависаний и падений.

Ключевые преимущества такого подхода:
минимальный оверхед по сети за счёт обхода части классического сетевого стека;
гибкость — фильтрация и маршрутизация реализуются в BPF‑программах;
расширяемость — можно добавлять свои поля и алгоритмы, не переписывая ядро.
Cilium на каждой ноде поднимает своего агента, который:
загружает BPF‑программы;
управляет BPF‑мапами (maps) с политиками;
синхронизируется через оператор.

4. Как устроены BPF‑мапы политик в Cilium и что мы добавили
Политики в Cilium хранятся в BPF‑мапах типа LPM_TRIE:
#ifdef POLICY_MAP /* Per-endpoint policy enforcement map */ struct { __uint(type, BPF_MAP_TYPE_LPM_TRIE); __type(key, struct policy_key); __type(value, struct policy_entry); __uint(pinning, LIBBPF_PIN_BY_NAME); __uint(max_entries, POLICY_MAP_SIZE); __uint(map_flags, BPF_F_NO_PREALLOC); } POLICY_MAP __section_maps_btf; #endif struct policy_key { struct bpf_lpm_trie_key lpm_key; __u32 sec_label; __u8 egress:1, pad:7; __u8 protocol; __u16 dport; }; struct policy_entry { __be16 proxy_port; __u8 deny:1, wildcard_protocol:1, wildcard_dport:1, pad:5; __u8 auth_type; __u16 pad1; __u16 pad2; __u64 packets; __u64 bytes; };
LPM (Longest Prefix Match) обозначает, что поиск записи в мапе идёт по принципу наибольшего совпадения префикса: чем больше совпадающих старших бит в ключе, тем «ближе» политика к пакету. (Полный алгоритм прекрасно описан в репозитории)
В типичном ключе политики Cilium есть:
служебный lpm_key (длина префикса);
security label;
направление трафика (ingress/egress);
протокол;
порт.
В значении (entry) хранится:
действие над пакетом;
счётчики пакетов и байт;
дополнительные флаги.
Мы расширили эти структуры:
в ключ добавили уровень из классификационной метки (astra_mac_level);
в entry — битовую маску категорий.
struct policy_key { struct bpf_lpm_trie_key lpm_key; __u32 sec_label; // +++ __u8 astra_mac_label; __u8 pad1; __u16 pad2; // +++ __u8 egress:1, pad:7; __u8 protocol; __u16 dport; }; struct policy_entry { __be16 proxy_port; __u8 deny:1, wildcard_protocol:1, wildcard_dport:1, pad:5; __u8 auth_type; __u16 pad1; __u16 pad2; __u64 packets; __u64 bytes; // +++ __u64 categories; // +++ };
Переменная astra_mac_label называется так по историческим причинам напоминает отсылку к MAC‑адресу, но фактически относится к mandatory access control.
Таким образом, политика получила новые измерения — уровень и категории, по которым можно фильтровать трафик.
5. Алгоритм поиска политики с учётом классификационных меток
Поиск в LPM‑мапе начинается с формирования ключа на основании данных пакета, включая разобранные IP options и классификационную метку.
Мы добавили код, который умеет парсить IP‑опции и извлекать нужные поля уровня и категорий.На момент начала проверки эти поля уже извлечены:
static __always_inline int __policy_can_access(const void *map, struct __ctx_buff *ctx, __u32 local_id, __u32 remote_id, __u16 ethertype __maybe_unused, __u16 dport, __u8 proto, int off __maybe_unused, int dir, bool is_untracked_fragment, __u8 *match_type, __s8 *ext_err, __u16 *proxy_port, __u8 mac_level, __u64 categories)
Внутри работает многоступенчатый алгоритм:
Формируем ключ с полным набором данных (включая уровень и категории) и делаем первый lookup в мапу.Если запись найдена, сравниваем категории и уже на этом шаге можем принять решение по пакету.
policy = map_lookup_elem(map, &key); if (likely(policy)) { if ((policy->categories & categories) == policy->categories) { astra_printk("AMAC Policy FOUND (1st lookup)"); *match_type = POLICY_MATCH_L3_ONLY; goto check_policy; } }Если совпадения нет, зануляем sec_label — в нашем алгоритме это означает, что поле перестаёт быть значимым для поиска, и делаем повторный lookup.
key.sec_label = 0; policy = map_lookup_elem(map, &key); if (likely(policy)) { if ((policy->categories & categories) == policy->categories) { astra_printk("AMAC Policy FOUND (2nd lookup)"); *match_type = POLICY_MATCH_L3_ONLY; goto check_policy; } }Если политика по классификационным меткам не найдена, возвращаем sec_label, зануляем astra_mac_level и переходим к стандартному алгоритму Cilium, который ищет соответствие по L3/L4.
key.astra_mac_level = 0; key.dport = dport; key.sec_label = remote_id; policy = map_lookup_elem(map, &key); if (likely(policy && !policy->wildcard_dport)) { printk("1. full L3/l4 match"); cilium_dbg3(ctx, DBG_L4_CREATE, remote_id, local_id, dport << 16 | proto); *match_type = POLICY_MATCH_L3_L4; /* 1. id/proto/port */ goto check_policy; }Далее sec_label также может быть занулён, и поиск продолжается только по L4.
/* L4-only lookup. */ key.sec_label = 0; l4policy = map_lookup_elem(map, &key);И в конце смотрим что нашли и принимается решение
if (likely(l4policy && !l4policy->wildcard_dport)) { astra_printk("2. ANY/proto/port"); *match_type = POLICY_MATCH_L4_ONLY; /* 2. ANY/proto/port */ goto check_l4_policy; } if (likely(policy && !policy->wildcard_protocol)) { astra_printk("3. id/proto/ANY"); *match_type = POLICY_MATCH_L3_PROTO; /* 3. id/proto/ANY */ goto check_policy; } if (likely(l4policy && !l4policy->wildcard_protocol)) { astra_printk("4. ANY/proto/ANY"); *match_type = POLICY_MATCH_PROTO_ONLY; /* 4. ANY/proto/ANY */ goto check_l4_policy; } if (likely(policy)) { astra_printk("5. id/ANY/ANY"); *match_type = POLICY_MATCH_L3_ONLY; /* 5. id/ANY/ANY */ goto check_policy; } if (likely(l4policy)) { astra_printk("6. ANY/ANY/ANY"); *match_type = POLICY_MATCH_ALL; /* 6. ANY/ANY/ANY */ goto check_l4_policy; }
Если ни на одном этапе политика не находится, пакет дропается.
Так мы аккуратно встроили учёт классификационных меток в существующую модель поиска, не ломая стандартную семантику Cilium.
6. Как политики с метками попадают в BPF: уровень Kubernetes
На уровне Kubernetes всё начинается с YAML‑манифеста CiliumNetworkPolicy, в котором мы добавили новые поля для работы с классификационными метками.
Например, можно явно запретить исходящий трафик для пакетов с уровнем 3 и категорией 1.
apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: name: "awesome-policy" spec: ingress: - fromEntities: - all ingressDeny: - amacs: - level: 3 categories: "0x1" egress: - toEntities: - all
И применить этот манифест, который пройдя через Kubernetes окажется в Cilium.

Чтобы такой манифест стал валидным мы:
расширили CRD Cilium, добавив поля для уровня и категорий;
внесли изменения во весь управляющий Go‑код, который обрабатывает политики и синхронизирует их с BPF‑мапами;
адаптировали утилиты, чтобы новые поля были «нативной» частью интерфейса, а не сторонней надстройкой.
Объём правок получился существенным, но для пользователя всё выглядит как нативная функциональность Cilium.
7. Практический эксперимент: смотрим мапы и endpoint’ы
Для проверки работоспособности мы подняли тестовый кластер из двух нод, установили Cilium и зашли на worker‑ноду.
Дальше пригодилась утилита cilium-dbg, которая умеет:
показывать список BPF‑мап (команда map);
работать с содержимым мап (bpf);
выводить список endpoint’ов на ноде (endpoint).
Мы посмотрели список мап и нашли те, которые содержат политики.

Внутри них уже были записи, разрешающие трафик, при этом в ключах и значениях видны добавленные поля уровня и категорий.

Числа в конце имени мапы соответствуют endpoint ID, что легко проверить по выводу cilium-dbg endpoint.Каждому endpoint Cilium назначает IP‑адрес и identity — уникальный набор security labels, который используется для масштабируемого применения политик к группам endpoint’ов.

Для низкоуровневого анализа мы также использовали bpftool, который позволяет дампить содержимое BPF‑мап побайтно.По дампу можно увидеть структуру ключа: порт, протокол, флаг направления трафика, байты выравнивания, уровень astra_mac, security label и lpm_key.

7.1. Добавляем нагрузку и новые политики
Дальше мы:
создавали pod и Cilium‑политику, блокирующую исходящий TCP‑трафик на порт 80;
apiVersion: v1 kind: Pod metadata: name: alpine labels: app: alpine-based spec: containers: - name: alpine tty: true stdin: true image: "alpine:latest" --- apiVersion: "cilium.io/v2" kind: CiliumNetworkPolicy metadata: name: "block-80-tcp" spec: endpointSelector: matchLabels: app: alpine-based egressDeny: - toPorts: - ports: - port: "80" protocol: TCP egress: - toEntities: - all
снова смотрели список мап на ноде — появлялась новая мапа для этой политики;

проверяли список endpoint’ов — там тоже появлялся новый endpoint.

Через cilium-dbg и bpftool легко увидеть свежую запись политики в мапе иубедиться, что новые поля уровня и категорий участвуют в принятии решения.


8. Наблюдаемость: Hubble, Prometheus и Grafana
В базовой конфигурации Cilium предоставляет довольно ограниченный набор метрик: в основном про внутреннее состояние самого Cilium, а не про трафик.
Для полноценного наблюдения за потоками трафика используется Hubble, который входит в состав агента Cilium и по умолчанию может быть отключён из‑за дополнительного оверхеда.
Hubble:
пишет метрики напрямую в Prometheus;
по gRPC отправляет flow‑события в Hubble Relay;
предоставляет визуализацию через Hubble UI и доступ через CLI.

Каждый flow содержит информацию:
о пакете от L2 до L4;
о политике, которая была применена к пакету.
Мы расширили message IP так, чтобы в flow были видны наши классификационные метки — их удобно подсвечивать в Hubble UI и анализировать через CLI.


8.1. Снова к практике
В экспериментальном кластере мы:
подняли поды, обменивающиеся пакетами с классификационными метками;
ввели политику, запрещающую трафик с определёнными метками;
через Hubble CLI получили вывод в текстовом и JSON‑формате, где в IP‑секции видно значения меток.


Также мы расширили метрики, которые Hubble экспортирует в Prometheus:
добавили два новых лейбла к метрике дропа пакетов — наличие классификационной метки и ноду, на которой произошёл дроп.

В Grafana это превращается, например, в два графика: один показывает drop‑rate пакетов с метками, другой — без них.Так можно быстро оценить, как политики по классификационным меткам влияют на трафик и где возникают проблемы.

9. Что мы получили в итоге
Проделанная работа дала нам несколько важных результатов:
разобрались, как Cilium использует BPF внутри и где лучше всего встраивать дополнительные сигналы безопасности;
добавили поддержку парсинга IP‑опций и переработали структуру политик, чтобы учитывать уровень и категории классификационной метки;
внесли изменения в управляющий Go‑код Cilium, CRD и утилиты, включая Hubble UI и CLI, чтобы новые поля были частью стандартизированного интерфейса;
расширили метрики и наблюдаемость, добавив видимость классификационных меток на уровне Hubble, Prometheus и Grafana;
дописали тесты на новый функционал и убедились, что существующие сценарии, включая connectivity‑тесты, не сломались.
В результате Cilium в окружении Astra Linux научился нативно учитывать классификационные метки, что даёт бОльший контроль трафика и прозрачность для команд безопасности и эксплуатации.
