fix(network): allow CoreDNS replies through the database ingress allowlist - #4147
Conversation
…wlist
database-ingress-allowlist sets endpointSelector {} (default-deny ingress) but
omits kube-system, so Cilium drops rev-NAT'd DNS replies to every pod in the
namespace. Live on control-3, endpoint 1100 = pgbouncer-rw-7ff5f7bdb4-rfwgp:
drop (Policy denied) bpf_lxc.c:2472, identity 19145->9091:
10.43.0.10:53 -> 10.42.2.107:38066 tcp SYN, ACK
identity 19145 = k8s-app=kube-dns. A reply from a pod IP matches the outbound
conntrack entry and bypasses policy; only the rev-NAT'd ClusterIP reply is
re-evaluated as a new ingress flow, which is why direct-to-pod queries succeed
while the ClusterIP times out on both UDP and TCP.
Which pods are hit is decided by service backend pinning: rfwgp is pinned to
BackendID 994 = 10.42.2.109, the CoreDNS pod on its own node, and conntrack
shows TxFlagsSeen=0x02 RxFlagsSeen=0x00 Packets=0. Poolers pinned to a remote
backend are unaffected, which is why deleting the pod has always looked like a
fix. Cilium cannot match a source port, so no toPorts is possible.
Downstream: crowdsec-lapi CrashLoopBackOff on 'cscli machines add', plus
radarr/sonarr/prowlarr/bazarr taking server_login_retry through pgbouncer-rw.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe database ingress CiliumNetworkPolicy now permits DNS traffic from kube-dns pods in the kube-system namespace and documents reverse-NAT behavior and the absence of port-specific filtering. ChangesDatabase DNS ingress
Estimated code review effort: 1 (Trivial) | ~2 minutes ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 Checkov (3.3.8)kubernetes/apps/kube-system/network-policies/app/database-ingress.yamlTraceback (most recent call last): Comment |
@@ spec.ingress @@
# cilium.io/v2/CiliumNetworkPolicy/database/database-ingress-allowlist
! - one list entry removed:
- - fromEndpoints:
- - matchLabels:
- io.kubernetes.pod.namespace: ai
- - matchLabels:
- io.kubernetes.pod.namespace: default
- - matchLabels:
- io.kubernetes.pod.namespace: media
- - matchLabels:
- io.kubernetes.pod.namespace: observability
- - matchLabels:
- io.kubernetes.pod.namespace: security
- - matchLabels:
- io.kubernetes.pod.namespace: database
! + one list entry added:
+ - fromEndpoints:
+ - matchLabels:
+ io.kubernetes.pod.namespace: ai
+ - matchLabels:
+ io.kubernetes.pod.namespace: default
+ - matchLabels:
+ io.kubernetes.pod.namespace: media
+ - matchLabels:
+ io.kubernetes.pod.namespace: observability
+ - matchLabels:
+ io.kubernetes.pod.namespace: security
+ - matchLabels:
+ io.kubernetes.pod.namespace: database
+ - matchLabels:
+ io.kubernetes.pod.namespace: kube-system
+ k8s-app: kube-dns
|
5034095 to
e3d4302
Compare
Reverts e4e3645 (#4135). It was added to force PgBouncer DNS over TCP, but PgBouncer never uses TCP for DNS here. Observed across three pooler pods on two nodes, sampled every 2s for 50s: exactly one connected UDP socket to 10.43.0.10:53 and zero TCP sockets to :53, socket inode unchanged throughout. pgbouncer 1.25.1 resolves through libcares 1.34.5. The option is not inert, though: it applies to every other process in the pod. The pooler's pid 1 is the Go /controller/manager, and Go's net dnsconfig parser honours use-vc, as does glibc NSS. Under the CoreDNS reply drops fixed in #4147 TCP is the worse transport, because a denied SYN,ACK wedges the caller through the full TCP SYN retry ladder while a dropped UDP reply retries in 1-2s. So this removes an aggravating factor, not dead config. Note the mechanism by which c-ares ignores use-vc is not established: c-ares 1.34.5 does parse the option and pgbouncer never passes ARES_OPT_FLAGS to override it. The socket observation is the evidence, not a source-level explanation.
Root cause
database-ingress-allowlist(#4072) setsendpointSelector: {}, making ingress default-deny for every pod indatabase. The allowlist omitskube-system, so Cilium drops rev-NAT'd CoreDNS replies. Captured live on control-3, endpoint 1100 =pgbouncer-rw-7ff5f7bdb4-rfwgp:cilium-dbg identity get 19145→k8s-app=kube-dns,namespace=kube-system.A reply from a pod IP matches the outbound conntrack entry and bypasses policy evaluation. Only the rev-NAT'd ClusterIP reply is re-evaluated as a new ingress flow carrying the CoreDNS identity. That is why direct-to-pod DNS succeeds in ~1ms while the ClusterIP times out on both UDP and TCP.
Cilium cannot match a source port (#19965) — the reply's destination is the client's ephemeral port — so
toPorts: [53]would match the wrong field and silently do nothing. Full-port ingress from the kube-dns identity is the floor.Why only some pods
Service backend pinning.
rfwgpis pinned to BackendID 994 =10.42.2.109, the CoreDNS pod on its own node; conntrack showsTxFlagsSeen=0x02 RxFlagsSeen=0x00 Packets=0— the reply dies at ingress before it can ever update conntrack. Poolers pinned to a remote backend are unaffected, which is why deleting the pod has always looked like a fix.Measured:
Blast radius
crowdsec-lapiCrashLoopBackOff oncscli machines add; radarr/sonarr/prowlarr/bazarr takingserver_login_retrythroughpgbouncer-rw; the CNPG primary and operator intermittently losing DNS.Scope
Cluster-scoped policy object only. Zero pods restarted, instantly reversible. The #4135 (
use-vc) revert was originally bundled here and has been split into #4149 — it mutatestemplate.specand would roll all 7 pooler pods, which would both inherit rollout risk into an urgent fix and destroy this PR's verification (see below).Reviewed and rejected
An earlier revision also added
host,remote-node,healthtofromEntitiesto protect kubelet probes on a future worker node. Dropped —AllowLocalhost: alwaysis Cilium's runtime default under k8s and already inserts an unconditional host-identity ingress allow into every endpoint's policy map, verified on a namespace with no CNP granting it. Kubelet only probes pods on its own node, so probes always arrive as the local host identity.healthtargets node health endpoints, never app pods.Follow-ups, not in this PR
fromEntities: [kube-apiserver]resolves to identity 7 ={reserved:kube-apiserver, reserved:remote-node}on this collapsed control plane, so it already grants every host-namespace process on every other node ANY-port ingress to everydatabasepod. Scoping totoPorts: 9443(cnpg webhook) would tighten it; counters support it (identity 7 → postgres = 0 bytes).hubble.enabled: false). feat(kube-system): add cilium network segmentation policies #4072's own body specified verification viahubble observe --namespace database --verdict DROPPED— a command that has never been runnable in this cluster. Agent-only Hubble (no relay, no UI) is the cheap fix and would have surfaced this drop immediately.Verification
kustomize buildrenders clean.kube-system/network-policiesis not suspended and reportsApplied revision: main@bc287c014, so this applies on merge.Post-merge: policy drops for identity 19145 stop, and the poolers recover without deletion. That last point is the signal — do not merge #4149 until it is confirmed, since a pooler roll would reset the pinned backends and make the result unreadable.
Summary by CodeRabbit