--- # Namespace baseline: deny everything both ways, except DNS to kube-system. # # SINGLE OWNER. This namespace is not labelled app-namespace=true, so Kyverno's # require-default-deny-baseline generate rule does not select it and never writes this object; # Flux is its only writer (the same posture as kured / crafty / headlamp / homepage). Do NOT add # the app-namespace label to namespace.yaml without deleting this object in the same change, or # the two controllers fight over it every reconcile (the 2026-09-30 drift-loop root cause). apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny namespace: cloudflared labels: homelab.keithjackson.dev/baseline: default-deny spec: podSelector: {} policyTypes: - Ingress - Egress ingress: [] egress: - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system ports: - protocol: UDP port: 53 - protocol: TCP port: 53 --- # The ONLY ingress: Prometheus scraping :2000. Shipped in the same file as the baseline on purpose # (the Longhorn/kured lesson): a deny-all without this allow leaves the ServiceMonitor's targets # DISCOVERED and every scrape timing out, which is the state the CloudflaredTunnelDown rule is # written to fire on rather than go quiet in. apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: cloudflared-allow-prometheus-scrape namespace: cloudflared labels: app.kubernetes.io/name: cloudflared spec: podSelector: matchLabels: app.kubernetes.io/name: cloudflared policyTypes: - Ingress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: monitoring ports: - protocol: TCP port: 2000 --- # Egress: the Cloudflare edge, the shared Gateway, and the Gateway's backends — nothing else. # # 1. world tcp+udp/7844 — the tunnel itself (QUIC on udp, HTTP/2 fallback on tcp). # world tcp/443 — Cloudflare's API / update endpoints that cloudflared calls at start. # Expressed as `world` + port rather than Cloudflare CIDRs: the edge ranges are Cloudflare's to # change, and a stale CIDR list would fail as a silent outage. Narrowing to toCIDR is a # possible later hardening; it is not done blind. # # 2. `ingress` entity tcp/443 — the shared Gateway's Envoy. Traffic to the # cilium-gateway-shared Service is handed to Envoy, whose identity is reserved:ingress. # # 3. The Gateway's BACKENDS, by namespace. NOT optional: Cilium >= 1.16.8 (CVE-2025-30162; this # cluster runs 1.20.x) makes the Gateway's Envoy re-evaluate THIS pod's egress policy against # the identity of the backend it routes the request to. With only rule 2, Envoy accepts the TCP # connection and then answers every request `403 Access denied` — measured for buildkit -> # gitea on 2026-10-30, see platform/buildkit/networkpolicy.yaml. So the backends are named: # * any namespace opted in to the shared Gateway (gateway-access=shared) — which is the set # of namespaces that can serve a route at all, so this tracks new apps automatically; # * MINUS the LAN/VPN-only services (operator decisions 2026-11-08): plex, jackett, pihole, # semaphore, qbittorrent, radarr, sonarr, headlamp. config.yaml already 404s their # hostnames; this is the second, independent layer: even a request that got past the # ingress rules would be refused by Envoy for these backends. # unifi and crafty-mc are not here because they are L4 LoadBalancer VIPs, not Gateway # backends; the crafty WEB UI (crafty., an HTTPRoute) is deliberately reachable. # # WHY NOT the `- {}` allow-all egress gitea-actions uses (apps/wave-1/gitea-actions.yaml)? # It would satisfy the L7 re-check just as well, but `{}` admits EVERY identity, including # kube-apiserver and host — which operator decision 4 (the Kubernetes API is never reachable # through the tunnel) rules out for the one pod that carries internet traffic in. The # namespace-label selector is the narrowest rule that still names every possible backend. # Verified 2026-11-08: Cilium identities carry `k8s:io.cilium.k8s.namespace.labels. # gateway-access`, and every namespace with an HTTPRoute on the shared Gateway is labelled # gateway-access=shared. # # BACKENDS NEED NO INGRESS CHANGE. Envoy forwards to a backend as reserved:ingress, which every # backend already admits; the source-identity check happens on THIS pod's egress, not on the # destination's ingress. Measured 2026-10-30 (decisions/2026-10-30-gitea-admit-buildkit.md): # gitea-actions is in none of Gitea's ingress allows and its requests through the Gateway reach # Gitea, while Gitea's endpoint shows only a `reserved:ingress` allow row. So, unlike a # ClusterIP client, cloudflared is not added to any app's inClusterClients. That also means # this rule does not open a direct cloudflared -> app pod path into any app namespace whose # ingress admits only reserved:ingress and named peers. # # DELIBERATELY ABSENT: kube-apiserver, host, remote-node, cluster, all. The Kubernetes API stays # local-only (operator decision 2026-11-08); conformance/tests/l0-static/cloudflared-ingress-rules # fails the gate if any of those entities appears here. apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: name: cloudflared-allow-egress namespace: cloudflared labels: app.kubernetes.io/name: cloudflared spec: endpointSelector: matchLabels: app.kubernetes.io/name: cloudflared egress: - toEntities: - world toPorts: - ports: - port: "7844" protocol: TCP - port: "7844" protocol: UDP - port: "443" protocol: TCP - toEntities: - ingress toPorts: - ports: - port: "443" protocol: TCP - toEndpoints: - matchExpressions: - key: io.cilium.k8s.namespace.labels.gateway-access operator: In values: - shared - key: io.kubernetes.pod.namespace operator: NotIn values: - plex - jackett - pihole - semaphore - qbittorrent - radarr - sonarr - headlamp - vuln-scanner