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 <noreply@anthropic.com>
464 lines
13 KiB
YAML
464 lines
13 KiB
YAML
# Default-deny ingress for the matrix namespace, with explicit allow rules per component.
|
|
# Egress is intentionally untouched (federation to arbitrary Matrix servers, ACME, SMTP,
|
|
# DNS all stay unrestricted).
|
|
#
|
|
# Lesson learned deploying the authentik namespace's equivalent policy: NetworkPolicy
|
|
# filters on the pod's actual container port, not the Service's external port (e.g.
|
|
# authentik-server's Service maps 80->9000). Wherever a Service here uses a *named*
|
|
# targetPort, this file references that name directly instead of guessing a number -
|
|
# Kubernetes resolves it from the pod spec, which is safer than a hardcoded port.
|
|
#
|
|
# matrix-stack-postgres already effectively has no dedicated chart NetworkPolicy of its
|
|
# own (unlike authentik-postgresql's Bitnami one) - the rules below are the only gate.
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: default-deny-ingress
|
|
namespace: matrix
|
|
spec:
|
|
podSelector: {}
|
|
policyTypes:
|
|
- Ingress
|
|
---
|
|
# axion1337.chat (root) -> Element Web
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: allow-ingress-element-web
|
|
namespace: matrix
|
|
spec:
|
|
podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: element-web
|
|
policyTypes:
|
|
- Ingress
|
|
ingress:
|
|
- from:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: kube-system
|
|
ports:
|
|
- protocol: TCP
|
|
port: element
|
|
---
|
|
# admin.axion1337.chat -> Element Admin
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: allow-ingress-element-admin
|
|
namespace: matrix
|
|
spec:
|
|
podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: element-admin
|
|
policyTypes:
|
|
- Ingress
|
|
ingress:
|
|
- from:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: kube-system
|
|
ports:
|
|
- protocol: TCP
|
|
port: http
|
|
---
|
|
# axion1337.chat/docs/setup -> Element desktop setup docs (our own nginx)
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: allow-ingress-element-web-docs
|
|
namespace: matrix
|
|
spec:
|
|
podSelector:
|
|
matchLabels:
|
|
app: element-web-docs
|
|
policyTypes:
|
|
- Ingress
|
|
ingress:
|
|
- from:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: kube-system
|
|
ports:
|
|
- protocol: TCP
|
|
port: 8080
|
|
---
|
|
# matrix.axion1337.chat AND the well-known delegation both front through haproxy
|
|
# (matrix-stack-synapse and matrix-stack-well-known Services both target haproxy's
|
|
# named ports, not synapse-main directly).
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: allow-ingress-haproxy
|
|
namespace: matrix
|
|
spec:
|
|
podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: haproxy
|
|
policyTypes:
|
|
- Ingress
|
|
ingress:
|
|
- from:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: kube-system
|
|
ports:
|
|
- protocol: TCP
|
|
port: haproxy-synapse
|
|
- protocol: TCP
|
|
port: haproxy-403
|
|
- protocol: TCP
|
|
port: haproxy-wkd
|
|
# Draupnir (Issue #18) calls Synapse's client-server API directly, in-namespace -
|
|
# without this it would be silently blocked by the default-deny policy.
|
|
- from:
|
|
- podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: draupnir
|
|
ports:
|
|
- protocol: TCP
|
|
port: haproxy-synapse
|
|
# Client-Scan-Dienst (Issue #19-Erweiterung) validiert Access-Tokens gegen Synapses
|
|
# eigenen /whoami-Endpoint statt eine eigene Auth zu bauen.
|
|
- from:
|
|
- podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: clamav-http-scanner
|
|
ports:
|
|
- protocol: TCP
|
|
port: haproxy-synapse
|
|
---
|
|
# account.axion1337.chat (Traefik) + matrix.axion1337.chat (also routes to MAS for some
|
|
# paths) + synapse-main calling MAS's internal port for session/token introspection.
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: allow-ingress-mas
|
|
namespace: matrix
|
|
spec:
|
|
podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: matrix-authentication-service
|
|
policyTypes:
|
|
- Ingress
|
|
ingress:
|
|
- from:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: kube-system
|
|
- podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: synapse-main
|
|
ports:
|
|
- protocol: TCP
|
|
port: 8080
|
|
- from:
|
|
- podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: synapse-main
|
|
ports:
|
|
- protocol: TCP
|
|
port: 8081
|
|
---
|
|
# Synapse itself: reached via haproxy (same namespace), calls from MAS (provisioning),
|
|
# metrics scraped by Alloy (monitoring namespace).
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: allow-ingress-synapse
|
|
namespace: matrix
|
|
spec:
|
|
podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: synapse-main
|
|
policyTypes:
|
|
- Ingress
|
|
ingress:
|
|
- from:
|
|
- podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: haproxy
|
|
- podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: matrix-authentication-service
|
|
ports:
|
|
- protocol: TCP
|
|
port: synapse-http
|
|
- protocol: TCP
|
|
port: synapse-health
|
|
- from:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: monitoring
|
|
ports:
|
|
- protocol: TCP
|
|
port: synapse-metrics
|
|
---
|
|
# mrtc.axion1337.chat (Traefik) for the auth handshake, plus Alloy scraping metrics.
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: allow-ingress-rtc-authorisation-service
|
|
namespace: matrix
|
|
spec:
|
|
podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: matrix-rtc-authorisation-service
|
|
policyTypes:
|
|
- Ingress
|
|
ingress:
|
|
- from:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: kube-system
|
|
- podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: matrix-rtc-sfu
|
|
ports:
|
|
- protocol: TCP
|
|
port: http
|
|
---
|
|
# The SFU: mrtc.axion1337.chat (Traefik) for signalling, Alloy for metrics, and the
|
|
# NodePort-exposed WebRTC media ports need to stay open to the internet by design -
|
|
# that's the actual point of a TURN/SFU media relay, not a mistake.
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: allow-ingress-rtc-sfu
|
|
namespace: matrix
|
|
spec:
|
|
podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: matrix-rtc-sfu
|
|
policyTypes:
|
|
- Ingress
|
|
ingress:
|
|
- from:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: kube-system
|
|
ports:
|
|
- protocol: TCP
|
|
port: http
|
|
- from:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: monitoring
|
|
ports:
|
|
- protocol: TCP
|
|
port: metrics
|
|
- from:
|
|
- ipBlock:
|
|
cidr: 0.0.0.0/0
|
|
ports:
|
|
- protocol: TCP
|
|
port: 30001
|
|
- protocol: UDP
|
|
port: 30002
|
|
---
|
|
# Postgres: only Synapse and MAS need data access; Alloy scrapes the exporter.
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: allow-ingress-postgres
|
|
namespace: matrix
|
|
spec:
|
|
podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: postgres
|
|
policyTypes:
|
|
- Ingress
|
|
ingress:
|
|
- from:
|
|
- podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: synapse-main
|
|
- podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: matrix-authentication-service
|
|
- podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: synapse-backup
|
|
ports:
|
|
- protocol: TCP
|
|
port: 5432
|
|
- from:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: monitoring
|
|
ports:
|
|
- protocol: TCP
|
|
port: 9187
|
|
---
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: allow-ingress-acme-solver
|
|
namespace: matrix
|
|
spec:
|
|
podSelector:
|
|
matchLabels:
|
|
acme.cert-manager.io/http01-solver: "true"
|
|
policyTypes:
|
|
- Ingress
|
|
ingress:
|
|
- from:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: kube-system
|
|
ports:
|
|
- protocol: TCP
|
|
port: 8089
|
|
|
|
# Note: coturn runs with hostNetwork: true, so NetworkPolicy does not apply to it at all -
|
|
# it's already gated by the Hetzner Cloud Firewall instead. Nothing to write here.
|
|
---
|
|
# ClamAV (Issue #19): only Synapse's check_media_file_for_spam module calls this, over
|
|
# clamd's plain TCP protocol on port 3310. Nothing else needs to reach it.
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: allow-ingress-clamav
|
|
namespace: matrix
|
|
spec:
|
|
podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: clamav
|
|
policyTypes:
|
|
- Ingress
|
|
ingress:
|
|
- from:
|
|
- podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: synapse-main
|
|
# Client-seitiger Scan-Dienst (Issue #19-Erweiterung) braucht denselben ClamAV.
|
|
- podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: clamav-http-scanner
|
|
ports:
|
|
- protocol: TCP
|
|
port: clamd
|
|
---
|
|
# axion1337.chat/_scan (Traefik) - client-seitiger Scan-Dienst, direkt vom Browser aufgerufen.
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: allow-ingress-clamav-http-scanner
|
|
namespace: matrix
|
|
spec:
|
|
podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: clamav-http-scanner
|
|
policyTypes:
|
|
- Ingress
|
|
ingress:
|
|
- from:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: kube-system
|
|
ports:
|
|
- protocol: TCP
|
|
port: http
|
|
---
|
|
# wiki.axion1337.chat (Traefik) -> Wiki.js (ADR-0014, #0048)
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: allow-ingress-wikijs
|
|
namespace: matrix
|
|
spec:
|
|
podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: wikijs
|
|
policyTypes:
|
|
- Ingress
|
|
ingress:
|
|
- from:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: kube-system
|
|
- podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: wikijs-config # Konfig-Job darf Wiki.js erreichen
|
|
ports:
|
|
- protocol: TCP
|
|
port: http
|
|
---
|
|
# Wiki.js -> sein eigener Postgres (nur von Wiki.js-Pods erreichbar)
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: allow-ingress-wikijs-postgres
|
|
namespace: matrix
|
|
spec:
|
|
podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: wikijs-postgres
|
|
policyTypes:
|
|
- Ingress
|
|
ingress:
|
|
- from:
|
|
- podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: wikijs
|
|
# Nächtliches Backup (wikijs-postgres-backup.yaml) braucht denselben DB-Zugang.
|
|
- podSelector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: wikijs-backup
|
|
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
|