Cilium¶
Cilium is the CNI, replaces kube-proxy, announces the LoadBalancer VIPs on L2 and applies
the CiliumNetworkPolicies, in audit mode. It runs in kube-system from the Helm chart
in kustomization.yaml with
values.yaml. Cilium's own Gateway API
support is off; Traefik handles ingress (traefik.md).
Bootstrap¶
Talos boots with cni: none and proxy.disabled, so no node becomes Ready until Cilium
runs. Terraform installs the first release from the same values.yaml and then applies the
IP pool and L2 policies with kubectl
(k8s-cilium.tf). It
ignores later changes to version and values, and Argo CD owns the release from then on.
See ../cluster/talos.md.
Settings that tie Cilium to Talos:
k8sServiceHost: localhost,k8sServicePort: 7445: the API through KubePrism.ipam.mode: kubernetes: Talos assigns the node pod CIDRs.ipv4NativeRoutingCIDR: 10.244.0.0/16must equal the cluster pod subnet. That is the Talos default;talos-cluster.tfdoes not override it. Native routing is required for the DSR load balancer mode (loadBalancer.mode: dsr,algorithm: maglev).bpf.hostLegacyRouting: true: Talos forwards kube-dns to the host DNS, which breaks without it.cgroup.autoMount.enabled: falsewithhostRoot: /sys/fs/cgroup: reuse the Talos mount.- The agent's capability list leaves out
SYS_MODULE; Talos does not let workloads load kernel modules. - The operator tolerates every taint (
operator.tolerations: [{operator: Exists}]). Nodes stay NotReady until Cilium runs, so the operator has to schedule on NotReady nodes during bootstrap. It runs two replicas with a PDB.
L2 announcements and LB IPAM¶
CiliumLoadBalancerIPPool default-pool hands out 10.3.10.100/29
(loadbalancer-ippool.yaml). The
assigned VIPs are listed in README.md. Services pin their address with the
lbipam.cilium.io/ips annotation.
Three CiliumL2AnnouncementPolicy objects
(l2-announcement-policy.yaml)
decide which nodes answer ARP for a VIP:
| Policy | Services | Nodes |
|---|---|---|
traefik-public-l2-policy |
label network.nordbye.it/vip: public |
workers |
traefik-private-l2-policy |
label network.nordbye.it/vip: private |
control planes |
default-l2-policy |
every Service without the label | all |
The Traefik VIPs get disjoint node sets so one bad node cannot take both gateways down. A new LoadBalancer Service with no label falls under the default policy.
After any change that rolls the agent DaemonSet, check the BPF LB map on each node, see
../observability/incidents.md.
Hubble¶
Hubble, the relay and the UI are on. The UI is at hubble.local.bigd.no on the private
gateway (httproute-hubble.yaml).
Hubble metrics, including policy verdicts per workload and namespace, go to Prometheus on
port 9965; agent, operator and Envoy metrics are on 9962, 9963 and 9964.
Hubble's TLS certificates are issued in-cluster by certgen (hubble.tls.auto.method:
cronJob), not by Helm, which would mint a new CA on every render and rotate it under the
agents on each Argo CD sync. Leaves are valid for 365 days (certValidityDuration) and
reissued by the cron schedule 0 0 1 */4 *. The leaf lifetime must end before the CA's
(tls.ca, three years) or certgen refuses to issue.
Network policies¶
Every app namespace except home-assistant, and most infra namespaces, has a
ciliumnetworkpolicy.yaml. List them with kubectl get ciliumnetworkpolicy -A.
policyAuditMode: true is a single agent-wide setting: drops are logged as AUDIT
verdicts in Hubble and counted in hubble_policy_verdicts_total, not enforced. The move to
enforcement is tracked in docs/backlog/README.md.
Cilium default-denies a direction only once a policy has a rule for it. An ingress-only policy leaves egress open.
A new app's policy, copied from the one closest to it:
- Ingress from Traefik on the container port. Every app uses the same selector:
- fromEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": traefik
"k8s:app.kubernetes.io/instance": traefik-traefik
toPorts:
- ports:
- port: "8080"
protocol: TCP
- Ingress from the KEDA interceptor as well if the app scales to zero, and always
fromEntities: [host, health]for probes.it-tools/ciliumnetworkpolicy.yamlhas all three. - No egress rules if the app needs broad outbound access, as
verksted/ciliumnetworkpolicy.yaml. - For restricted egress, allow DNS to
kube-dnswith an L7dnsrule and thentoFQDNsfor the external names.external-dns/ciliumnetworkpolicy.yamlis the template. Without therules.dnsblock Cilium never learns the addresses behind a name, and thetoFQDNsrule matches nothing. - Prometheus scrapes come from pods labelled
app.kubernetes.io/name: prometheusinmonitoring.
Check a new policy by looking for audited flows involving the app, in Hubble or with this query in Prometheus:
sum by (source, destination, direction) (increase(hubble_policy_verdicts_total{action="audit", destination_namespace="<ns>"}[1h]))
In that metric destination is the pod the policy applies to and source is the peer, in
both directions.