3.4Сеть, доступ и отладка
Проследишь запрос через DNS, Service и сетевые правила, а затем проверишь права Pod на Kubernetes API. Разберёшь, как NetworkPolicy может заблокировать DNS и как отлаживать образ без shell.
- Какие цепочки kube-proxy пишет ради ClusterIP и откуда у правила вероятность 0,5.
- Почему внешнее имя из пода стоит восьми DNS-запросов и что меняют точка в конце и
dnsConfig. - Что отдаёт DNS для headless Service и почему gRPC-клиент не сразу видит новые поды.
- Чем Gateway API отличается от Ingress и что стало с ingress-nginx.
- Почему NetworkPolicy принимается, даже если её некому исполнять, и как default deny отрезает DNS.
- Сколько живёт токен пода и как проверить права через
kubectl auth can-i. - Что читать в событиях и
describeи как отладить distroless-под.
Кластер: kind 0.33.0, Kubernetes 1.37.0, один узел dv-k8s-control-plane, containerd 2.3.4, kindnet,
CoreDNS 1.14.6. Его только читали: манифесты проверены --dry-run=server, а то, что без записи не увидеть,
описано по документации и исходникам 1.37. Команды с # выполнены на узле.
Service: адрес, которого нет ни на одном интерфейсе
В главе 3.1 Service назван набором правил в ядре узла. Два таких Service есть почти в любом кластере:
$ kubectl get svc,endpointslices -A -o wide
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
default service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 4m57s <none>
kube-system service/kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 4m55s k8s-app=kube-dns
NAMESPACE NAME ADDRESSTYPE PORTS ENDPOINTS AGE
default endpointslice.discovery.k8s.io/kubernetes IPv4 6443 172.23.0.2 4m57s
kube-system endpointslice.discovery.k8s.io/kube-dns-wkh2n IPv4 53,53,9153 10.244.0.4,10.244.0.2 4m49s
$ kubectl get cm kube-proxy -n kube-system -o jsonpath='{.data.config\.conf}' | grep '^mode:'
mode: iptables
Адрес 10.96.0.10 никто не слушает, его знают только правила kube-proxy на каждом узле. Если режим не задан,
kube-proxy 1.37 берёт iptables. Режим nftables стабилен с 1.33, на свежих ядрах советуют его,
а ipvs устарел в 1.35: kube-proxy обещает отключить его по умолчанию в 1.40 и удалить в 1.43. Правила
для DNS по UDP:
# iptables-save -t nat | grep 'A KUBE-SERVICES.*kube-dns:dns cluster IP'
-A KUBE-SERVICES -d 10.96.0.10/32 -p udp -m comment --comment "kube-system/kube-dns:dns cluster IP" -m udp --dport 53 -j KUBE-SVC-TCOU7JCQXEZGVUNU
# iptables-save -t nat | grep '^-A KUBE-SVC-TCOU7JCQXEZGVUNU'
-A KUBE-SVC-TCOU7JCQXEZGVUNU ! -s 10.244.0.0/16 -d 10.96.0.10/32 -p udp -m comment --comment "kube-system/kube-dns:dns cluster IP" -m udp --dport 53 -j KUBE-MARK-MASQ
-A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns -> 10.244.0.2:53" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-YIL6JZP7A3QYXJU2
-A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns -> 10.244.0.4:53" -j KUBE-SEP-WXWGHGKZOCNYRYI7
KUBE-SERVICES подключена к PREROUTING и OUTPUT и по адресу и порту отправляет
пакет в цепочку сервиса. Там первое правило метит для MASQUERADE пакеты не из сети подов, например с узла:
им подставят адрес узла, и ответ вернётся тем же путём. Дальше лестница: первое правило срабатывает с вероятностью 0,5,
второе забирает остальное, а цепочка KUBE-SEP-… делает DNAT на под. Для N эндпоинтов вероятности
1/N, 1/(N−1) и так до 1/2 (writeServiceToEndpointRules в pkg/proxy/iptables/proxier.go).
Случайность видна по счётчикам. dig в сетевом namespace узла отправил тысячу запросов на ClusterIP:
# iptables-save -c -t nat | grep -o '^\[[0-9:]*\].*kube-dns:dns -> [0-9.:]*'
[1434:153438] -A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns -> 10.244.0.2:53
[1491:159522] -A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns -> 10.244.0.4:53
$ docker run --rm --network container:dv-k8s-control-plane --entrypoint sh \
registry.k8s.io/e2e-test-images/agnhost:2.39 -c \
'i=0; while [ $i -lt 1000 ]; do echo kube-dns.kube-system.svc.cluster.local; i=$((i+1)); done > /tmp/q; dig @10.96.0.10 +short -f /tmp/q | sort | uniq -c'
1000 10.96.0.10
# iptables-save -c -t nat | grep -o '^\[[0-9:]*\].*kube-dns:dns -> [0-9.:]*'
[1921:205547] -A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns -> 10.244.0.2:53
[1982:212059] -A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns -> 10.244.0.4:53
# conntrack -L -p udp --orig-dst 10.96.0.10 2>/dev/null | grep -o 'src=10.244.0.[0-9]*' | sort | uniq -c
487 src=10.244.0.2
491 src=10.244.0.4
Счётчики выросли на 487 и 491, как и число записей conntrack, а запросов было 1000: таблицу nat проходит
только первый пакет потока (глава 1.3 статьи «Linux и ОС»), а 22 запроса ушли с порта, для которого запись в этой
серии уже была. С TCP под выбирается на SYN. Если у Service нет готовых эндпоинтов, в filter стоит
REJECT с комментарием has no endpoints, и клиент сразу получает отказ. В режиме nftables
сервис ищется в словаре service-ips, без перебора всех сервисов подряд, а эндпоинт выбирает
numgen random mod N.
EndpointSlice и sessionAffinity
Адреса kube-proxy берёт из EndpointSlice: до 100 адресов в срезе (--max-endpoints-per-slice), у каждого
свои условия.
$ kubectl get endpointslices -n kube-system -l kubernetes.io/service-name=kube-dns \
-o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]} {.conditions} {.targetRef.name}{"\n"}{end}'
10.244.0.4 {"ready":true,"serving":true,"terminating":false} coredns-559f6c778d-tdsh4
10.244.0.2 {"ready":true,"serving":true,"terminating":false} coredns-559f6c778d-gx9h8
ready значит «готов и не удаляется», serving повторяет готовность пода,
terminating говорит об удалении; как они меняются при остановке, разобрано в главе 3.2. Endpoints в 1.33
объявлен устаревшим, и kubectl get endpoints из главы 3.1 печатает об этом Warning.
# orders-svc.yaml
apiVersion: v1
kind: Service
metadata:
name: orders
spec:
selector:
app: orders
ports:
- name: http
port: 80
targetPort: 8080
sessionAffinity: ClientIP
$ kubectl apply --dry-run=server -f orders-svc.yaml \
-o jsonpath='{.spec.clusterIP} {.spec.sessionAffinity} {.spec.sessionAffinityConfig}{"\n"}'
10.96.0.0 ClientIP {"clientIP":{"timeoutSeconds":10800}}
sessionAffinity: ClientIP закрепляет клиента за подом на 10800 секунд (допустимо до 86400). Адрес 10.96.0.0
подставил dry-run, настоящий ClusterIP не выделен. В iptables закрепление держит модуль recent:
KUBE-SEP-… запоминает источник, а в KUBE-SVC-… перед лестницей встают правила
-m recent --rcheck --seconds 10800 --reap. Список у каждого узла свой, клиентом считается IP, и весь
трафик из-за одного NAT или от ingress-контроллера выглядит одним клиентом.
CoreDNS и записи Service
$ kubectl get cm coredns -n kube-system -o jsonpath='{.data.Corefile}'
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30 {
disable success cluster.local
disable denial cluster.local
}
loop
reload
loadbalance
}
kubernetes отвечает за cluster.local по данным из API, pods insecure добавляет
записи вида 10-244-0-2.kube-system.pod.cluster.local без проверки пода. forward отправляет
остальное серверам из resolv.conf пода CoreDNS, cache зону кластера не кэширует,
loadbalance перемешивает A-записи. Ниже dig из образа agnhost в сетевом
namespace узла:
# dig +noall +answer @10.96.0.10 kube-dns.kube-system.svc.cluster.local
kube-dns.kube-system.svc.cluster.local. 30 IN A 10.96.0.10
# dig +noall +answer @10.96.0.10 SRV _dns._udp.kube-dns.kube-system.svc.cluster.local
_dns._udp.kube-dns.kube-system.svc.cluster.local. 30 IN SRV 0 100 53 kube-dns.kube-system.svc.cluster.local.
# dig @10.96.0.10 api.example.com.default.svc.cluster.local | grep -E "status|flags"
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 25361
;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
; EDNS: version: 0, flags:; udp: 4096
На несуществующее имя зоны CoreDNS отвечает NXDOMAIN сам, с флагом aa. У headless Service нет ни адреса,
ни правил kube-proxy, а записи по спецификации DNS для Kubernetes 1.1.0 такие:
# orders-headless.yaml
apiVersion: v1
kind: Service
metadata:
name: orders-headless
spec:
clusterIP: None
selector:
app: orders
ports:
- name: grpc
port: 9000
$ kubectl apply --dry-run=server -f orders-headless.yaml \
-o jsonpath='{.spec.clusterIP} {.spec.clusterIPs}{"\n"}'
None ["None"]
| Запрос | Обычный Service | Headless Service |
|---|---|---|
A orders.shop.svc.cluster.local | ClusterIP | адреса готовых эндпоинтов; нет готовых - NXDOMAIN |
SRV _grpc._tcp.orders.shop.svc… | запись на именованный порт | запись на пару «готовый эндпоинт, порт» |
A orders-0.orders.shop.svc… | в спецификации нет, но CoreDNS отвечает так же | адрес пода, если у эндпоинта есть hostname |
Hostname эндпоинт получает, когда у пода есть hostname и subdomain с именем Service,
StatefulSet ставит их сам. gRPC за обычным Service держит одно соединение с одним подом (статья «Сети», глава 9.4).
Headless Service отдаёт готовые адреса на момент запроса, а grpc-go 1.83 перечитывает DNS при обрыве соединения, не
чаще раза в 30 секунд и без оглядки на TTL. Новые поды клиент увидит после обрыва, поэтому на сервере ставят
MaxConnectionAge.
ndots:5: восемь запросов на одно имя
Под с dnsPolicy: ClusterFirst, это значение по умолчанию, получает resolv.conf от kubelet:
# cat /var/lib/containerd/io.containerd.grpc.v1.cri/sandboxes/$(crictl pods -q --name local-path-provisioner)/resolv.conf
search local-path-storage.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5
kubelet (GetPodDNS в pkg/kubelet/network/dns/dns.go) ставит домены
<namespace>.svc.<домен>, svc.<домен>, <домен> и домены
узла, сервер из clusterDNS и ndots:5, а опции узла отбрасывает. Поды CoreDNS с
dnsPolicy: Default получили файл узла как есть: сервер 192.168.65.254 (DNAT на DNS Docker) и
ndots:0. Под с hostNetwork и ClusterFirst kubelet тоже переводит на
Default, нужен ClusterFirstWithHostNet.
С ndots:5 имя, где точек меньше пяти, резолвер сначала пробует с каждым доменом поиска и потом как есть.
В api.example.com две точки, в orders.default.svc.cluster.local четыре. Стенд в Docker:
CoreDNS 1.14.6 с плагином log, у клиента resolv.conf пода из default, где
вместо 10.96.0.10 этот CoreDNS, а resolve на Go 1.27.1 (CGO_ENABLED=0) вызывает
net.DefaultResolver.LookupHost:
$ docker run --rm --network dv-c4-dns -v "$PWD/resolv.conf:/etc/resolv.conf:ro" \
-v "$PWD/resolve:/resolve:ro" debian:13 /resolve api.example.com
api.example.com [203.0.113.10] <nil> 1ms
$ docker logs dv-c4-coredns 2>&1 | awk '/INFO/ {print $5, $7, $12}' | tr -d '"'
AAAA api.example.com.default.svc.cluster.local. NXDOMAIN
A api.example.com.default.svc.cluster.local. NXDOMAIN
AAAA api.example.com.svc.cluster.local. NXDOMAIN
A api.example.com.svc.cluster.local. NXDOMAIN
A api.example.com.cluster.local. NXDOMAIN
AAAA api.example.com.cluster.local. NXDOMAIN
A api.example.com. NOERROR
AAAA api.example.com. NOERROR
Шесть запросов из восьми ушли впустую, и каждый прошёл DNAT и conntrack. Отказы CoreDNS даёт сам, наружу уходят последние два. Число запросов зависит от библиотеки:
| Резолвер, тот же стенд | api.example.com | api.example.com. |
|---|---|---|
| Go 1.27.1, чистый резолвер | 8: A и AAAA на четыре имени | 2 |
glibc 2.41, getent ahosts (Debian 13) | 4: только A (AI_ADDRCONFIG, IPv6-адреса нет); с --no-addrconfig 8 | 1 |
musl 1.2.6, wget из busybox (Alpine 3.24) | 8 | 2 (getent hosts) |
- Точка в конце делает имя полным. HTTP-клиент Go 1.27.1 шлёт SNI и сверяет сертификат без точки, а
Hostуходит какapi.example.com., и принять его должен сервер. dnsConfigсndots: "2"пускает имя с двумя точками как есть первым. Несуществующее имя Go и glibc всё равно проведут по доменам, а musl при точках не меньшеndotsпоиск не делает (name_from_dns_search), иorders.default.svcу него не нашёлся.- NodeLocal DNSCache (стабилен с 1.18) ставит кэш на узел: запросов не меньше, но они минуют DNAT и conntrack, а отказы кэшируются рядом с подом.
# orders-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: orders-dns
spec:
containers:
- name: app
image: registry.local/orders:1.4.2
dnsConfig:
options:
- name: ndots
value: "2"
$ kubectl apply --dry-run=server -f orders-pod.yaml \
-o jsonpath='{.spec.dnsPolicy} {.spec.dnsConfig}{"\n"}'
ClusterFirst {"options":[{"name":"ndots","value":"2"}]}
Ingress, Gateway API и конец ingress-nginx
Таймауты, переписывание путей и canary в Ingress каждый контроллер задаёт своими аннотациями, и манифест под один контроллер другому не подходит. Документация называет API Ingress замороженным (не удалят, но и развивать не будут) и рекомендует Gateway API. Его CRD от SIG Network ставят отдельно, в kind их нет:
$ kubectl api-resources | grep -i -E 'gateway|ingress'
ingressclasses networking.k8s.io/v1 false IngressClass
ingresses ing networking.k8s.io/v1 true Ingress
$ kubectl apply --dry-run=server -f orders-route.yaml
error: resource mapping not found for name: "orders" namespace: "default" from "orders-route.yaml": no matches for kind "HTTPRoute" in version "gateway.networking.k8s.io/v1"
ensure CRDs are installed first
# orders-route.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: orders
namespace: default
spec:
parentRefs:
- name: public
namespace: infra
hostnames: ["api.example.com"]
rules:
- matches:
- path: { type: PathPrefix, value: /orders }
backendRefs:
- name: orders
port: 80
Вход поделён между владельцами:
- GatewayClass задаёт контроллер: Envoy Gateway, Istio, NGINX Gateway Fabric, Traefik и другие.
- Gateway с портами, сертификатами и
allowedRoutesотдан платформе. - HTTPRoute, GRPCRoute лежат в namespace приложения и цепляются через
parentRefs. - ReferenceGrant разрешает ссылку в чужой namespace.
В v1.6.2 (3 сентября 2026) стандартный канал держит в v1 GatewayClass, Gateway, HTTPRoute, GRPCRoute,
ReferenceGrant, BackendTLSPolicy, ListenerSet, TLSRoute, TCPRoute и UDPRoute, последние два с v1.6.0. GRPCRoute
сопоставляет запросы по сервису и методу gRPC, то есть шлюз видит отдельные вызовы, а не соединение.
11 ноября 2025 года SIG Network и Security Response Committee объявили о закрытии kubernetes/ingress-nginx:
поддержка по мере сил шла до марта 2026, дальше ни выпусков, ни исправлений уязвимостей. Последние выпуски, v1.15.1,
v1.14.5 и v1.13.9, вышли 19 марта 2026, репозиторий ушёл в архив. Установленные контроллеры работают.
Переезжать советуют на Gateway API с утилитой ingress2gateway (1.0 от 20 марта
2026) или на другой Ingress-контроллер.
NetworkPolicy: кто её исполняет
NetworkPolicy описывает разрешённые соединения, а исполняет её CNI. API-сервер примет политику в любом кластере, поля «применена» у неё нет, а по документации без контроллера, который её реализует, политика не действует.
# orders-netpol.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: default-deny, namespace: default}
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: allow-dns, namespace: default}
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}]
$ kubectl apply --dry-run=server -f orders-netpol.yaml
networkpolicy.networking.k8s.io/default-deny created (server dry run)
networkpolicy.networking.k8s.io/allow-dns created (server dry run)
$ kubectl logs -n kube-system ds/kindnet \
| grep -E 'kube-network-policies|Policy engine'
I0917 07:22:14.174492 1 controller.go:173] "Starting controller" name="kube-network-policies"
I0917 07:22:14.675365 1 controller.go:185] "Policy engine is ready."
Под изолирован в направлении, если его выбирает хоть одна политика с этим типом в policyTypes;
разрешения складываются, запретов в API нет. Ответ на разрешённое соединение проходит сам, трафик с узла пода разрешён
всегда, и пробы kubelet политика не режет. Service по имени не указать: поды выбирают метками, namespace - меткой
kubernetes.io/metadata.name.
В kind 0.33 политики исполняет kindnet: с kind 0.24 в него встроен kube-network-policies. У его ClusterRole есть
watch на networkpolicies, а на узле таблица inet kindnet-network-policies с
набором podips-v4 и правилами ip saddr @podips-v4 queue flags bypass to 101: пакеты подов под
политикой уходят через NFQUEUE в kindnetd, разрешённое соединение помечается меткой conntrack.
created одинаково там, где политики исполняются, и там, где их молча игнорируют. Проверяй делом: после
default deny соединение без разрешения должно перестать устанавливаться.
ServiceAccount, токен пода и RBAC
Под работает от имени ServiceAccount, по умолчанию default, и admission-плагин монтирует ему том
kube-api-access-…, это видно и в dry-run:
$ kubectl apply --dry-run=server -f orders-pod.yaml -o jsonpath='{.spec.serviceAccountName} {.spec.volumes[*].name}{"\n"}'
default kube-api-access-h8g6x
$ kubectl describe pod -n kube-system coredns-559f6c778d-gx9h8 | sed -n '/kube-api-access-b6wbz:/,/DownwardAPI/p'
kube-api-access-b6wbz:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 3607
ConfigMapName: kube-root-ca.crt
Optional: false
DownwardAPI: true
Projected-том кладёт в /var/run/secrets/kubernetes.io/serviceaccount токен, CA кластера и имя namespace.
Токен выдаёт TokenRequest, это JWT с привязкой к поду: в нём имя и UID пода, узла и ServiceAccount. Когда объект пода
с этим UID исчезает из API, токен перестаёт действовать. Срок 3607 секунд, час и семь секунд, API-сервер узнаёт по константе
WarnOnlyBoundTokenExpirationSeconds: с --service-account-extend-token-expiration (включён по
умолчанию) выписывает токен на год, а настоящий срок пишет в warnafter. Дальше токен работает, но в аудите
каждый запрос с ним помечен authentication.k8s.io/stale-token. kubelet обновляет файл после 80% срока,
около 48 минут. Сервису без API том не нужен: automountServiceAccountToken: false.
Права даёт RBAC: Role действует в namespace, ClusterRole во всём кластере или как шаблон, RoleBinding и ClusterRoleBinding связывают роль с субъектом. Правила только разрешают и складываются. CoreDNS:
$ kubectl get clusterrolebinding system:coredns -o jsonpath='{.roleRef.name} {.subjects}{"\n"}'
system:coredns [{"kind":"ServiceAccount","name":"coredns","namespace":"kube-system"}]
$ kubectl auth can-i --list --as=system:serviceaccount:kube-system:coredns \
| grep -E '^(Resources|endpoints|namespaces|pods|services|endpointslices)'
Resources Non-Resource URLs Resource Names Verbs
endpoints [] [] [list watch]
namespaces [] [] [list watch]
pods [] [] [list watch]
services [] [] [list watch]
endpointslices.discovery.k8s.io [] [] [list watch]
$ kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:kube-system:coredns
no
--as работает через impersonation, а встроенная роль edit в 1.37 разрешает
impersonate на ServiceAccount: разработчик с edit может действовать от имени любого
ServiceAccount своего namespace.
События, describe и логи упавшего контейнера
Event живёт час (--event-ttl API-сервера), и ночное падение утром в событиях не найти.
kubectl get events выводит их в порядке имён, а имя события начинается с имени объекта, так что строки
сгруппированы по объектам. kubectl events сортирует по времени, умеет --for,
--types=Warning и -w:
$ kubectl events -n kube-system --for pod/coredns-559f6c778d-gx9h8
LAST SEEN TYPE REASON OBJECT MESSAGE
27m Warning FailedScheduling Pod/coredns-559f6c778d-gx9h8 0/1 nodes are available: 1 node(s) had untolerated taint(s). preemption: 0/1 nodes are available: 1 Preemption is not helpful for scheduling.
26m Normal Scheduled Pod/coredns-559f6c778d-gx9h8 Successfully assigned kube-system/coredns-559f6c778d-gx9h8 to dv-k8s-control-plane
26m Normal Pulled Pod/coredns-559f6c778d-gx9h8 Container image "registry.k8s.io/coredns/coredns:v1.14.6" already present on machine and can be accessed by the pod
26m Normal Created Pod/coredns-559f6c778d-gx9h8 Container created
26m Normal Started Pod/coredns-559f6c778d-gx9h8 Container started
26m Warning Unhealthy Pod/coredns-559f6c778d-gx9h8 Readiness probe failed: Get "http://10.244.0.2:8181/ready": dial tcp 10.244.0.2:8181: connect: connection refused
26m Warning Unhealthy Pod/coredns-559f6c778d-gx9h8 Readiness probe failed: HTTP probe failed with statuscode: 503
Здесь вся история старта: планировщик ждал готовности узла, потом readiness-проба не достучалась до порта, а
затем получала 503, пока плагин kubernetes не синхронизировался (в логе пода
plugin/ready: Plugins not ready: "kubernetes"). На старте это обычный шум. describe pod
показывает события внизу, а выше то, чего в них нет:
| Поле describe | Что из него следует |
|---|---|
Last State, Reason, Exit Code | как умер прошлый экземпляр: OOMKilled, Error с кодом, Completed |
Restart Count | растёт - причина в логе прошлого экземпляра |
Ready, Conditions | пускают ли трафик; PodScheduled False - под не размещён |
Liveness, Readiness | фактические порт, путь, таймаут и пороги |
logs --previous отдаёт лог прошлого экземпляра, а kubelet хранит один мёртвый экземпляр на контейнер
(--maximum-dead-containers-per-container, по умолчанию 1): лог позапрошлого падения уже не достать. Без
перезапусков ответ такой:
$ kubectl logs -n kube-system coredns-559f6c778d-gx9h8 --previous
Error from server (BadRequest): previous terminated container "coredns" in pod "coredns-559f6c778d-gx9h8" not found
$ kubectl logs -n kube-system -l k8s-app=kube-proxy | wc -l
10
С селектором kubectl logs отдаёт по 10 последних строк с пода (справка к --tail): начало лога
так не увидеть.
kubectl debug и port-forward
В distroless нет shell (для Docker это разобрано в главе 2.2 статьи «Docker»). kubectl debug добавляет в
работающий под эфемерный контейнер через подресурс pods/ephemeralcontainers, не перезапуская его. Команды
раздела не запускались (эфемерный контейнер меняет под), поведение взято из kubectl explain и исходников
kubectl 1.37.
$ kubectl debug -it orders-7d9f-xk2 --image=busybox:1.37 --target=app -- sh
Эфемерный контейнер нельзя изменить или удалить, у него нет resources, портов и проб, после выхода он
остаётся в поде завершённым. Сеть у контейнеров пода общая, а --target=app добавляет пространство процессов
app: ps покажет сервис, его файлы видны в /proc/<pid>/root, если хватает
прав на ptrace. Остальное решает профиль:
--profile | Эфемерный контейнер | debug node/… |
|---|---|---|
general, по умолчанию с 1.36 | SYS_PTRACE | сеть, PID и IPC узла, корень узла в /host |
baseline, restricted | ничего; у restricted не root и drop: ALL | обычный под на узле |
netadmin | NET_ADMIN, NET_RAW | namespace узла без /host |
sysadmin | privileged | namespace узла, /host, privileged |
legacy был умолчанием до 1.36 и уйдёт в 1.39. Под node-debugger-… от debug node/
терпит все taint и после выхода остаётся, удаляют его руками. С general он не привилегирован, но
chroot /host вопреки документации пройдёт: право на chroot есть по умолчанию.
kubectl port-forward открывает поток через API-сервер (pods/portforward, с 1.31 по умолчанию
поверх WebSocket) до kubelet, а containerd в сетевом namespace пода подключается к localhost:<порт>
(sandbox_portforward_linux.go в containerd 2.3.4). Service, kube-proxy и политики в этом пути не участвуют,
и port-forward service/orders лишь выбирает один под по селектору.
Сервис на 127.0.0.1:8080 через port-forward доступен: containerd подключается к localhost
внутри пода. Соседи и Service ходят на IP пода и получают отказ (адрес слушателя разобран в главе 1.3 статьи «Linux и ОС»).
Вопросы
4no such host указывает на DNS, мгновенный отказ на эндпоинты или порт, таймаут на политику
или сеть.Звено за звеном
- Имя. Короткое
ordersнаходится только из своего namespace, из другого нужноorders.shop. Проверяй тем же резолвером, что у сервиса (GODEBUG=netdns=2,getent hosts), а неdig(глава 1.3 статьи «Linux и ОС»). - Эндпоинты.
kubectl get endpointslices -n shop -l kubernetes.io/service-name=orders: если пусто, селектор не совпал с метками, еслиreadyложно, под не проходит readiness. Сверь иtargetPort. - Узел.
iptables-save -t nat | grep 'shop/orders'покажет цепочку сервиса. - Политика.
kubectl get networkpolicyв обоих namespace: Egress клиента, Ingress сервера. - Процесс. Эфемерный контейнер с
--targetиnetstat -tlnпокажет адрес, который слушает сервис.
| Ошибка Go-клиента | Куда смотреть |
|---|---|
lookup orders on 10.96.0.10:53: no such host | имя и namespace |
lookup …: i/o timeout | до CoreDNS не доходят пакеты: default deny без allow-dns, сам CoreDNS |
connect: connection refused сразу | нет готовых эндпоинтов; процесс слушает не тот адрес или порт |
i/o timeout при соединении | NetworkPolicy, сеть между узлами |
Когда пакеты до сервера имён не доходят, Go 1.27.1 при ndots:5 вернул i/o timeout
через 40 секунд (опыт в Docker): четыре имени × attempts:2 × timeout:5. Если в ошибке
есть lookup, виноват DNS, а не сервис.
--target, падающий копией через
--copy-to без проб и меток, узел через debug node/. Нужны права на подресурсы и
разрешение Pod Security.Под жив
Эфемерный контейнер с --target=app видит порты, файлы процесса в /proc/<pid>/fd и
/debug/pprof/goroutine?debug=2 через wget localhost:…. Если на уровне пода задан
пользователь не root, capabilities профиля могут не подействовать, предупреждает справка kubectl debug.
Под падает
$ kubectl debug orders-7d9f-xk2 -it --copy-to=orders-debug --image=busybox:1.37 -- sh
Копия повторяет спецификацию пода и добавляет отладочный контейнер. Профили снимают с неё метки, аннотации и
пробы (init-контейнеры остаются: --keep-init-containers=true) и делают процессы общими. На копию не
пойдёт трафик Service, её не посчитает ReplicaSet, и liveness не убьёт зависший на старте процесс.
--set-image=app=… подменит образ. Удаляют копию руками. Для беды на узле есть
kubectl debug node/<узел> --profile=sysadmin и chroot /host.
Что может не пустить
- RBAC. kubectl шлёт PATCH на
pods/ephemeralcontainers, а во встроенныхeditиadmin1.37 этого подресурса нет, хотяpods/execиpods/portforwardесть (grepпо всем ClusterRole кластера). Для копии и узла нужно право создавать поды. - Pod Security Admission проверяет и
ephemeralcontainers(его нет вignoredPodSubresources), так чтоsysadminв namespace уровняbaselineотклонят, как и профиль по умолчанию.
Почему упало всё
Default deny на Egress блокирует и DNS: пакет к 10.96.0.10:53 после DNAT идёт на под CoreDNS, которого никто не
разрешил, и Go-резолвер ждёт до 40 секунд (вопрос 1). Первым добавляют allow-dns, по UDP и TCP. У
API-сервера hostNetwork, его эндпоинт на адресе узла, и podSelector его не
выберет, нужен ipBlock:
# orders-egress-api.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: orders-to-apiserver, namespace: default}
spec:
podSelector:
matchLabels: {app: orders}
policyTypes: [Egress]
egress:
- to: [{ipBlock: {cidr: 172.23.0.2/32}}]
ports: [{protocol: TCP, port: 6443}]
$ kubectl get endpointslices kubernetes \
-o jsonpath='{.endpoints[0].addresses[0]} {.ports[0].port}{"\n"}'
172.23.0.2 6443
$ kubectl apply --dry-run=server -f orders-egress-api.yaml
networkpolicy.networking.k8s.io/orders-to-apiserver created (server dry run)
Сверяет CNI ipBlock до трансляции адреса или после, документация не определяет, и судьбу открытых
соединений при новой политике тоже оставляет CNI: такое проверяют на своём.
И или ИЛИ
Два манифеста отличаются одним дефисом перед podSelector, и API понимает их по-разному:
$ kubectl apply --dry-run=server -f orders-from-api.yaml -o jsonpath='{.spec.policyTypes} {.spec.ingress[0].from}{"\n"}'
["Ingress"] [{"namespaceSelector":{"matchLabels":{"kubernetes.io/metadata.name":"shop"}},"podSelector":{"matchLabels":{"app":"api"}}}]
$ kubectl apply --dry-run=server -f orders-from-api-or.yaml -o jsonpath='{.spec.policyTypes} {.spec.ingress[0].from}{"\n"}'
["Ingress"] [{"namespaceSelector":{"matchLabels":{"kubernetes.io/metadata.name":"shop"}}},{"podSelector":{"matchLabels":{"app":"api"}}}]
В первом один элемент from: поды app: api из shop. Во втором два: все поды
shop и поды app: api из namespace политики. policyTypes в файлах нет, и API
записал ["Ingress"].
resourceNames и
RoleBinding, проверка через kubectl auth can-i --as. Токен обновляет kubelet, client-go перечитывает файл
сам, ломается клиент, прочитавший токен один раз.# orders-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata: {name: orders, namespace: default}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: {name: orders-config-reader, namespace: default}
rules:
- apiGroups: [""]
resources: [configmaps]
resourceNames: [orders-flags]
verbs: [get, list, watch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: {name: orders-config-reader, namespace: default}
subjects:
- {kind: ServiceAccount, name: orders, namespace: default}
roleRef: {apiGroup: rbac.authorization.k8s.io, kind: Role, name: orders-config-reader}
$ kubectl apply --dry-run=server -f orders-rbac.yaml
serviceaccount/orders created (server dry run)
role.rbac.authorization.k8s.io/orders-config-reader created (server dry run)
rolebinding.rbac.authorization.k8s.io/orders-config-reader created (server dry run)
resourceNames сужает права до одного объекта, но list и watch по документации
RBAC пройдут, только если в запросе есть селектор полей metadata.name с этим именем: в client-go это
fields.OneTermEqualSelector("metadata.name", "orders-flags").String() в ListOptions.FieldSelector.
Проверка и токен
Dry-run не покажет права роли, а auth can-i видит только существующие привязки. После применения
kubectl auth can-i watch configmaps/orders-flags -n default --as=system:serviceaccount:default:orders
должен ответить yes, а get secrets - no. rest.InClusterConfig()
в client-go 1.37 задаёт BearerTokenFile, и транспорт перечитывает файл раз в минуту, так что обновление
токена kubelet клиент не замечает. Самописный клиент, прочитавший токен при старте, не позже чем через час и семь
секунд попадёт в stale-token, а с --service-account-extend-token-expiration=false получит 401.
can-i отвечает yes, а информер получает 403
can-i watch configmaps/orders-flags спрашивает про объект по имени и проходит по
resourceNames. Информер без селектора полей просит все ConfigMap namespace, и роль его не пускает.
Расширять роль не нужно, нужно добавить селектор metadata.name.
Что стоит запомнить
- Service направляет трафик к выбранным Pod через сетевой dataplane кластера.
- NetworkPolicy действует только при поддержке сетевого плагина.
- ServiceAccount и RBAC определяют доступ Pod к API, а не доступ к обычной сети.
Частые ошибки
- Default deny egress может заблокировать DNS, даже если разрешение для приложения добавлено.
- Успешный DNS lookup не доказывает, что endpoint Service доступен.
Самопроверка
Сначала реши задачу самостоятельно, затем открой ответ и сравни ход рассуждений.
Чтение кода
1. Прочитай проверку RBAC
Проверка ServiceAccount reporter в namespace payments дала результаты:
get secrets: no
get pods: yesКакие два вывода можно сделать и какой нельзя?
Ответ и разбор
Этой учётке разрешено читать Pod в namespace payments и запрещено читать Secret в нём. Нельзя заключить, что Pod не может читать Secret, уже смонтированный в filesystem, или что доступа нет в других namespace; результат относится только к проверенному действию и области.
Код-ревью
2. Добавь DNS к egress allowlist
После включения default deny egress сервис не разрешает имена. Политика пропускает DNS только по TCP.
egress:
- to:
- namespaceSelector: {}
ports:
- {protocol: TCP, port: 53}
Ответ и разбор
Обычные DNS-запросы используют UDP/53; TCP нужен для крупных ответов и fallback. Добавь отдельные разрешения UDP и TCP/53 к DNS-pod или namespace с учётом labels кластера. Сверь labels CoreDNS и проверь поддержку NetworkPolicy сетевым плагином.
Применение
3. Раздели DNS и доступность Service
Имя orders.payments.svc.cluster.local разрешается в ClusterIP, но TCP connect к порту 8080 завершается timeout. Назови проверки по пути запроса, не меняя DNS.
Ответ и разбор
Проверь готовые EndpointSlices Service и соответствие selector labels, затем совпадение targetPort с listener контейнера. Проверь NetworkPolicy и правила dataplane, после чего проверь listener с самого Pod и клиента. DNS уже разрешил адрес; повторная правка DNS не объясняет timeout к ClusterIP.
Применение
4. Отладь образ без shell
Pod не содержит shell, но нужно проверить его filesystem и DNS. Какие варианты отладки есть и чего нельзя делать с рабочим Pod наугад?
Ответ и разбор
Сначала изучи Events и логи. Ephemeral debug container может разделять сетевое пространство Pod, но его filesystem не совпадает с root filesystem приложения. Для чтения файлов можно адресовать процесс целевого контейнера и посмотреть /proc/, если runtime поддерживает target process и хватает прав; иначе используй отдельный диагностический образ или штатную выгрузку файла. Не меняй рабочий image и не запускай privileged debug container без согласования.