From e7469a36b848dc0e7fa309e344cfcb32d20b043d Mon Sep 17 00:00:00 2001 From: Thore Cimbal Date: Wed, 19 Aug 2026 12:00:00 +0000 Subject: [PATCH] feat(netpol): deny pods the metadata service, leave everything else open (#0088) First egress rule in matrix, authentik and monitoring. It allows DNS, the cluster ranges and the whole internet, and denies only 169.254.0.0/16 - link-local, where Hetzner serves instance metadata unauthenticated to any pod. Deliberately narrow. The textbook cut, 0.0.0.0/0 except RFC1918, would have severed two things here, both over 10.0.0.3 on the private Hetzner network: Alloy writes metrics and logs there, and the TURN rotation reaches Gitea through a hostAlias to that address. Private ranges therefore stay open. The payoff is modest and should be stated as such: measured from a pod, the service answers with instance-id, hostname, region, MAC and network config, while userdata and public-keys are empty. No credentials are exposed here, unlike the AWS case this hardening usually targets. It costs nothing though, and it closes the class. Two preconditions checked rather than assumed, because both are the usual way this breaks: kube-system carries kubernetes.io/metadata.name so the DNS rule actually matches, and the cluster is IPv4-only so 0.0.0.0/0 really does cover everything. Rollback is deleting the one policy per namespace. Co-Authored-By: Claude Opus 5 --- apps/authentik/networkpolicy.yaml | 53 ++++++++++++++++++++++++++++++ apps/monitoring/kustomization.yaml | 1 + apps/monitoring/networkpolicy.yaml | 53 ++++++++++++++++++++++++++++++ apps/production/networkpolicy.yaml | 53 ++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 apps/monitoring/networkpolicy.yaml diff --git a/apps/authentik/networkpolicy.yaml b/apps/authentik/networkpolicy.yaml index d8f3ed7..459b646 100644 --- a/apps/authentik/networkpolicy.yaml +++ b/apps/authentik/networkpolicy.yaml @@ -94,3 +94,56 @@ spec: ports: - protocol: TCP port: 8089 +--- +# Egress: alles bleibt erlaubt AUSSER dem Metadaten-Dienst (management #0088). +# +# Warum so schmal? Der Lehrbuch-Schnitt (0.0.0.0/0 except RFC1918) haette hier zwei +# Dinge zerrissen, beide ueber 10.0.0.3 (CFGMON im privaten Hetzner-Netz): Alloy +# schreibt Metriken und Logs dorthin, und der TURN-Rotations-CronJob erreicht Gitea +# ueber einen hostAlias auf dieselbe Adresse. Private Netze bleiben deshalb offen. +# +# 169.254.0.0/16 ist Link-Local; darin liegt bei Hetzner unter 169.254.169.254 der +# Metadaten-Dienst, aus jedem Pod unauthentifiziert abfragbar (nachgemessen 2026-08-19: +# HTTP 200 mit instance-id, hostname, region, MAC, Netzkonfiguration). userdata und +# public-keys waren LEER - es liegen hier also keine Zugangsdaten offen. Der Gewinn ist +# entsprechend bescheiden; der Schnitt kostet aber nichts und schliesst die Klasse. +# +# ACHTUNG beim Erweitern: Sobald eine Egress-Regel fuer einen Pod existiert, gilt fuer +# ihn Default-Deny fuer alles NICHT Aufgefuehrte. Die drei Bloecke unten sind daher +# Pflicht, nicht Bequemlichkeit - insbesondere DNS: fehlt es, steht alles, und der +# Fehler sieht wie ein Anwendungsproblem aus, nicht wie eine Firewall. +# +# Rollback: diese eine Policy im Namespace loeschen. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: egress-block-metadata + namespace: authentik +spec: + podSelector: {} + policyTypes: + - Egress + egress: + # 1. DNS - zuerst, weil ohne DNS nichts geht. + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + # 2. Cluster-intern: Pods und Services (inkl. API-Server 10.43.0.1). + - to: + - ipBlock: + cidr: 10.42.0.0/16 + - ipBlock: + cidr: 10.43.0.0/16 + # 3. Alles uebrige - Foederation, ACME, SMTP, Registries, privates Netz - + # ausser Link-Local. + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + - 169.254.0.0/16 diff --git a/apps/monitoring/kustomization.yaml b/apps/monitoring/kustomization.yaml index 784532a..c841e05 100644 --- a/apps/monitoring/kustomization.yaml +++ b/apps/monitoring/kustomization.yaml @@ -6,3 +6,4 @@ resources: - kube-state-metrics.yaml - alloy-config.yaml - alloy.yaml + - networkpolicy.yaml diff --git a/apps/monitoring/networkpolicy.yaml b/apps/monitoring/networkpolicy.yaml new file mode 100644 index 0000000..2d82cdf --- /dev/null +++ b/apps/monitoring/networkpolicy.yaml @@ -0,0 +1,53 @@ +--- +# Egress: alles bleibt erlaubt AUSSER dem Metadaten-Dienst (management #0088). +# +# Warum so schmal? Der Lehrbuch-Schnitt (0.0.0.0/0 except RFC1918) haette hier zwei +# Dinge zerrissen, beide ueber 10.0.0.3 (CFGMON im privaten Hetzner-Netz): Alloy +# schreibt Metriken und Logs dorthin, und der TURN-Rotations-CronJob erreicht Gitea +# ueber einen hostAlias auf dieselbe Adresse. Private Netze bleiben deshalb offen. +# +# 169.254.0.0/16 ist Link-Local; darin liegt bei Hetzner unter 169.254.169.254 der +# Metadaten-Dienst, aus jedem Pod unauthentifiziert abfragbar (nachgemessen 2026-08-19: +# HTTP 200 mit instance-id, hostname, region, MAC, Netzkonfiguration). userdata und +# public-keys waren LEER - es liegen hier also keine Zugangsdaten offen. Der Gewinn ist +# entsprechend bescheiden; der Schnitt kostet aber nichts und schliesst die Klasse. +# +# ACHTUNG beim Erweitern: Sobald eine Egress-Regel fuer einen Pod existiert, gilt fuer +# ihn Default-Deny fuer alles NICHT Aufgefuehrte. Die drei Bloecke unten sind daher +# Pflicht, nicht Bequemlichkeit - insbesondere DNS: fehlt es, steht alles, und der +# Fehler sieht wie ein Anwendungsproblem aus, nicht wie eine Firewall. +# +# Rollback: diese eine Policy im Namespace loeschen. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: egress-block-metadata + namespace: monitoring +spec: + podSelector: {} + policyTypes: + - Egress + egress: + # 1. DNS - zuerst, weil ohne DNS nichts geht. + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + # 2. Cluster-intern: Pods und Services (inkl. API-Server 10.43.0.1). + - to: + - ipBlock: + cidr: 10.42.0.0/16 + - ipBlock: + cidr: 10.43.0.0/16 + # 3. Alles uebrige - Foederation, ACME, SMTP, Registries, privates Netz - + # ausser Link-Local. + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + - 169.254.0.0/16 diff --git a/apps/production/networkpolicy.yaml b/apps/production/networkpolicy.yaml index 3a15d5d..487dd78 100644 --- a/apps/production/networkpolicy.yaml +++ b/apps/production/networkpolicy.yaml @@ -408,3 +408,56 @@ spec: ports: - protocol: TCP port: postgres +--- +# Egress: alles bleibt erlaubt AUSSER dem Metadaten-Dienst (management #0088). +# +# Warum so schmal? Der Lehrbuch-Schnitt (0.0.0.0/0 except RFC1918) haette hier zwei +# Dinge zerrissen, beide ueber 10.0.0.3 (CFGMON im privaten Hetzner-Netz): Alloy +# schreibt Metriken und Logs dorthin, und der TURN-Rotations-CronJob erreicht Gitea +# ueber einen hostAlias auf dieselbe Adresse. Private Netze bleiben deshalb offen. +# +# 169.254.0.0/16 ist Link-Local; darin liegt bei Hetzner unter 169.254.169.254 der +# Metadaten-Dienst, aus jedem Pod unauthentifiziert abfragbar (nachgemessen 2026-08-19: +# HTTP 200 mit instance-id, hostname, region, MAC, Netzkonfiguration). userdata und +# public-keys waren LEER - es liegen hier also keine Zugangsdaten offen. Der Gewinn ist +# entsprechend bescheiden; der Schnitt kostet aber nichts und schliesst die Klasse. +# +# ACHTUNG beim Erweitern: Sobald eine Egress-Regel fuer einen Pod existiert, gilt fuer +# ihn Default-Deny fuer alles NICHT Aufgefuehrte. Die drei Bloecke unten sind daher +# Pflicht, nicht Bequemlichkeit - insbesondere DNS: fehlt es, steht alles, und der +# Fehler sieht wie ein Anwendungsproblem aus, nicht wie eine Firewall. +# +# Rollback: diese eine Policy im Namespace loeschen. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: egress-block-metadata + namespace: matrix +spec: + podSelector: {} + policyTypes: + - Egress + egress: + # 1. DNS - zuerst, weil ohne DNS nichts geht. + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + # 2. Cluster-intern: Pods und Services (inkl. API-Server 10.43.0.1). + - to: + - ipBlock: + cidr: 10.42.0.0/16 + - ipBlock: + cidr: 10.43.0.0/16 + # 3. Alles uebrige - Foederation, ACME, SMTP, Registries, privates Netz - + # ausser Link-Local. + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + - 169.254.0.0/16