Compare commits

..
Author SHA1 Message Date
claude-botandClaude Opus 5.5 5aee6a54ad revert(netpol): no world egress for mosquitto-bootstrap
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Runtime package installs are not to be enabled by policy. The Jobs feed only
the unused legacy password_file and are removed in a separate PR, which must
merge before this one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 17:33:25 +02:00
claude-botandClaude Opus 5.5 8a53171ad0 fix(netpol): let mosquitto-bootstrap Jobs reach Alpine mirrors
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
The Jobs run apk add curl jq on every execution; without world 443/80 they
fail and block the home-assistant Kustomization. Also correct the stale
renderer egress comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 17:26:49 +02:00
claude-botandClaude Opus 5.5 299194247e feat(netpol): allow mqtt-account-controller to deliver to edge-1
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was canceled
edge-1.iot.renner.uno:8443 is the deliver.esp32 target in mqtt-accounts.yaml
(https+mTLS credential push). Matched by FQDN, not the current 10.2.30.108.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 17:26:00 +02:00
claude-bot 6b6e1e7e04 Merge remote-tracking branch 'origin/main' into worktree-egress-lockdown
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline failed
# Conflicts:
#	kubernetes/infrastructure/network-policies/gitea-ns.yaml
#	kubernetes/infrastructure/network-policies/matrix-ns.yaml
2026-10-24 12:45:03 +02:00
claude-botandClaude Opus 5.5 752fcd5d03 feat(netpol): restrict pod egress to the outside world per workload
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was canceled
Drop namespace-wide world egress and give each workload only the external
destinations it needs (FQDN where the host is in our config, port-scoped
world otherwise). Namespaces that had ingress-only policies get an
in-cluster + DNS egress baseline. CNPG instances get Hetzner S3 via one
clusterwide policy.

Out of scope: kube-system, flux-system, rook-ceph, woodpecker-ci,
music-assistant, evcc, openbao.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 12:43:38 +02:00
security-bot 55cd083484 chore(image-automation): bump ghcr.io/advplyr/audiobookshelf:2.37.2
ci/woodpecker/cron/e2e Pipeline failed
ci/woodpecker/cron/security-scan Pipeline was successful
Updates managed by image-automation-controller.
2026-10-23 20:49:32 +00:00
security-bot ada3d3d0b4 chore(image-automation): bump ghcr.io/home-assistant/home-assistant:2026.10.3
Updates managed by image-automation-controller.
2026-10-23 19:50:03 +00:00
security-bot 1bd3111912 chore(image-automation): bump ghcr.io/music-assistant/server:2.10.8
Updates managed by image-automation-controller.
2026-10-23 14:49:29 +00:00
31 changed files with 1037 additions and 87 deletions
@@ -45,6 +45,7 @@ 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
@@ -208,6 +209,14 @@ 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"],
@@ -340,6 +349,14 @@ 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:
@@ -468,6 +485,7 @@ 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,6 +166,11 @@ 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."""
@@ -294,6 +299,15 @@ 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.1 # {"$imagepolicy": "flux-system:audiobookshelf"}
image: ghcr.io/advplyr/audiobookshelf:2.37.2 # {"$imagepolicy": "flux-system:audiobookshelf"}
ports:
- name: http
containerPort: 80
@@ -374,39 +374,10 @@ data:
name: lidarr-gate
state: absent
# 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
# 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.
# 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,6 +114,8 @@ 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.2 # {"$imagepolicy": "flux-system:home-assistant"}
image: ghcr.io/home-assistant/home-assistant:2026.10.3 # {"$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.7 # {"$imagepolicy": "flux-system:music-assistant"}
image: ghcr.io/music-assistant/server:2.10.8 # {"$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.7 # {"$imagepolicy": "flux-system:music-assistant"}
image: ghcr.io/music-assistant/server:2.10.8 # {"$imagepolicy": "flux-system:music-assistant"}
env:
- name: LOG_LEVEL
value: info
@@ -87,11 +87,9 @@ 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:
@@ -166,3 +164,76 @@ 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,11 +66,9 @@ 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:
@@ -94,3 +92,19 @@ 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,7 +70,6 @@ spec:
- toEntities:
- host
- remote-node
- world
# ICMP — egress mirror.
- toEndpoints:
- matchExpressions:
@@ -94,3 +93,43 @@ 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
@@ -0,0 +1,74 @@
# 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
@@ -0,0 +1,34 @@
# 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,10 +24,7 @@ spec:
- host
- remote-node
# ICMP — all standard ICMPv4 + ICMPv6 from any cluster pod. See
# 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).
# home-assistant-ns.yaml for the rationale.
- fromEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
@@ -50,3 +47,39 @@ 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,8 +22,7 @@ spec:
- host
- remote-node
# ICMP — all standard ICMPv4 + ICMPv6 from any cluster pod. See
# home-assistant-ns.yaml for the rationale. INGRESS ONLY: do NOT
# add an egress section to this CNP.
# home-assistant-ns.yaml for the rationale.
- fromEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
@@ -46,3 +45,21 @@ 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,4 +48,109 @@ 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,11 +92,27 @@ 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,10 +143,6 @@ 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
@@ -261,10 +257,7 @@ 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.
#
# 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.
# The egress half is trmnl-baseline-egress (toEntities: cluster).
#
# Source scoped to the renderer pod specifically, not the whole trmnl
# namespace: Terminus itself sits in that namespace and has no business
@@ -351,3 +344,142 @@ 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. INGRESS ONLY.
# home-assistant-ns.yaml for the rationale.
- fromEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
@@ -56,3 +56,41 @@ 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. INGRESS ONLY.
# home-assistant-ns.yaml for the rationale.
- fromEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
@@ -56,3 +56,43 @@ 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,6 +9,7 @@ resources:
- audiobookshelf-ns.yaml
- home-assistant-ns.yaml
- spegel-ns.yaml
- cert-manager-ns.yaml
- crowdsec-ns.yaml
- database-ns.yaml
- immich-ns.yaml
@@ -37,4 +38,5 @@ 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. INGRESS ONLY.
# home-assistant-ns.yaml for the rationale.
- fromEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
@@ -58,8 +58,7 @@ spec:
- { type: 143, family: IPv6 }
---
# Egress for the app pods (synapse, mas, element-web),
# 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).
# selected by part-of; the mx-pg CNPG pods are covered by mx-pg-egress.
#
# Nothing here reaches the internet: that is the network half of
# "federation off". The one exception is synapse-push-egress below.
@@ -187,3 +186,32 @@ 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. INGRESS ONLY.
# home-assistant-ns.yaml for the rationale.
- fromEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
@@ -57,6 +57,24 @@ 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.
@@ -84,3 +102,120 @@ 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,6 +24,10 @@ spec:
protocol: UDP
- port: "53"
protocol: TCP
# rules.dns lets Cilium learn the toFQDNs mapping
rules:
dns:
- matchPattern: "*"
- toEntities:
- kube-apiserver
- toEndpoints:
@@ -34,9 +38,9 @@ spec:
- ports:
- port: "8200"
protocol: TCP
# NetBird management API (netbird.renner.uno).
- toEntities:
- world
# NetBird management API.
- toFQDNs:
- matchName: "netbird.renner.uno"
toPorts:
- ports:
- port: "443"
@@ -88,7 +88,6 @@ spec:
- toEntities:
- host
- remote-node
- world
# ICMP — egress mirror.
- toEndpoints:
- matchExpressions:
@@ -268,3 +267,49 @@ 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,14 +53,20 @@ 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
# api.mistral.ai, api.anthropic.com, api.typesafe.ai.
- toEntities:
- world
# Model APIs.
- toFQDNs:
- matchName: "api.mistral.ai"
- matchName: "api.anthropic.com"
- matchName: "api.typesafe.ai"
toPorts:
- ports:
- port: "443"
@@ -54,12 +54,10 @@ spec:
- ports:
- port: "53"
protocol: UDP
# host/remote-node: kube-apiserver (leader election Lease) + cross-node peers;
# world: fallback pulls to docker.io/quay.io/ghcr.io/registry.k8s.io.
# host/remote-node: kube-apiserver (leader election Lease) + cross-node peers
- 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. INGRESS ONLY.
# home-assistant-ns.yaml for the rationale.
- fromEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
@@ -86,10 +86,9 @@ spec:
---
# Egress allowlist for the Terminus pod only.
#
# 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.
# 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.
#
# Terminus needs exactly three outbound destinations, all HTTPS:
# - trmnl.com → GET /api/models, /api/palettes (MODEL_SYNCHRONIZER,
@@ -152,3 +151,34 @@ 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,14 +40,18 @@ 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.
- toEntities:
- world
- toFQDNs:
- matchName: "api.uptimerobot.com"
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. INGRESS ONLY.
# home-assistant-ns.yaml for the rationale.
- fromEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
@@ -42,6 +42,32 @@ 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,7 +8,8 @@
# belongs in its own policy with its own selector, as
# `wp-pg-allow-cnpg-operator` below does.
#
# There is no egress section, so egress is unrestricted. Still open (#433).
# Egress: in-cluster + DNS only (server, agent, wp-pg). The CNPG S3 backup
# egress comes from the clusterwide CNPG policy.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
@@ -36,7 +37,6 @@ 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,6 +79,24 @@ 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,13 +85,31 @@ 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
- world
# 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
# ICMP — egress mirror.
- toEndpoints:
- matchExpressions:
@@ -115,3 +133,21 @@ 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