# Cilium egress to a domain: CiliumNetworkPolicy, the DNS proxy and toFQDNs

> How a CiliumNetworkPolicy lets a Pod reach one external domain: the DNS rule that sends lookups through Cilium's proxy, the toFQDNs rule that turns answers into allowed addresses, identities, and Hubble showing each verdict.

Interactive graph: https://k8wiz.com/graphs/cilium-tofqdns-dns-proxy-egress-policy

A NetworkPolicy allows addresses, and the addresses of an outside API change. Cilium's `toFQDNs` lets you write the name instead. Cilium learns which addresses the name has by watching the Pod's own DNS lookups, and allows only those.

The policy below lets `payments` reach `api.stripe.com` on port 443 and the `ledger` Service in another namespace, and nothing else.

## Resources

### Pod payments-6d9f7-…

created by a controller, v1, namespace shop

The source of the traffic. The policy selects it by its `app: payments` label.

### CiliumEndpoint payments-6d9f7-…

created by a controller, cilium.io/v2, namespace shop

Created by the agent for each Pod it manages. It holds the Pod's addresses, its numeric identity and whether ingress and egress policy are enforced for it.

### CiliumIdentity 48213

created by a controller, cilium.io/v2

A number that stands for one set of labels. Every Pod with the same labels shares it, and the datapath matches packets on this number, not on Pod IPs.

### CiliumNetworkPolicy payments-egress

you write it, cilium.io/v2, namespace shop

Three egress rules. The first allows DNS to CoreDNS and, through `rules.dns`, sends it via the DNS proxy. The second allows port 443 to whatever `api.stripe.com` resolved to. The third allows the ledger. Everything else from the Pod is dropped.

### DaemonSet cilium

comes with an add-on, apps/v1, namespace kube-system

The agent on each node. It loads the eBPF programs that enforce policy, runs the DNS proxy and keeps the cache of names and addresses that `toFQDNs` rules read.

### Deployment coredns

comes with an add-on, apps/v1, namespace kube-system

Answers the lookup. The Pod still addresses CoreDNS; the agent's proxy sits in the path, reads the question and the answer, and passes both on.

### External API api.stripe.com:443

outside the cluster

The one site the Pod may reach. Its addresses come and go, which is why the policy names it.

### Any other site example.org:443

outside the cluster

Resolves fine, since the DNS rule allows every name. The connection is dropped, because no rule allows the address it resolved to.

### Service ledger

you write it, v1, namespace finance

An internal dependency in another namespace. Once a policy has egress rules, it needs its own `toEndpoints` rule or it is dropped with the rest.

### Deployment hubble-relay

comes with an add-on, apps/v1, namespace kube-system

Collects the flows each agent records. `hubble observe --verdict DROPPED` shows which connection was denied, from which Pod, to which name and port.

## How the resources connect

- CiliumNetworkPolicy payments-egress → Pod payments-6d9f7-…: endpointSelector (selects)
- Pod payments-6d9f7-… → CiliumEndpoint payments-6d9f7-…: agent creates (creates)
- CiliumEndpoint payments-6d9f7-… → CiliumIdentity 48213: identity (references)
- DaemonSet cilium → CiliumNetworkPolicy payments-egress: enforces (references)
- Pod payments-6d9f7-… → DaemonSet cilium: DNS via proxy (traffic)
- DaemonSet cilium → Deployment coredns: forwards lookup (traffic)
- Pod payments-6d9f7-… → External API api.stripe.com:443: 443, learned IPs (traffic)
- Pod payments-6d9f7-… → Any other site example.org:443: dropped (traffic)
- Pod payments-6d9f7-… → Service ledger: 8080 (traffic)
- Deployment hubble-relay → DaemonSet cilium: reads flows (references)

## How it works

1. **The policy selects the Pod.** The agent sees that `payments-egress` selects the endpoint and switches its egress to default deny. `kubectl get ciliumendpoints -n shop` shows egress enforcement as enabled.
2. **The lookup goes through the DNS proxy.** The Pod asks CoreDNS for `api.stripe.com`. Because the rule for port 53 has a `rules.dns` block, the agent redirects the query to its proxy, which checks the name against the patterns and forwards it.
3. **The answer becomes an allowed address.** The proxy reads the addresses in the answer, stores them against the name, and updates the datapath before it hands the answer to the Pod. By the time the application connects, those addresses match the `toFQDNs` rule.
4. **The connection is allowed.** The Pod opens port 443 to one of the learned addresses and the packet passes. A connection to the same address on another port is still dropped.
5. **Everything else is dropped.** `example.org` resolves, but its addresses match no rule. The ledger is reached through its own `toEndpoints` rule, by identity and not by address.
6. **Hubble shows the verdicts.** `hubble observe --namespace shop --protocol dns` lists the queries the proxy saw, and `--verdict DROPPED` lists the connections that were denied.

## Common mistakes

- Without `rules.dns` on the port 53 rule, DNS works and the proxy never sees it. `toFQDNs` learns nothing and every connection to the domain is dropped.
- `matchName` is exact: `stripe.com` doesn't cover `api.stripe.com`. `matchPattern: "*.stripe.com"` covers subdomains and not the bare domain.
- The Pod can only reach addresses it looked up itself while the policy was in place. An application that caches an address across a restart of the agent can find it dropped until it resolves again.
- A rule with `toFQDNs` can't also hold `toEndpoints` or `toCIDR`. Put them in separate rules of the same `egress` list.
- Narrowing `rules.dns` to one name makes every other lookup fail with REFUSED, in-cluster names included. Keep a pattern for `*.cluster.local` or allow `*`.

## The manifests you write

```yaml
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: payments-egress
  namespace: shop
spec:
  endpointSelector:
    matchLabels:
      app: payments
  egress:
    - toEndpoints:
        - matchLabels:
            k8s:io.kubernetes.pod.namespace: kube-system
            k8s:k8s-app: kube-dns
      toPorts:
        - ports:
            - port: "53"
              protocol: ANY
          rules:
            dns:
              - matchPattern: "*"
    - toFQDNs:
        - matchName: api.stripe.com
      toPorts:
        - ports:
            - port: "443"
              protocol: TCP
    - toEndpoints:
        - matchLabels:
            k8s:io.kubernetes.pod.namespace: finance
            app: ledger
      toPorts:
        - ports:
            - port: "8080"
              protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: ledger
  namespace: finance
spec:
  selector:
    app: ledger
  ports:
    - port: 8080
```
