# 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