Compare commits

..
Author SHA1 Message Date
claude-botandClaude Opus 5.5 58196acebc fix(authentik): embedded outpost config lives in the blueprint
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
The embedded outpost's config is Authentik's own and does not depend on
authentik-gate-sync. A blueprint update of an existing outpost is partial
(importer: partial=True unless MUST_CREATED), so the entry sets config
without providers and the gates the controller attached stay.

config is replaced whole; the entry mirrors the live row
(authentik_outposts_outpost, 2026-10-23) with refresh_interval
minutes=1. kubernetes_namespace stays default: the outpost has the
Local Kubernetes Cluster connection, and the authentik namespace would
let the worker create an ak-outpost Service for it.

The controller drops embedded_config/ensure_embedded_config and only
attaches providers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 19:00:21 +02:00
claude-botandClaude Opus 5.5 95f3ff86b9 fix(authentik-gate-sync): refresh the embedded outpost every minute
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
Each authentik-server replica's embedded outpost loses its websocket whenever gunicorn recycles the worker holding it (AUTHENTIK_WEB__MAX_REQUESTS). The reconnect backoff never resets (goauthentik/authentik#26463), so a replica then misses gate changes for up to 5 minutes and catches up only on the refresh interval. A 1-minute interval bounds that gap; worker recycling stays as is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:19:49 +02:00
31 changed files with 87 additions and 1037 deletions
@@ -45,7 +45,6 @@ INGRESS_NS = os.environ.get("INGRESS_NAMESPACE", "ingress")
DEFAULT_AUTHZ_FLOW = os.environ.get("DEFAULT_AUTHORIZATION_FLOW", "default-provider-authorization-implicit-consent")
INVALIDATION_FLOW = os.environ.get("INVALIDATION_FLOW", "default-provider-invalidation-flow")
COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN", "renner.uno")
AK_BROWSER_URL = os.environ.get("AUTHENTIK_HOST_BROWSER", "https://auth.renner.uno/")
INTERVAL = int(os.environ.get("INTERVAL_SECONDS", "60"))
HTTP_TIMEOUT = 20
@@ -209,14 +208,6 @@ def resolve_backend(gate, service):
return sp["port"], selector, target
def embedded_config(config):
"""The embedded outpost's config with the host the gates depend on. The
embedded outpost builds the sign-in redirect from authentik_host, so it is
the public URL, which is also Authentik's default for it."""
return {**(config or {}), "authentik_host": AK_BROWSER_URL, "authentik_host_insecure": False,
"authentik_host_browser": AK_BROWSER_URL}
def provider_body(gate, flows):
return {
"name": gate["name"],
@@ -349,14 +340,6 @@ def embedded_outpost(ak):
return found[0]
def ensure_embedded_config(ak):
outpost = embedded_outpost(ak)
want = embedded_config(outpost.get("config"))
if outpost.get("config") != want:
ak.req("PATCH", f"outposts/instances/{outpost['pk']}/", {"config": want})
LOG.info("embedded outpost: updated config")
def ensure_gate(ak, gate, flows):
name = gate["name"]
if gate["group"] and ak.one("core/groups/", name=gate["group"]) is None:
@@ -485,7 +468,6 @@ def reconcile():
sources = [i for i in k8s.get("/apis/networking.k8s.io/v1/ingresses").get("items", []) if is_gated(i)]
shared = shared_hosts(sources)
flows = {f["slug"]: f["pk"] for f in ak.list("flows/instances/")}
ensure_embedded_config(ak)
desired = set() # (kind, ns, name) of every Kubernetes object some gate wants
gates = set() # gate names whose Authentik side should exist
@@ -166,11 +166,6 @@ class Objects(unittest.TestCase):
def test_drift(self):
self.assertEqual(c.drift({"a": 1, "b": 2}, {"a": 1, "b": 3}), {"b": 3})
def test_embedded_config_keeps_other_keys(self):
cfg = c.embedded_config({"log_level": "debug", "authentik_host": c.AK_URL})
self.assertEqual(cfg, {"log_level": "debug", "authentik_host": c.AK_BROWSER_URL,
"authentik_host_insecure": False, "authentik_host_browser": c.AK_BROWSER_URL})
class FakeAuthentik:
"""In-memory stand-in for the endpoints the controller uses; records calls."""
@@ -299,15 +294,6 @@ class Reconcile(unittest.TestCase):
c.embedded_outpost(self.ak)
self.assertEqual(self.ak.get(f"outposts/instances/{ldap['pk']}/")["providers"], [7])
def test_embedded_config(self):
c.ensure_embedded_config(self.ak)
cfg = self.ak.get(f"outposts/instances/{self.embedded['pk']}/")["config"]
self.assertEqual((cfg["log_level"], cfg["authentik_host"], cfg["authentik_host_browser"]),
("info", c.AK_BROWSER_URL, c.AK_BROWSER_URL))
before = len(self.ak.calls)
c.ensure_embedded_config(self.ak)
self.assertEqual(self.ak.calls[before:], [])
def test_provider_moves_from_proxy_mode(self):
self.ak.add("providers/proxy/", {"name": self.g["name"], "mode": "proxy",
"internal_host": "http://llama-server.ai.svc.cluster.local:8080",
@@ -70,7 +70,7 @@ spec:
mountPath: /ca
containers:
- name: audiobookshelf
image: ghcr.io/advplyr/audiobookshelf:2.37.2 # {"$imagepolicy": "flux-system:audiobookshelf"}
image: ghcr.io/advplyr/audiobookshelf:2.37.1 # {"$imagepolicy": "flux-system:audiobookshelf"}
ports:
- name: http
containerPort: 80
@@ -374,10 +374,39 @@ data:
name: lidarr-gate
state: absent
# The embedded outpost is not declared here: authentik-gate-sync owns its
# providers list (every gate) and the hosts in its config, and a
# blueprint entry must carry `providers`, which would detach the gates
# on every apply.
# No `providers`: authentik-gate-sync attaches the gates, and an update
# of an existing outpost is partial. `config` is replaced whole, so it
# carries every key, kubernetes_namespace included.
- model: authentik_outposts.outpost
identifiers:
name: authentik Embedded Outpost
state: present
attrs:
config:
log_level: info
# The embedded outpost builds the sign-in redirect from
# authentik_host, so it is the public URL.
authentik_host: "https://auth.renner.uno/"
authentik_host_insecure: false
authentik_host_browser: "https://auth.renner.uno/"
# A replica whose outpost websocket is down picks up gate changes
# only on this interval; the reconnect backoff never resets
# (goauthentik/authentik#26463).
refresh_interval: "minutes=1"
object_naming_template: "ak-outpost-%(name)s"
container_image: null
docker_labels: null
docker_network: ladmin_default
docker_map_ports: true
kubernetes_namespace: default
kubernetes_replicas: 1
kubernetes_service_type: ClusterIP
kubernetes_disabled_components: []
kubernetes_image_pull_secrets: []
kubernetes_ingress_annotations: {}
kubernetes_ingress_class_name: null
kubernetes_ingress_secret_name: "authentik-outpost-tls"
kubernetes_json_patches: null
# Shared LDAPS cert for the LDAP Outpost. Authentik's data model
# owns the cert per-LDAPProvider (no outpost-level cert field), so
-2
View File
@@ -114,8 +114,6 @@ spec:
env:
- name: AK_URL
value: http://authentik-server.authentik.svc.cluster.local
- name: AUTHENTIK_HOST_BROWSER
value: https://auth.renner.uno/
- name: DEFAULT_AUTHORIZATION_FLOW
value: default-provider-authorization-implicit-consent
- name: INTERVAL_SECONDS
@@ -128,7 +128,7 @@ spec:
memory: 32Mi
containers:
- name: home-assistant
image: ghcr.io/home-assistant/home-assistant:2026.10.3 # {"$imagepolicy": "flux-system:home-assistant"}
image: ghcr.io/home-assistant/home-assistant:2026.10.2 # {"$imagepolicy": "flux-system:home-assistant"}
env:
- name: TZ
value: Europe/Berlin
@@ -52,7 +52,7 @@ spec:
# stdlib only; Flux keeps the tag in lockstep via the shared image
# policy) — no extra dependency.
- name: reconcile-publish-ip
image: ghcr.io/music-assistant/server:2.10.8 # {"$imagepolicy": "flux-system:music-assistant"}
image: ghcr.io/music-assistant/server:2.10.7 # {"$imagepolicy": "flux-system:music-assistant"}
command: ["/app/venv/bin/python", "/scripts/reconcile.py"]
volumeMounts:
- name: data
@@ -62,7 +62,7 @@ spec:
readOnly: true
containers:
- name: music-assistant
image: ghcr.io/music-assistant/server:2.10.8 # {"$imagepolicy": "flux-system:music-assistant"}
image: ghcr.io/music-assistant/server:2.10.7 # {"$imagepolicy": "flux-system:music-assistant"}
env:
- name: LOG_LEVEL
value: info
@@ -87,9 +87,11 @@ spec:
- port: "53"
protocol: UDP
# host/remote-node: kubelet probes + node-local reach (Ceph mons on host net)
# world: image pulls and llama-server's Hugging Face model download
- toEntities:
- host
- remote-node
- world
# ICMP — egress mirror; see ingress block above.
- toEndpoints:
- matchExpressions:
@@ -164,76 +166,3 @@ spec:
- ports:
- port: "8080"
protocol: TCP
---
# Hugging Face model download on cold cache (llama-server, llama-embed, wiki-embed).
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: models-egress-huggingface
namespace: ai
spec:
endpointSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- llama-server
- llama-embed
- wiki-embed
egress:
# rules.dns lets Cilium learn the toFQDNs mapping
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
- toFQDNs:
- matchName: "huggingface.co"
- matchName: "hf.co"
- matchPattern: "*.huggingface.co"
- matchPattern: "*.hf.co"
toPorts:
- ports:
- port: "443"
protocol: TCP
---
# wiki: Anthropic API (distillation) and TypeSafe API (typed judgements).
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: wiki-egress-apis
namespace: ai
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: wiki
egress:
# rules.dns lets Cilium learn the toFQDNs mapping
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
- toFQDNs:
- matchName: "api.anthropic.com"
- matchName: "api.typesafe.ai"
toPorts:
- ports:
- port: "443"
protocol: TCP
@@ -66,9 +66,11 @@ spec:
- port: "53"
protocol: UDP
# host/remote-node: kubelet probes + node-local reach (Ceph mons on host net)
# world: image pulls, OBC S3 endpoint reachable via the cluster's LB IP, migration pipeline API
- toEntities:
- host
- remote-node
- world
# ICMP — egress mirror; see ingress block above.
- toEndpoints:
- matchExpressions:
@@ -92,19 +94,3 @@ spec:
- { type: 135, family: IPv6 }
- { type: 136, family: IPv6 }
- { type: 143, family: IPv6 }
---
# audiobookshelf (metadata providers) and libation (Audible): HTTPS to the internet.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: audiobookshelf-egress-https
namespace: audiobookshelf
spec:
endpointSelector: {}
egress:
- toEntities:
- world
toPorts:
- ports:
- port: "443"
protocol: TCP
@@ -70,6 +70,7 @@ spec:
- toEntities:
- host
- remote-node
- world
# ICMP — egress mirror.
- toEndpoints:
- matchExpressions:
@@ -93,43 +94,3 @@ spec:
- { type: 135, family: IPv6 }
- { type: 136, family: IPv6 }
- { type: 143, family: IPv6 }
---
# authentik server and worker: SMTP relay (Proton) for mail, HTTPS for
# outbound webhooks, OIDC/social sources and update checks.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: authentik-egress
namespace: authentik
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: authentik
app.kubernetes.io/part-of: authentik
egress:
# rules.dns lets Cilium learn the toFQDNs mapping
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
- toFQDNs:
- matchName: "smtp.protonmail.ch"
toPorts:
- ports:
- port: "587"
protocol: TCP
- toEntities:
- world
toPorts:
- ports:
- port: "443"
protocol: TCP
@@ -1,74 +0,0 @@
# EGRESS ONLY: an ingress section would flip ingress to default-deny and
# break the apiserver -> webhook admission calls.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: default-egress
namespace: cert-manager
spec:
endpointSelector: {}
egress:
# All in-cluster destinations (pods, nodes, kube-apiserver).
- toEntities:
- cluster
# rules.dns lets Cilium learn FQDN mappings.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
---
# Controller: ACME (Let's Encrypt prod + staging), Cloudflare DNS-01 API, and
# DNS-01 self-check against the recursive nameservers.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: cert-manager-controller-egress-world
namespace: cert-manager
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/component: controller
app.kubernetes.io/name: cert-manager
egress:
# rules.dns lets Cilium learn the toFQDNs mapping.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
- toFQDNs:
- matchName: "acme-v02.api.letsencrypt.org"
- matchName: "acme-staging-v02.api.letsencrypt.org"
- matchName: "api.cloudflare.com"
toPorts:
- ports:
- port: "443"
protocol: TCP
# Mirrors dns01RecursiveNameservers in
# kubernetes/infrastructure/cert-manager/helmrelease.yaml.
- toCIDR:
- 1.1.1.1/32
- 8.8.8.8/32
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
@@ -1,34 +0,0 @@
# CNPG instance pods: the barman-cloud plugin sidecar archives WAL and base
# backups to Hetzner S3. Additive only: every namespace holding a CNPG cluster
# also carries a namespace egress policy covering in-cluster traffic, so
# selecting these pods here does not cut anything else.
apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
name: cnpg-barman-s3-egress
spec:
endpointSelector:
matchLabels:
cnpg.io/podRole: instance
egress:
# rules.dns lets Cilium learn the toFQDNs mapping
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
- toFQDNs:
- matchName: "fsn1.your-objectstorage.com"
- matchPattern: "*.fsn1.your-objectstorage.com"
toPorts:
- ports:
- port: "443"
protocol: TCP
@@ -24,7 +24,10 @@ spec:
- host
- remote-node
# ICMP — all standard ICMPv4 + ICMPv6 from any cluster pod. See
# home-assistant-ns.yaml for the rationale.
# home-assistant-ns.yaml for the rationale. INGRESS ONLY: do NOT
# add an egress section to this CNP — that's what broke attempt
# 66dbe9f (it transitioned this ns from default-allow egress to
# egress-enforced-with-only-ICMP, killing DNS / apiserver / world).
- fromEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
@@ -47,39 +50,3 @@ spec:
- { type: 135, family: IPv6 }
- { type: 136, family: IPv6 }
- { type: 143, family: IPv6 }
egress:
# All in-cluster destinations (pods, nodes, kube-apiserver).
- toEntities:
- cluster
# rules.dns lets Cilium learn FQDN mappings.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
---
# lapi: CAPI + hub index. agent: hub/collection fetch.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: crowdsec-egress-world
namespace: crowdsec
spec:
endpointSelector:
matchLabels:
k8s-app: crowdsec
egress:
- toEntities:
- world
toPorts:
- ports:
- port: "443"
protocol: TCP
@@ -22,7 +22,8 @@ spec:
- host
- remote-node
# ICMP — all standard ICMPv4 + ICMPv6 from any cluster pod. See
# home-assistant-ns.yaml for the rationale.
# home-assistant-ns.yaml for the rationale. INGRESS ONLY: do NOT
# add an egress section to this CNP.
- fromEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
@@ -45,21 +46,3 @@ spec:
- { type: 135, family: IPv6 }
- { type: 136, family: IPv6 }
- { type: 143, family: IPv6 }
egress:
# All in-cluster destinations (pods, nodes, kube-apiserver).
- toEntities:
- cluster
# rules.dns lets Cilium learn FQDN mappings.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
@@ -48,109 +48,4 @@ spec:
- toEntities:
- host
- remote-node
---
# external-dns (Cloudflare provider): Cloudflare API.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: external-dns-egress
namespace: apps
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: external-dns
app.kubernetes.io/instance: external-dns
egress:
# rules.dns lets Cilium learn the toFQDNs mapping
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
- toFQDNs:
- matchName: "api.cloudflare.com"
toPorts:
- ports:
- port: "443"
protocol: TCP
---
# external-dns-unifi: UniFi controller API on the UDM (UNIFI_HOST).
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: external-dns-unifi-egress
namespace: apps
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: external-dns
app.kubernetes.io/instance: external-dns-unifi
egress:
# rules.dns lets Cilium learn the toFQDNs mapping
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
- toFQDNs:
- matchName: "udm.renner.uno"
toPorts:
- ports:
- port: "443"
protocol: TCP
---
# gatus: DNS probes against the UDM resolver by IP; HTTP checks target in-cluster names.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: gatus-egress
namespace: apps
spec:
endpointSelector:
matchLabels:
app: gatus
egress:
- toEntities:
- world
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
---
# ical-proxy: fetches caller-supplied calendar feed URLs.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: ical-proxy-egress
namespace: apps
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: ical-proxy
egress:
- toEntities:
- world
toPorts:
- ports:
- port: "443"
protocol: TCP
- port: "80"
protocol: TCP
@@ -92,27 +92,11 @@ spec:
- ports:
- port: "8644"
protocol: TCP
# Push mirror to GitHub and other external targets.
- toEntities:
- host
- remote-node
---
# gitea: push mirrors, external webhooks and OIDC/OAuth sources over HTTPS.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: gitea-egress-https
namespace: gitea
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: gitea
egress:
- toEntities:
- world
toPorts:
- ports:
- port: "443"
protocol: TCP
---
# Traefik serves git.renner.uno / gitea.renner.uno (:3000) and the gitea-ssh
# entrypoint (:2222). Lives beside the baseline so both land in one reconcile.
@@ -143,6 +143,10 @@ spec:
- toEntities:
- host
- remote-node
# World: IoT-VLAN devices, HA Cloud, object storage, and third-party
# integration endpoints on the LAN and internet.
- toEntities:
- world
# ICMP — egress mirror of the ingress ICMP allow above. Lets HA
# send ICMP to any cluster pod (covers the kernel's ICMPv6
# Destination Unreachable when CoreDNS responses land on a
@@ -257,7 +261,10 @@ spec:
# hairpin off the home-assistant-bgp LoadBalancer and are unusable from a
# pod. It reaches HA at home-assistant-bgp.home-assistant.svc.cluster.local.
#
# The egress half is trmnl-baseline-egress (toEntities: cluster).
# Only the ingress half is needed here. Unlike the openbao rotator, the trmnl
# namespace has no default-deny EGRESS covering this pod — terminus-egress is
# scoped to the Terminus pods by label and does not select the renderer — so
# there is no source-side rule to add.
#
# Source scoped to the renderer pod specifically, not the whole trmnl
# namespace: Terminus itself sits in that namespace and has no business
@@ -344,142 +351,3 @@ spec:
- ports:
- port: "5432"
protocol: TCP
---
# home-assistant: IoT-VLAN devices, HA Cloud, and third-party integration
# endpoints on the LAN and internet.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: home-assistant-egress-world
namespace: home-assistant
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: home-assistant
egress:
- toEntities:
- world
---
# shelly-manager: HTTP RPC to Shelly devices on the IoT VLAN.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: shelly-manager-egress
namespace: home-assistant
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: shelly-manager
egress:
- toCIDR:
- 10.2.30.0/24
toPorts:
- ports:
- port: "80"
protocol: TCP
---
# esphome: OTA/API to devices on the IoT VLAN; HTTPS for GitHub, PlatformIO and PyPI during builds.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: esphome-egress
namespace: home-assistant
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: esphome
egress:
- toCIDR:
- 10.2.30.0/24
- toEntities:
- world
toPorts:
- ports:
- port: "443"
protocol: TCP
---
# zigbee2mqtt: Zigbee coordinator (SLZB) TCP socket.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: zigbee2mqtt-egress
namespace: home-assistant
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: zigbee2mqtt
egress:
# rules.dns lets Cilium learn the toFQDNs mapping
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
- toFQDNs:
- matchName: "slzb-zigbee.renner.uno"
toPorts:
- ports:
- port: "6638"
protocol: TCP
---
# mqtt-account-controller: credential delivery to edge-1 (deliver.esp32 in mqtt-accounts.yaml).
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: mqtt-account-controller-egress
namespace: home-assistant
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: mqtt-account-controller
egress:
# rules.dns lets Cilium learn the toFQDNs mapping
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
- toFQDNs:
- matchName: "edge-1.iot.renner.uno"
toPorts:
- ports:
- port: "8443"
protocol: TCP
---
# whisper and speech-to-phrase: model downloads over HTTPS.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: speech-models-egress
namespace: home-assistant
spec:
endpointSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- whisper
- speech-to-phrase
egress:
- toEntities:
- world
toPorts:
- ports:
- port: "443"
protocol: TCP
@@ -33,7 +33,7 @@ spec:
- host
- remote-node
# ICMP — all standard ICMPv4 + ICMPv6 from any cluster pod. See
# home-assistant-ns.yaml for the rationale.
# home-assistant-ns.yaml for the rationale. INGRESS ONLY.
- fromEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
@@ -56,41 +56,3 @@ spec:
- { type: 135, family: IPv6 }
- { type: 136, family: IPv6 }
- { type: 143, family: IPv6 }
egress:
# All in-cluster destinations (pods, nodes, kube-apiserver).
- toEntities:
- cluster
# rules.dns lets Cilium learn FQDN mappings.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
---
# immich-server and machine-learning: HTTPS to the internet (model downloads, metadata/reverse geocoding, version checks).
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: immich-egress-world
namespace: immich
spec:
endpointSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values: [immich-server, immich-machine-learning]
egress:
- toEntities:
- world
toPorts:
- ports:
- port: "443"
protocol: TCP
@@ -33,7 +33,7 @@ spec:
- host
- remote-node
# ICMP — all standard ICMPv4 + ICMPv6 from any cluster pod. See
# home-assistant-ns.yaml for the rationale.
# home-assistant-ns.yaml for the rationale. INGRESS ONLY.
- fromEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
@@ -56,43 +56,3 @@ spec:
- { type: 135, family: IPv6 }
- { type: 136, family: IPv6 }
- { type: 143, family: IPv6 }
egress:
# All in-cluster destinations (pods, nodes, kube-apiserver).
- toEntities:
- cluster
# rules.dns lets Cilium learn FQDN mappings.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
---
# Token job pods install tooling at runtime (apt, dl.k8s.io).
# Selected by the job-controller `job-name` label, so the influxdb StatefulSet pod gets no world egress.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: influxdb-token-jobs-egress-world
namespace: influxdb
spec:
endpointSelector:
matchExpressions:
- key: job-name
operator: Exists
egress:
- toEntities:
- world
toPorts:
- ports:
- port: "443"
protocol: TCP
- port: "80"
protocol: TCP
@@ -9,7 +9,6 @@ resources:
- audiobookshelf-ns.yaml
- home-assistant-ns.yaml
- spegel-ns.yaml
- cert-manager-ns.yaml
- crowdsec-ns.yaml
- database-ns.yaml
- immich-ns.yaml
@@ -38,5 +37,4 @@ resources:
- renovate-ns.yaml
- hermes-ns.yaml
- allow-cilium-health.yaml
- cnpg-barman-s3.yaml
- kube-system-hubble-relay.yaml
@@ -33,7 +33,7 @@ spec:
- host
- remote-node
# ICMP — all standard ICMPv4 + ICMPv6 from any cluster pod. See
# home-assistant-ns.yaml for the rationale.
# home-assistant-ns.yaml for the rationale. INGRESS ONLY.
- fromEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
@@ -58,7 +58,8 @@ spec:
- { type: 143, family: IPv6 }
---
# Egress for the app pods (synapse, mas, element-web),
# selected by part-of; the mx-pg CNPG pods are covered by mx-pg-egress.
# selected by part-of so the mx-pg CNPG pods keep unrestricted egress for
# the barman-cloud WAL archive to Hetzner (same trade as trmnl-ns.yaml).
#
# Nothing here reaches the internet: that is the network half of
# "federation off". The one exception is synapse-push-egress below.
@@ -186,32 +187,3 @@ spec:
- ports:
- port: "8443"
protocol: TCP
---
# mx-pg CNPG pods: in-cluster + DNS. Unselected by the app egress policies above;
# S3 backup egress comes from the clusterwide CNPG policy.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: mx-pg-egress
namespace: matrix
spec:
endpointSelector:
matchLabels:
cnpg.io/cluster: mx-pg
egress:
- toEntities:
- cluster
# rules.dns lets Cilium learn FQDN mappings.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
@@ -34,7 +34,7 @@ spec:
- host
- remote-node
# ICMP — all standard ICMPv4 + ICMPv6 from any cluster pod. See
# home-assistant-ns.yaml for the rationale.
# home-assistant-ns.yaml for the rationale. INGRESS ONLY.
- fromEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
@@ -57,24 +57,6 @@ spec:
- { type: 135, family: IPv6 }
- { type: 136, family: IPv6 }
- { type: 143, family: IPv6 }
egress:
# All in-cluster destinations (pods, nodes, kube-apiserver).
- toEntities:
- cluster
# rules.dns lets Cilium learn FQDN mappings.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
---
# Hermes Agent (hermes ns) reads metrics, alerts and logs: the Prometheus,
# Alertmanager and Loki HTTP APIs, scoped to those pods and ports.
@@ -102,120 +84,3 @@ spec:
protocol: TCP
- port: "3100"
protocol: TCP
---
# Alertmanager: SMTP submission (smtp_smarthost in helmrelease-prometheus.yaml). Only receiver is email.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: alertmanager-egress-world
namespace: monitoring
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: alertmanager
egress:
# rules.dns lets Cilium learn the toFQDNs mapping.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
- toFQDNs:
- matchName: "smtp.protonmail.ch"
toPorts:
- ports:
- port: "587"
protocol: TCP
---
# Prober: targets are defined by Probe objects (monitoring-rules), any port.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: blackbox-exporter-egress-world
namespace: monitoring
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: prometheus-blackbox-exporter
egress:
- toEntities:
- world
---
# Grafana: plugin download (grafana.com) + OAuth, and the legacy Prometheus datasource.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: grafana-egress-world
namespace: monitoring
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: grafana
egress:
# rules.dns lets Cilium learn the toFQDNs mapping.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
- toEntities:
- world
toPorts:
- ports:
- port: "443"
protocol: TCP
# Datasource "Prometheus (legacy)".
- toFQDNs:
- matchName: "prometheus.srv.renner.uno"
toPorts:
- ports:
- port: "9090"
protocol: TCP
---
# Prometheus: off-site Uptime Kuma scrape (additionalScrapeConfigs job uptime-kuma). No remoteWrite configured.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: prometheus-egress-world
namespace: monitoring
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: prometheus
egress:
# rules.dns lets Cilium learn the toFQDNs mapping.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
- toFQDNs:
- matchName: "kuma.renner.uno"
toPorts:
- ports:
- port: "443"
protocol: TCP
@@ -24,10 +24,6 @@ spec:
protocol: UDP
- port: "53"
protocol: TCP
# rules.dns lets Cilium learn the toFQDNs mapping
rules:
dns:
- matchPattern: "*"
- toEntities:
- kube-apiserver
- toEndpoints:
@@ -38,9 +34,9 @@ spec:
- ports:
- port: "8200"
protocol: TCP
# NetBird management API.
- toFQDNs:
- matchName: "netbird.renner.uno"
# NetBird management API (netbird.renner.uno).
- toEntities:
- world
toPorts:
- ports:
- port: "443"
@@ -88,6 +88,7 @@ spec:
- toEntities:
- host
- remote-node
- world
# ICMP — egress mirror.
- toEndpoints:
- matchExpressions:
@@ -267,49 +268,3 @@ spec:
- ports:
- port: "8000"
protocol: TCP
---
# protonmail-bridge: Proton API (HTTPS) and update/status endpoints (HTTP).
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: protonmail-bridge-egress
namespace: paperless
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: protonmail-bridge
egress:
- toEntities:
- world
toPorts:
- ports:
- port: "443"
protocol: TCP
- port: "80"
protocol: TCP
---
# syncthing: relays/discovery (443), STUN (3478), peer sync (22000), local discovery (21027).
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: syncthing-egress
namespace: paperless
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: syncthing
egress:
- toEntities:
- world
toPorts:
- ports:
- port: "443"
protocol: TCP
- port: "3478"
protocol: UDP
- port: "22000"
protocol: TCP
- port: "22000"
protocol: UDP
- port: "21027"
protocol: UDP
@@ -53,20 +53,14 @@ spec:
protocol: UDP
- port: "53"
protocol: TCP
# rules.dns lets Cilium learn the toFQDNs mapping
rules:
dns:
- matchPattern: "*"
# gitea.renner.uno via Traefik.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": ingress
app.kubernetes.io/name: traefik
# Model APIs.
- toFQDNs:
- matchName: "api.mistral.ai"
- matchName: "api.anthropic.com"
- matchName: "api.typesafe.ai"
# api.mistral.ai, api.anthropic.com, api.typesafe.ai.
- toEntities:
- world
toPorts:
- ports:
- port: "443"
@@ -54,10 +54,12 @@ spec:
- ports:
- port: "53"
protocol: UDP
# host/remote-node: kube-apiserver (leader election Lease) + cross-node peers
# host/remote-node: kube-apiserver (leader election Lease) + cross-node peers;
# world: fallback pulls to docker.io/quay.io/ghcr.io/registry.k8s.io.
- toEntities:
- host
- remote-node
- world
# ICMP — egress mirror.
- toEndpoints:
- matchExpressions:
@@ -33,7 +33,7 @@ spec:
- host
- remote-node
# ICMP — all standard ICMPv4 + ICMPv6 from any cluster pod. See
# home-assistant-ns.yaml for the rationale.
# home-assistant-ns.yaml for the rationale. INGRESS ONLY.
- fromEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
@@ -86,9 +86,10 @@ spec:
---
# Egress allowlist for the Terminus pod only.
#
# Deliberately NOT applied to the other pods: they are covered by
# trmnl-baseline-egress (in-cluster + DNS), and the tr-pg barman-cloud WAL
# archive egress comes from the clusterwide CNPG policy.
# Deliberately NOT applied to the tr-pg CNPG pods: selecting them here
# would make their egress restricted too, and the barman-cloud WAL archive
# path to Hetzner object storage is not worth putting behind an FQDN rule
# that fails closed. They keep unrestricted egress, matching immich/authentik.
#
# Terminus needs exactly three outbound destinations, all HTTPS:
# - trmnl.com → GET /api/models, /api/palettes (MODEL_SYNCHRONIZER,
@@ -151,34 +152,3 @@ spec:
- toEntities:
- host
- remote-node
---
# In-cluster + DNS for every pod except terminus (terminus-egress is its
# allowlist; selecting it here would widen it to all of `cluster`).
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: trmnl-baseline-egress
namespace: trmnl
spec:
endpointSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: NotIn
values: [terminus]
egress:
- toEntities:
- cluster
# rules.dns lets Cilium learn FQDN mappings.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
@@ -40,18 +40,14 @@ spec:
protocol: UDP
- port: "53"
protocol: TCP
# rules.dns lets Cilium learn the toFQDNs mapping
rules:
dns:
- matchPattern: "*"
# EndpointMonitor watch, Ingress reads, and the startup GET of the config
# Secret.
- toEntities:
- kube-apiserver
# api.uptimerobot.com. Not reached at all until an EndpointMonitor exists:
# the controller makes no API call at startup.
- toFQDNs:
- matchName: "api.uptimerobot.com"
- toEntities:
- world
toPorts:
- ports:
- port: "443"
@@ -19,7 +19,7 @@ spec:
- host
- remote-node
# ICMP — all standard ICMPv4 + ICMPv6 from any cluster pod. See
# home-assistant-ns.yaml for the rationale.
# home-assistant-ns.yaml for the rationale. INGRESS ONLY.
- fromEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
@@ -42,32 +42,6 @@ spec:
- { type: 135, family: IPv6 }
- { type: 136, family: IPv6 }
- { type: 143, family: IPv6 }
egress:
# All in-cluster destinations (pods, nodes, kube-apiserver).
- toEntities:
- cluster
# rules.dns lets Cilium learn the toFQDNs mapping.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
# Hetzner S3: velero, node-agent and kopia maintenance jobs.
- toFQDNs:
- matchName: "fsn1.your-objectstorage.com"
- matchPattern: "*.fsn1.your-objectstorage.com"
toPorts:
- ports:
- port: "443"
protocol: TCP
---
# backup-verify CronJob: kube-apiserver, DNS, Hetzner Cloud API, SSH to its VM.
apiVersion: cilium.io/v2
@@ -8,8 +8,7 @@
# belongs in its own policy with its own selector, as
# `wp-pg-allow-cnpg-operator` below does.
#
# Egress: in-cluster + DNS only (server, agent, wp-pg). The CNPG S3 backup
# egress comes from the clusterwide CNPG policy.
# There is no egress section, so egress is unrestricted. Still open (#433).
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
@@ -37,6 +36,7 @@ spec:
- host
- remote-node
# ICMP — from any cluster pod. See home-assistant-ns.yaml for the rationale.
# INGRESS ONLY: do NOT add an egress section to this CNP.
#
# Needed here specifically because wp-pg-allow-cnpg-operator below attaches
# an L4 rule (toPorts :8000) to the wp-pg pods, and Cilium blocks ICMP to an
@@ -79,24 +79,6 @@ spec:
# instance manager. It lands in stage 2 as its own policy selecting the
# server pod, the way woodpecker-allow-rotators already scopes its :8000
# rule in apps/woodpecker/networkpolicy.yaml.
egress:
# All in-cluster destinations (pods, nodes, kube-apiserver).
- toEntities:
- cluster
# rules.dns lets Cilium learn FQDN mappings.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
---
# The CNPG operator polls each instance's status endpoint over HTTPS
# (https://<pod-ip>:8000/pg/status) to populate Cluster status. Without it the
@@ -85,31 +85,13 @@ spec:
- ports:
- port: "53"
protocol: UDP
# world covers two things WordPress genuinely needs outbound: the
# wordpress.org update/plugin API, and the Hetzner Object Storage
# endpoint the MariaDB Backup writes to.
- toEntities:
- host
- remote-node
# Hetzner Object Storage for the MariaDB backup CronJob pods (no stable
# label, hence namespace-wide). rules.dns lets Cilium learn the mapping.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
- toFQDNs:
- matchName: "fsn1.your-objectstorage.com"
- matchPattern: "*.fsn1.your-objectstorage.com"
toPorts:
- ports:
- port: "443"
protocol: TCP
- world
# ICMP — egress mirror.
- toEndpoints:
- matchExpressions:
@@ -133,21 +115,3 @@ spec:
- { type: 135, family: IPv6 }
- { type: 136, family: IPv6 }
- { type: 143, family: IPv6 }
---
# wordpress: wordpress.org update/plugin API and other HTTPS fetches.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: wordpress-egress-https
namespace: wordpress
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: wordpress
egress:
- toEntities:
- world
toPorts:
- ports:
- port: "443"
protocol: TCP