Zum Inhalt springen
Deutsch

Cluster-Sicherheit

Der Cluster-Transport ist standardmäßig unauthentifiziertes Plain-TCP. In Ordnung innerhalb eines privaten Netzwerks, in dem jeder Host bereits vertrauenswürdig ist. Nicht in Ordnung irgendwo sonst — öffentliches Internet, Multi-Tenant-K8s, nicht vertrauenswürdige Vermittler.

Diese Seite ist die How-to zur Absicherung des Cluster-Transports. Für den operativen Kontext siehe Operations — Cluster-Sicherheit.

BedrohungMaßnahme
LauschangriffTLS auf dem Cluster-Transport
MITM (Abfangen + Verändern)TLS + Zertifikatsprüfung
Unautorisierter Join (jeder mit Port-Zugriff kann joinen)mTLS (zertifikatsbasierte Peer-Authentifizierung)
Kompromittiertes Zertifikat (für gestohlene Identität ausgestellt)Cert-Rotation + Revocation
Replay eines mitgeschnittenen Gossip-FramesTLS, dazu die Sequence pro Sender, die jeder Cluster durchsetzt
Ein Mitglied spricht im Payload für einen anderen NodeFramework-Handler halten die im Payload behauptete Identität gegen den Peer der Verbindung — siehe was eine Replica ihren Peers glaubt

mTLS ist das, was die letzte Zeile überhaupt trägt, und nicht etwas, das sie doppelt: eine Behauptung gegen den Peer der Verbindung zu halten, ist erst dann etwas wert, wenn dieser Peer authentifiziert ist.

Diese Zeile gilt auch für das In-Process-Worker-Mesh — den einzigen Pfad, auf dem sie früher nicht hielt: Der WorkerBroker adressiert jeden Frame auf den MessagePort um, über den er hereinkam, ein Worker kann also nicht die Adresse eines Geschwisters in from schreiben. Dafür ist nichts zu konfigurieren, und kein mTLS trägt es — der Host vergibt jedem Worker seine Adresse und hält dessen Port, die Bindung ist also strukturell statt authentifiziert.

import { TcpTransport, NodeAddress, Cluster, ClusterOptions } from 'actor-ts/cluster';
const transport = new TcpTransport(
NodeAddress.parse('actor-ts://my-app@10.0.0.5:2552'),
system.log,
{
cert: fs.readFileSync('./tls/cluster.crt'),
key: fs.readFileSync('./tls/cluster.key'),
ca: fs.readFileSync('./tls/ca.crt'),
rejectUnauthorized: true,
},
);
const clusterOptions = ClusterOptions.create()
.withHost(host)
.withPort(port)
.withSeeds(seeds)
.withTransport(transport);
await Cluster.join(
system,
clusterOptions,
);

TLS-gewrapptes TCP, alles-oder-nichts pro Cluster. Jeder Node verwendet dieselbe TLS-Konfiguration; gemischtes Plain + TLS funktioniert nicht.

Das Minimum:

  • Ein CA-Cert + privater Schlüssel (zum Signieren von Per-Node-Certs).
  • Ein Per-Node-Cert + privater Schlüssel (von der CA signiert).
Terminal-Fenster
# CA erzeugen (einmalig):
openssl req -x509 -newkey rsa:4096 -keyout ca.key -out ca.crt -days 3650 -nodes \
-subj "/CN=actor-ts-ca"
# Per-Node-Cert:
openssl req -newkey rsa:4096 -keyout node-1.key -out node-1.csr -nodes \
-subj "/CN=node-1.cluster.local"
openssl x509 -req -in node-1.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out node-1.crt -days 365

Jeder Node lädt:

  • Sein eigenes Cert + Key.
  • Das CA-Cert (um die Certs der Peers zu verifizieren).
{
cert: fs.readFileSync('./tls/this-node.crt'),
key: fs.readFileSync('./tls/this-node.key'),
ca: fs.readFileSync('./tls/ca.crt'),
rejectUnauthorized: true, // ← Peers ohne gültiges Cert ablehnen
}

Mit rejectUnauthorized: true + einer gemeinsamen CA verlangt jede Verbindung von beiden Seiten ein gültiges, von der CA signiertes Cert. Keine anonymen Joins; kein Plain-Text-Fallback.

cert-manager + Secrets ist das Standardmuster:

# Cert verwaltet von cert-manager:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: actor-ts-cluster
spec:
secretName: actor-ts-cluster-tls
issuerRef:
name: actor-ts-ca
kind: ClusterIssuer
commonName: actor-ts
dnsNames:
- actor-ts-cluster.svc
duration: 8760h
renewBefore: 720h
---
# Pod mounted das TLS-Secret:
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
volumeMounts:
- name: tls
mountPath: /etc/tls
readOnly: true
volumes:
- name: tls
secret:
secretName: actor-ts-cluster-tls

cert-manager erneuert Certs automatisch; Pods laden sie beim Neustart neu. Für Cert-Rotation ohne Downtime nutze einen Rolling Restart, nachdem neue Certs bereitliegen.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: cluster-internal-only
spec:
podSelector:
matchLabels:
app: actor-ts
ingress:
- from:
- podSelector:
matchLabels:
app: actor-ts
ports:
- protocol: TCP
port: 2552

Nur app=actor-ts-Pods können Port 2552 erreichen. Kombiniert mit mTLS sind das zwei unabhängige Schichten.

TLS fügt ~5-15 % Overhead pro Byte auf dem Transport hinzu. Für typische Actor-Nachrichten (kleine Payloads, moderater Durchsatz) unbemerkbar. Für hochfrequenten Cluster-Verkehr messbar, aber selten der Flaschenhals.