Kai Ole Hartwig
6 Min. Lesezeit
Niedrig
Von

TYPO3 auf Kubernetes unter IT-Grundschutz, Teil 6: Netz und Mandantentrennung

Ein Kubernetes-Cluster ist von Haus aus offen. Jeder Pod erreicht jeden anderen, und jeder Pod darf ins Internet. Für mehrere TYPO3-Mandanten auf einem Cluster ist das der falsche Ausgangspunkt.

Dieser Teil zeigt, wie die Trennung funktioniert: vom Namespace über Default-Deny-Regeln bis zu Admission-Regeln, die Fehlkonfigurationen gar nicht erst zulassen. Und er zeigt, wo diese Trennung endet.

Teil 6 von 14 der Serie „TYPO3 auf Kubernetes unter IT-Grundschutz“. Die Übersicht aller Teile steht in Teil 1.

01 — Ein Namespace pro Mandant

Jeder Mandant bekommt einen eigenen Namespace. Das ist die Einheit, an der Rechte, Netzregeln, Quotas und die Pod Security hängen. Die Pod Security Admission setzt das Profil restricted über ein Label durch:

 

apiVersion: v1
kind: Namespace
metadata:
  name: tenant-a
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest

 

Das Profil verbietet unter anderem privilegierte Container, Host-Namespaces und das Ausführen als root. Es verlangt ein Seccomp-Profil und das Entfernen aller Capabilities.

Eine ResourceQuota begrenzt, wie viel CPU, Speicher und Storage ein Mandant insgesamt belegen darf. Eine LimitRange setzt Vorgaben für einzelne Container. So kann ein Mandant mit Last oder einem Fehler die anderen nicht verdrängen.

02 — Default Deny für Ingress und Egress

Die erste NetworkPolicy im Namespace verbietet alles, in beide Richtungen. Danach folgen gezielte Freigaben pro Komponente: der Ingress-Controller darf zum Proxy, der Proxy zur PHP-Laufzeit, die Anwendung zur Datenbank und zum Cache, alle zum DNS.

 

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: tenant-a
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: app-egress
  namespace: tenant-a
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/component: app
  policyTypes: [Egress]
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - {port: 53, protocol: UDP}
        - {port: 53, protocol: TCP}
    - to:
        - podSelector:
            matchLabels:
              app.kubernetes.io/component: db
      ports:
        - {port: 3306, protocol: TCP}
    - to:
        - podSelector:
            matchLabels:
              app.kubernetes.io/component: cache
      ports:
        - {port: 6379, protocol: TCP}
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0
            except:
              - 10.0.0.0/8
              - 172.16.0.0/12
              - 192.168.0.0/16
              - 169.254.0.0/16
      ports:
        - {port: 443, protocol: TCP}

 

Der Internet-Zugang ist auf Port 443 beschränkt. Private Netze und der Bereich 169.254.0.0/16 sind ausgenommen. Darin liegt der Metadatendienst der Cloud, über den ein kompromittierter Pod sonst an Zugangsdaten des Knotens kommen könnte. Im Dual-Stack-Betrieb braucht es denselben Block für IPv6, sonst bleibt dort alles offen. Mehr zu Dual-Stack steht im Beitrag zu IPv6-only und Dual-Stack auf Kubernetes.

Wichtig: NetworkPolicies wirken nur, wenn das CNI-Plugin sie durchsetzt. Ein kurzer Test mit einem Pod, der eine verbotene Verbindung versucht, gehört deshalb zur Einrichtung dazu.

03 — Admission-Regeln statt Hoffnung

Die Pod Security deckt das Standardprofil ab. Eigene Regeln kommen über eine ValidatingAdmissionPolicy dazu. Sie ist seit Kubernetes 1.30 allgemein verfügbar und kommt ohne externen Webhook aus. Die Regeln sind CEL-Ausdrücke, die der API-Server selbst auswertet:

 

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: tenant-pod-rules
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: [CREATE, UPDATE]
        resources: [pods, pods/ephemeralcontainers]
  variables:
    - name: images
      expression: "object.spec.containers.map(c, c.image) + (has(object.spec.initContainers) ? object.spec.initContainers.map(c, c.image) : []) + (has(object.spec.ephemeralContainers) ? object.spec.ephemeralContainers.map(c, c.image) : [])"
  validations:
    - expression: "!has(object.spec.hostNetwork) || !object.spec.hostNetwork"
      message: "hostNetwork ist nicht erlaubt"
    - expression: "!has(object.spec.volumes) || object.spec.volumes.all(v, !has(v.hostPath))"
      message: "hostPath ist nicht erlaubt"
    - expression: "variables.images.all(i, i.startsWith('registry.example.org/'))"
      message: "Images nur aus der eigenen Registry"
    - expression: "has(object.spec.automountServiceAccountToken) && !object.spec.automountServiceAccountToken"
      message: "Service-Account-Token nicht automatisch einbinden"

 

Die Registry-Regel prüft bewusst alle Container eines Pods, auch initContainer und nachträglich angehängte Debug-Container. Sonst käme ein fremdes Image einfach über einen dieser Wege in den Pod. Deshalb greift die Policy auch auf die Subressource pods/ephemeralcontainers. Das Debug-Image für kubectl debug aus Teil 2 muss dann ebenfalls aus der eigenen Registry kommen.

Ein ValidatingAdmissionPolicyBinding bindet die Regel an alle Namespaces der Mandanten. Eine zweite Regel prüft, dass jeder Mandanten-Namespace das Label für restricted trägt. So kann niemand die Pod Security unbemerkt abschwächen.

Die Regel zum Service-Account-Token hat einen einfachen Grund. Eine TYPO3-Anwendung braucht keinen Zugriff auf die Kubernetes-API. Ein automatisch eingebundenes Token wäre im Fall einer Kompromittierung nur ein zusätzliches Werkzeug für den Angreifer.

04 — Wo die Isolation endet

Namespaces, NetworkPolicies und Admission-Regeln trennen Mandanten auf der Ebene von Kubernetes. Alle Pods auf einem Knoten teilen sich aber denselben Kernel. Eine Schwachstelle im Kernel kann diese Grenzen überwinden.

Bei hohem Schutzbedarf gibt es zwei Wege, die Grenze weiter nach unten zu ziehen:

Bezug zum IT-Grundschutz

Die Planung der Trennung ist Teil von APP.4.4.A1. Die Separierung von Pods (APP.4.4.A4) und der Netze (APP.4.4.A7) setzen Pod Security und NetworkPolicies um. Die Regel gegen automatisch eingebundene Tokens gehört zur Nutzung von Service-Accounts (APP.4.4.A9). Für erhöhten Schutzbedarf kommen dedizierte Knoten (APP.4.4.A14), die Trennung auf Knoten- und Cluster-Ebene (APP.4.4.A15) und Mikro-Segmentierung (APP.4.4.A18) dazu, sowie die weitergehende Isolation und Kapselung der Container (SYS.1.6.A26). Aus SYS.1.6 greift die Separierung der Administrations- und Zugangsnetze (SYS.1.6.A5), aus der Netzplanung NET.1.1.

Häufige Fragen

Reicht ein Namespace pro Mandant für die Trennung?+

Für die Ordnung ja, für die Isolation nicht. Ein Namespace allein verhindert keinen Netzverkehr und keine privilegierten Pods. Erst Pod Security, NetworkPolicies und Quotas machen daraus eine Grenze. Den Kernel teilen sich die Pods trotzdem.

Warum auch den ausgehenden Verkehr einschränken?+

Eine kompromittierte Anwendung lädt typischerweise Werkzeuge nach oder leitet Daten aus. Ohne Egress-Regeln geht beides ungehindert. Besonders wichtig ist der Metadatendienst der Cloud, über den sonst Zugangsdaten des Knotens erreichbar wären.

Pod Security oder Admission Policy?+

Beides. Die Pod Security setzt das Standardprofil durch. Die ValidatingAdmissionPolicy ergänzt eigene Regeln, etwa zur Registry oder zum Service-Account-Token. Sie stellt außerdem sicher, dass das Label für die Pod Security überhaupt gesetzt ist.

Wann brauche ich dedizierte Knoten?+

Bei hohem Schutzbedarf oder wenn Anwendungen mit sehr unterschiedlichem Schutzbedarf im selben Cluster laufen. Eine Schwachstelle im Kernel kann die Grenzen zwischen Namespaces überwinden. Getrennte Knoten ziehen die Grenze eine Ebene tiefer.

Fazit

Mandantentrennung im Cluster ist eine Reihe von Schichten. Der Namespace ordnet, die Pod Security begrenzt, NetworkPolicies schneiden die Wege zu, und Admission-Regeln verhindern, dass jemand diese Schichten aus Versehen aufweicht. Bei hohem Schutzbedarf gehört die ehrliche Frage dazu, ob ein geteilter Kernel noch reicht.

Weiter geht es mit Teil 7: Secrets ohne Git. Zurück zu Teil 5: mTLS auf jeder Strecke.

Ich trenne die Mandanten in deinem Cluster sauber. Im Netz, bei der Zulassung und auf Knotenebene.

Namespaces mit Pod Security restricted, Default-Deny-NetworkPolicies mit gezielten Freigaben, Admission-Regeln ohne externen Webhook und eine Einschätzung, ob dedizierte Knoten nötig sind.

Plattform-Betrieb statt Beratung auf Papier: Auf Wunsch betreibe ich die Mandantentrennung dauerhaft mit.

Termin buchen →

Über den Autor

Foto von Kai Ole Hartwig.

Kai Ole Hartwig

Freiberuflicher DevSecOps-Berater · OnlyOle Consulting

Programmiert seit 2002 – autodidaktisch gelernt, 2012 mit KO-Web selbständig gemacht. Über 100 Projekte, Fokus auf Security, Performance, Automatisierung und Qualität. Heute freiberuflich: DevSecOps-Beratung, Schulungen und Softwareentwicklung.