This episode discusses domain name (FQDN) based policy: how the Cilium DNS proxy works, egress rules with toFQDNs, TTL and cache management, and the practice of allowing outbound access only to specific domains. You will also learn to verify FQDN rules with Hubble.

The egress policies we created in episode 6 are still IP-based — and that is a problem. Many applications communicate with external services through domain names such as api.external.com, whose IP addresses can change at any time. Writing a policy against a domain's IP will break as soon as the DNS changes. Episode 10 answers this problem with DNS-based policy (FQDN).
Cilium handles DNS with a DNS proxy: all DNS requests from pods pass through the proxy, which records the requested domain name and the resolved IP addresses. From this record, toFQDNs policies can allow egress to specific domains — not to IPs. This is a far more stable and human way to write rules.
The DNS proxy is active by default in modern Cilium. When a pod makes a DNS query, the request does not go directly to the upstream server — it passes through the Cilium proxy first. The proxy forwards the query, stores the result, and tells the data plane which IP addresses are associated with which domain name.
With this data, a policy can read: "allow the frontend pod to egress to api.external.com". Cilium translates this into a rule that allows egress to whatever IP the domain currently resolves to. When the domain's IP address changes, the proxy updates its rules automatically.
It must be emphasized that the DNS proxy does not change the resolution result. It only adds a layer of observation and control between the pod and the DNS server. If the pod asks about api.external.com, the proxy still forwards the query to the real DNS server and returns the same answer — what differs is that Cilium now knows which domain name the pod uses.
An additional benefit of this approach: the proxy can enforce DNS policy itself. In recent versions, you can restrict DNS queries to a specific list of domains (DNS policy), so even before a connection is made, a pod cannot ask about a forbidden domain. This is an additional defense layer beyond the network policy.
Verify that the DNS proxy is active in your cluster:
cilium status | grep -i dnscilium status | grep -i dns must show a line mentioning the active DNS proxy. If not, enable it with --set dnsProxy.enable=true at install time.
FQDN rules are written inside the egress block with the toFQDNs field. Each item contains the allowed domain name. An example policy allowing the frontend pod to access an external API and the DNS server:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-fqdn
spec:
endpointSelector:
matchLabels:
app: frontend
egress:
- toFQDNs:
- matchName: api.external.com
- matchPattern: "*.external.com"
toPorts:
- ports:
- port: "443"
protocol: TCP
- toEndpoints:
- matchLabels:
io.kubernetes.pod.namespace: kube-system
matchLabels:
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDPmatchName: api.external.com matches a domain name exactly, while matchPattern: "*.external.com" matches subdomains. The second block allows access to the cluster's CoreDNS so DNS resolution keeps working — without it, the pod cannot make DNS queries at all.
The DNS resolution results are stored by the proxy in a cache shared with the data plane. Every cache entry has a lifetime (TTL) taken from the upstream DNS response. When the TTL expires, the proxy re-resolves and updates the allowed IP addresses.
It is important to understand: as long as a domain keeps being queried, its addresses stay fresh and policy works normally. However, if a domain is never queried within its TTL, the cache entry can expire and egress traffic to the old address will be rejected — this is the cause of many "suddenly cannot access" bugs in production. To prevent it, make sure the application uses repeated DNS resolution (TDD or cache with refresh), not storing the IP forever.
Let us combine all the concepts in one real scenario: the frontend pod may only access api.external.com over HTTPS, and all other access must be blocked. Apply the policy above, then verify with Hubble:
hubble observe --namespace default --type to-endpoint
hubble observe --verdict DROPPED --namespace defaulthubble observe --namespace default --type to-endpoint shows the outgoing flows; note that the destination addresses are the resolved IPs of api.external.com. If you try to access another domain, hubble observe --verdict DROPPED will show the rejection with the related policy drop reason.
For a quick test directly from the pod:
kubectl exec frontend -- curl -s -o /dev/null -w "%{http_code}" https://api.external.com
kubectl exec frontend -- curl -s -o /dev/null -w "%{http_code}" https://www.google.comkubectl exec frontend -- curl https://api.external.com must succeed, while access to any other domain must fail because it is not allowed by the FQDN policy.
When a domain suddenly becomes unreachable, do not guess — look at what is happening in the DNS proxy and the data plane. The two Hubble commands below answer the most common questions:
hubble observe --dns --since 15m
hubble observe --verdict DROPPED --since 15mhubble observe --dns --since 15m shows all DNS queries passing through the proxy, complete with the requested domain. hubble observe --verdict DROPPED --since 15m shows the rejected flows along with their drop reasons. If the queried domain does not appear in the DNS logs, the problem is not the policy — the application did not even resolve.
Note that events in these two layers can differ in order: a flow can be dropped because the FQDN policy does not yet recognize the resolved address (cache not ready), or because the address expired from the cache (TTL ended). Comparing the times in both Hubble outputs gives a clue about which one happened.
kubectl exec -n kube-system -it ds/cilium -- cilium-dbg bpf fqdn cache listcilium-dbg bpf fqdn cache list shows the contents of the FQDN cache in the data plane: domain name, IP addresses, and expiry. If the domain you want to access is not in the cache, the application has never queried that domain within the TTL — this goes back to the practical rule from the TTL section: make sure the application does repeated resolution, not storing the IP forever.
Warning
Do not use toFQDNs for domains whose addresses change frequently without making sure the application re-resolves. The correct pattern is for the application to do periodic DNS lookups and use matchName for stable domains or matchPattern for subdomain blocks.
Key takeaways:
toFQDNs allows egress based on domain names, not IPs.matchName for exact domains; matchPattern for subdomain wildcards.hubble observe --verdict DROPPED proves that access outside the domains is blocked.In the next episode 11, we will discuss bandwidth management and QoS — the eBPF-based bandwidth manager, pacing with BBR for high throughput, the kubernetes.io/egress-bandwidth annotation, and how to verify rate limits and their impact on latency. This turns QoS from a concept into measurable numbers.