Всем привет, меня зовут Антон Баранов, ведущий разработчик платформы контейнеризации «Боцман» в Группе Астра.

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)

Внутри работает многоступенчатый алгоритм:

  1. Формируем ключ с полным набором данных (включая уровень и категории) и делаем первый 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;
        }
    }
  2. Если совпадения нет, зануляем 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;
        }
    }
  3. Если политика по классификационным меткам не найдена, возвращаем 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;
    }
  4. Далее sec_label также может быть занулён, и поиск продолжается только по L4.

    /* L4-only lookup. */
    key.sec_label = 0;
    l4policy = map_lookup_elem(map, &key);
  5. И в конце  смотрим что нашли и принимается решение

    
    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 научился нативно учитывать классификационные метки, что даёт бОльший контроль трафика и прозрачность для команд безопасности и эксплуатации.