Zum Inhalt springen

Kubernetes-Netzwerkrichtlinien: Pod-Traffic absichern

Meistere Kubernetes-Netzwerkrichtlinien, um Pod-Traffic zu kontrollieren, Mikrosegmentierung umzusetzen und Workloads mehrschichtig zu schützen.

5 Min. Lesezeit
Diagramm eines Kubernetes-Clusters, das zeigt, wie Netzwerkrichtlinien den Datenverkehr zwischen Pods steuern

Standardmäßig kann in einem Kubernetes-Cluster jeder Pod mit jedem anderen Pod kommunizieren. Dieses flache Netzwerkmodell ist in der Entwicklung praktisch, in der Produktion jedoch gefährlich. Ein kompromittierter Pod kann uneingeschränkt auf deine Datenbank, interne APIs und die Kontrollebene zugreifen.

Netzwerkrichtlinien sind Kubernetes-native Firewalls, die festlegen, welche Pods miteinander kommunizieren dürfen. Sie setzen Mikrosegmentierung um – die Praxis, granulare Verkehrsregeln zwischen Workloads zu definieren, anstatt sich allein auf Perimeter-Sicherheit zu verlassen.

Das Standardverhalten des Netzwerks verstehen

Bevor eine NetworkPolicy angewendet wird, erlaubt Kubernetes jeglichen Ingress- und Egress-Datenverkehr zwischen Pods. Sobald du eine Richtlinie auf einen Pod anwendest, wechselt dieser Pod für die angegebene Richtung von „alles erlauben" zu „standardmäßig verweigern".

ymlyaml
# ❌ No network policies: every pod can reach everything
# Pod A (frontend) → Pod B (api) ✓
# Pod A (frontend) → Pod C (database) ✓  ← This shouldn't happen
# Pod B (api) → Pod C (database) ✓
# Pod D (compromised) → Pod C (database) ✓  ← Dangerous
ymlyaml
# ✅ Default deny: block everything, then allow explicitly
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}  # Applies to ALL pods in namespace
  policyTypes:
    - Ingress
    - Egress

Diese eine Richtlinie sperrt den gesamten Namespace sofort ab. Kein Pod kann mehr Datenverkehr empfangen oder senden, bis du Allow-Regeln erstellst. Fang hier an und baue schrittweise aus – das ist deutlich sicherer, als offen zu starten und Lücken im Nachhinein zu schließen.

Allow-Regeln für gängige Muster erstellen

Das häufigste Muster ist eine dreistufige Anwendung: Das Frontend spricht mit der API, die API spricht mit der Datenbank. Jede Stufe sollte nur ihren unmittelbaren Nachbarn erreichen können.

ymlyaml
# Allow frontend pods to receive traffic from ingress controller
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-ingress-to-frontend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: frontend
      tier: web
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              name: ingress-system
          podSelector:
            matchLabels:
              app: nginx-ingress
      ports:
        - protocol: TCP
          port: 3000
ymlyaml
# Allow API pods to receive traffic ONLY from frontend pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-api
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api-server
      tier: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
              tier: web
      ports:
        - protocol: TCP
          port: 8080
ymlyaml
# Allow database pods to receive traffic ONLY from API pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-to-database
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: postgres
      tier: database
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: api-server
              tier: backend
      ports:
        - protocol: TCP
          port: 5432

Wird der Frontend-Pod jetzt kompromittiert, kann der Angreifer zwar die API erreichen, aber nicht direkt auf die Datenbank zugreifen. Jeder weitere Schritt erfordert eine zusätzliche Kompromittierung, was seitliche Bewegungen im Netzwerk erheblich erschwert.

Egress-Datenverkehr kontrollieren

Ingress-Richtlinien schützen Pods vor unerwünschten eingehenden Verbindungen. Egress-Richtlinien verhindern, dass Pods unerwünschte ausgehende Verbindungen aufbauen – ebenso wichtig, um Datenexfiltration zu verhindern.

ymlyaml
# ❌ Pod can reach any external IP (data exfiltration risk)
# A compromised pod could send data to attacker-controlled servers
 
# ✅ Restrict API server egress to known destinations
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-server-egress
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api-server
  policyTypes:
    - Egress
  egress:
    # Allow DNS resolution
    - to:
        - namespaceSelector:
            matchLabels:
              name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
    # Allow connection to database
    - to:
        - podSelector:
            matchLabels:
              app: postgres
              tier: database
      ports:
        - protocol: TCP
          port: 5432
    # Allow connection to Redis
    - to:
        - podSelector:
            matchLabels:
              app: redis
      ports:
        - protocol: TCP
          port: 6379
    # Allow connection to external payment API
    - to:
        - ipBlock:
            cidr: 203.0.113.0/24  # Payment provider IP range
      ports:
        - protocol: TCP
          port: 443

Nimm DNS immer in deine Egress-Regeln auf. Ohne DNS können Pods keine Hostnamen auflösen – auch keine internen Servicenamen. Das ist der häufigste Fehler bei der Umsetzung von Egress-Richtlinien.

Tests und Validierung von Richtlinien

Netzwerkrichtlinien sind notorisch schwer zu testen. Eine falsch konfigurierte Richtlinie kann deine Anwendung stillschweigend lahmlegen. Nutze dafür dedizierte Testwerkzeuge und -muster.

tstypescript
// Policy validation script
interface PolicyTestCase {
  name: string;
  source: { namespace: string; labels: Record<string, string> };
  destination: { namespace: string; labels: Record<string, string>; port: number };
  expected: "allow" | "deny";
}
 
const testCases: PolicyTestCase[] = [
  {
    name: "Frontend can reach API",
    source: {
      namespace: "production",
      labels: { app: "frontend", tier: "web" },
    },
    destination: {
      namespace: "production",
      labels: { app: "api-server", tier: "backend" },
      port: 8080,
    },
    expected: "allow",
  },
  {
    name: "Frontend cannot reach database directly",
    source: {
      namespace: "production",
      labels: { app: "frontend", tier: "web" },
    },
    destination: {
      namespace: "production",
      labels: { app: "postgres", tier: "database" },
      port: 5432,
    },
    expected: "deny",
  },
  {
    name: "API can reach database",
    source: {
      namespace: "production",
      labels: { app: "api-server", tier: "backend" },
    },
    destination: {
      namespace: "production",
      labels: { app: "postgres", tier: "database" },
      port: 5432,
    },
    expected: "allow",
  },
  {
    name: "Random pod cannot reach database",
    source: {
      namespace: "production",
      labels: { app: "debug-pod" },
    },
    destination: {
      namespace: "production",
      labels: { app: "postgres", tier: "database" },
      port: 5432,
    },
    expected: "deny",
  },
];
 
async function runPolicyTests(
  tests: PolicyTestCase[]
): Promise<{ passed: number; failed: number; results: string[] }> {
  let passed = 0;
  let failed = 0;
  const results: string[] = [];
 
  for (const test of tests) {
    const actual = await probeConnection(
      test.source,
      test.destination
    );
 
    if (actual === test.expected) {
      passed++;
      results.push(`✅ ${test.name}: ${actual} (expected ${test.expected})`);
    } else {
      failed++;
      results.push(`❌ ${test.name}: ${actual} (expected ${test.expected})`);
    }
  }
 
  return { passed, failed, results };
}
 
async function probeConnection(
  source: PolicyTestCase["source"],
  destination: PolicyTestCase["destination"]
): Promise<"allow" | "deny"> {
  // In practice: kubectl run a temp pod with source labels,
  // attempt connection to destination, check exit code
  return "allow"; // Placeholder
}

Führe diese Tests in der CI gegen einen Staging-Cluster mit denselben angewendeten Richtlinien aus. Sie dienen sowohl als Dokumentation als auch als Regressionstests: Ändert jemand eine Richtlinie, erkennt die Testsuite unbeabsichtigte Änderungen.

Namespace-übergreifende Richtlinien

Microservices erstrecken sich häufig über mehrere Namespaces. Netzwerkrichtlinien unterstützen namespaceübergreifende Regeln über Namespace-Selektoren, wobei die Syntax genaue Aufmerksamkeit erfordert.

ymlyaml
# Allow monitoring namespace to scrape metrics from all production pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-monitoring-scrape
  namespace: production
spec:
  podSelector: {}  # All pods in production
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              name: monitoring
          podSelector:
            matchLabels:
              app: prometheus
      ports:
        - protocol: TCP
          port: 9090  # Metrics port
ymlyaml
# Allow production pods to send logs to logging namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-logging-egress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              name: logging
          podSelector:
            matchLabels:
              app: fluentd
      ports:
        - protocol: TCP
          port: 24224

Beschrifte deine Namespaces einheitlich. Das Label name: monitoring auf dem Monitoring-Namespace ist das, was den namespaceübergreifenden Selektor überhaupt funktionieren lässt. Ohne korrekte Namespace-Labels schlagen namespaceübergreifende Richtlinien still und leise fehl.

Verbindungsprobleme debuggen

Blockiert eine Netzwerkrichtlinie unerwartet Datenverkehr, kann die Fehlersuche frustrierend sein. Ein systematisches Vorgehen erspart dir stundenlanges Rätselraten.

shbash
# Step 1: Verify which policies apply to the target pod
kubectl get networkpolicy -n production -o wide
 
# Step 2: Check pod labels match policy selectors
kubectl get pod api-server-abc123 -n production --show-labels
 
# Step 3: Test connectivity from source pod
kubectl exec -it frontend-xyz789 -n production -- \
  wget --timeout=3 -qO- http://api-server:8080/health
 
# Step 4: Check if CNI plugin supports network policies
# (Not all do! Flannel without Calico won't enforce policies)
kubectl get pods -n kube-system | grep -E 'calico|cilium|weave'
tstypescript
// Automated connectivity diagnostic
interface ConnectivityDiagnostic {
  sourcePopod: string;
  targetPod: string;
  port: number;
  policiesApplied: string[];
  cniSupportsPolicy: boolean;
  namespaceLabelsCorrect: boolean;
  podLabelsMatch: boolean;
  portInPolicy: boolean;
}
 
function diagnoseBlockedTraffic(
  diag: ConnectivityDiagnostic
): string[] {
  const suggestions: string[] = [];
 
  if (!diag.cniSupportsPolicy) {
    suggestions.push(
      "CNI plugin does not support NetworkPolicy. " +
      "Install Calico, Cilium, or another policy-aware CNI."
    );
  }
 
  if (diag.policiesApplied.length === 0) {
    suggestions.push(
      "No policies found for target pod. " +
      "Check if default-deny exists in this namespace."
    );
  }
 
  if (!diag.podLabelsMatch) {
    suggestions.push(
      "Source pod labels don't match any ingress 'from' selector. " +
      "Verify labels on both pods and in the policy."
    );
  }
 
  if (!diag.portInPolicy) {
    suggestions.push(
      `Port ${diag.port} is not listed in any matching policy's port list.`
    );
  }
 
  return suggestions;
}

Die wichtigsten Erkenntnisse

Netzwerkrichtlinien verwandeln die Sicherheit von Kubernetes von einem flachen, offenen Netzwerk in eine sauber segmentierte Architektur. Das wichtigste Prinzip ist Default-Deny: Blockiere zunächst alles und erstelle dann explizite Allow-Regeln für bekannte Verkehrsmuster. So sind neue Deployments standardmäßig sicher, statt versehentlich offenzuliegen.

Die typischen Fallstricke sind gut vorhersehbar: DNS in Egress-Regeln vergessen, Namespaces für namespaceübergreifende Richtlinien nicht beschriften und auf einem Cluster deployen, dessen CNI keine Richtlinien unterstützt. Teste deine Richtlinien im Staging mit automatisierten Konnektivitätsprüfungen und behandle sie wie Code – versioniert, reviewt und über die CI gemeinsam mit den Workloads deployt, die sie schützen.

Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX