diff --git a/apps/wave-0/gitea.yaml b/apps/wave-0/gitea.yaml index a89908e..067949e 100644 --- a/apps/wave-0/gitea.yaml +++ b/apps/wave-0/gitea.yaml @@ -185,6 +185,10 @@ spec: egress: - toEntities: - world + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: keycloak + app: keycloak --- # HTTPRoute gitea-3000 (R-TLS/R-DNS) apiVersion: gateway.networking.k8s.io/v1 diff --git a/apps/wave-0/jackett.yaml b/apps/wave-0/jackett.yaml index e90d27d..87a143e 100644 --- a/apps/wave-0/jackett.yaml +++ b/apps/wave-0/jackett.yaml @@ -184,6 +184,10 @@ spec: egress: - toEntities: - world + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: keycloak + app: keycloak --- # HTTPRoute jackett-9117 (R-TLS/R-DNS) apiVersion: gateway.networking.k8s.io/v1 diff --git a/apps/wave-0/pihole.yaml b/apps/wave-0/pihole.yaml index 34641b9..c0ca673 100644 --- a/apps/wave-0/pihole.yaml +++ b/apps/wave-0/pihole.yaml @@ -140,6 +140,10 @@ spec: egress: - toEntities: - world + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: keycloak + app: keycloak --- # HTTPRoute pihole-80 (R-TLS/R-DNS) apiVersion: gateway.networking.k8s.io/v1 diff --git a/apps/wave-0/qbittorrent.yaml b/apps/wave-0/qbittorrent.yaml index b587b31..02c66c6 100644 --- a/apps/wave-0/qbittorrent.yaml +++ b/apps/wave-0/qbittorrent.yaml @@ -184,6 +184,10 @@ spec: egress: - toEntities: - world + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: keycloak + app: keycloak --- # HTTPRoute qbittorrent-8080 (R-TLS/R-DNS) apiVersion: gateway.networking.k8s.io/v1 diff --git a/apps/wave-1/nextcloud.yaml b/apps/wave-1/nextcloud.yaml index f4edfc7..6f68a40 100644 --- a/apps/wave-1/nextcloud.yaml +++ b/apps/wave-1/nextcloud.yaml @@ -205,6 +205,10 @@ spec: egress: - toEntities: - world + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: keycloak + app: keycloak --- # HTTPRoute nextcloud-80 (R-TLS/R-DNS) apiVersion: gateway.networking.k8s.io/v1 diff --git a/apps/wave-1/radarr.yaml b/apps/wave-1/radarr.yaml index f1e3795..d8aa2b0 100644 --- a/apps/wave-1/radarr.yaml +++ b/apps/wave-1/radarr.yaml @@ -206,6 +206,10 @@ spec: egress: - toEntities: - world + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: keycloak + app: keycloak --- # HTTPRoute radarr-7878 (R-TLS/R-DNS) apiVersion: gateway.networking.k8s.io/v1 diff --git a/apps/wave-1/semaphore.yaml b/apps/wave-1/semaphore.yaml index 3b522e7..40b6388 100644 --- a/apps/wave-1/semaphore.yaml +++ b/apps/wave-1/semaphore.yaml @@ -209,6 +209,10 @@ spec: egress: - toEntities: - world + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: keycloak + app: keycloak --- # HTTPRoute semaphore-3000 (R-TLS/R-DNS) apiVersion: gateway.networking.k8s.io/v1 diff --git a/apps/wave-1/sonarr.yaml b/apps/wave-1/sonarr.yaml index dcd04bb..6518a9a 100644 --- a/apps/wave-1/sonarr.yaml +++ b/apps/wave-1/sonarr.yaml @@ -206,6 +206,10 @@ spec: egress: - toEntities: - world + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: keycloak + app: keycloak --- # HTTPRoute sonarr-8989 (R-TLS/R-DNS) apiVersion: gateway.networking.k8s.io/v1 diff --git a/catalog/render.py b/catalog/render.py index e6e81d3..ef64298 100644 --- a/catalog/render.py +++ b/catalog/render.py @@ -527,6 +527,28 @@ def r_internet_egress(s: dict) -> dict: switch (a cluster-wide singleton keyed on the egress-class pod label) decide the PATH; this policy decides that north-south egress is ALLOWED at all. """ + egress = [{"toEntities": ["world"]}] + # SSO apps (oidc/forward-auth) reach Keycloak at keycloak.home, which coredns split-horizon + # resolves to the shared Gateway VIP (an L2-announced LoadBalancer IP Cilium classifies as + # `world`), so the pod hairpins THROUGH the Gateway. Under Cilium >=1.16.8/1.17.2 (the + # CVE-2025-30162 fix, present in 1.20), the Gateway's L7 proxy re-evaluates THIS pod's egress + # policy against Keycloak's REAL backend cluster identity (enforce_policy_on_l7lb) — `world` + # matches the VIP at L3/L4 but NOT the resolved keycloak identity at L7, so OIDC discovery/ + # token 403s ("Access denied") without an explicit allow to the keycloak backend. Scope it to + # the apps that actually talk to Keycloak (auth mode oidc or forward-auth). + if s.get("auth", {}).get("mode") in ("oidc", "forward-auth"): + egress.append( + { + "toEndpoints": [ + { + "matchLabels": { + "io.kubernetes.pod.namespace": "keycloak", + "app": "keycloak", + } + } + ] + } + ) return { "apiVersion": "cilium.io/v2", "kind": "CiliumNetworkPolicy", @@ -537,7 +559,7 @@ def r_internet_egress(s: dict) -> dict: }, "spec": { "endpointSelector": {"matchLabels": {"app.kubernetes.io/name": s["name"]}}, - "egress": [{"toEntities": ["world"]}], + "egress": egress, }, } diff --git a/platform/crafty/networkpolicy.yaml b/platform/crafty/networkpolicy.yaml index 5389889..c5b1282 100644 --- a/platform/crafty/networkpolicy.yaml +++ b/platform/crafty/networkpolicy.yaml @@ -80,7 +80,10 @@ spec: # Bedrock binaries) when the operator creates/updates a server. # - The oauth2-proxy sidecar must reach Keycloak at keycloak.home.keithjackson.dev, which coredns # split-horizon resolves to the Gateway VIP (an L2-announced LoadBalancer IP Cilium classifies as -# `world`) — so `world` egress covers both the jar downloads AND the OIDC path (same as headlamp). +# `world`); the sidecar hairpins THROUGH the shared Gateway. `world` covers the jar downloads and +# the VIP at L3/L4, but under Cilium >=1.16.8/1.17.2 (CVE-2025-30162 fix, in 1.20) the Gateway's +# L7 proxy re-checks this egress against Keycloak's REAL backend identity (enforce_policy_on_l7lb), +# so the OIDC path 403s ("Access denied") without the explicit keycloak toEndpoints allow below. # - The sidecar -> Crafty hop is pod-local (127.0.0.1:8443) and is NOT subject to network policy. apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy @@ -96,3 +99,7 @@ spec: egress: - toEntities: - world + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: keycloak + app: keycloak diff --git a/platform/headlamp/networkpolicy.yaml b/platform/headlamp/networkpolicy.yaml index 2908fc3..75f4051 100644 --- a/platform/headlamp/networkpolicy.yaml +++ b/platform/headlamp/networkpolicy.yaml @@ -53,9 +53,12 @@ spec: # Egress allow: Headlamp's backend must reach (a) the kube-apiserver — it forwards each user's OIDC # id_token there for per-user RBAC — and (b) Keycloak for the OIDC auth-code/token/JWKS exchange. # Keycloak is reached at keycloak.home.keithjackson.dev, which coredns split-horizon resolves to the -# Gateway VIP (an L2-announced LoadBalancer IP that Cilium classifies as `world`), so `world` egress -# covers the Keycloak path (same as the gitea OIDC bootstrap). DNS is already allowed by the -# default-deny baseline above. +# Gateway VIP (an L2-announced LoadBalancer IP that Cilium classifies as `world`); the pod hairpins +# THROUGH the shared Gateway. `world` matches the VIP at L3/L4, but under Cilium >=1.16.8/1.17.2 (the +# CVE-2025-30162 fix, present in 1.20) the Gateway's L7 proxy re-evaluates this egress against +# Keycloak's REAL backend cluster identity (enforce_policy_on_l7lb) — so `world` alone 403s the OIDC +# path ("Access denied"). The explicit toEndpoints allow to the keycloak backend below is required. +# DNS is already allowed by the default-deny baseline above. apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: @@ -71,3 +74,7 @@ spec: - toEntities: - kube-apiserver - world + - toEndpoints: + - matchLabels: + io.kubernetes.pod.namespace: keycloak + app: keycloak