Zum Inhalt springen
Deutsch

TLS everywhere

Produktions-sicher heißt TLS auf jeder Netzwerkoberfläche. Jede Komponente des Frameworks hat ihre eigene TLS-Konfiguration; diese Seite ist der Katalog, wo TLS zu aktivieren ist und welches Cert-Material jede Komponente braucht.

import { TcpTransport, Cluster, ClusterOptions } from 'actor-ts/cluster';
const transport = new TcpTransport(self, 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);

Mutually authenticated TLS zwischen Cluster-Nodes. Siehe Cluster-Sicherheit für die vollständige Diskussion.

Jedes Feld hier nimmt das Zertifikatsmaterial selbst entgegen, niemals einen Pfad darauf — deshalb liest das Beispiel die Dateien selbst ein. Nichts im Transport greift auf das Dateisystem zu, und weder Bun noch Node noch Deno akzeptiert in diesen Feldern einen Dateinamen.

cert und key sind auf einem Listener nur gemeinsam gültig. Ein tls-Objekt mit nur einem von beiden brachte den Listener bisher zu dem Schluss „kein TLS”, und er lauschte im Klartext — während dieselbe Konfiguration weiterhin über TLS hinauswählte. Der Cluster bildete sich also, und nichts sah falsch aus. Diese Kombination wird jetzt beim Bind abgelehnt; ein so fehlkonfigurierter Knoten degradiert damit nicht mehr still, sondern scheitert laut.

Nicht überspringen — auch in internen Netzwerken gilt Defense in Depth.

import { HttpExtensionId } from 'actor-ts/http';
const http = system.extension(HttpExtensionId);
await http.newServerAt('0.0.0.0', 8443)
.useBackend(new FastifyBackend({
https: {
cert: fs.readFileSync('./tls/http.crt'),
key: fs.readFileSync('./tls/http.key'),
},
}))
.bind(routes);

Für HTTPS auf der Anwendungsebene. Oft am Load Balancer terminiert stattdessen — in K8s übernimmt Service/Ingress TLS, die App spricht intern Plain-HTTP. Wählen je nach Form deiner Infrastruktur.

import { FastifyBackend } from 'actor-ts/http';
import { managementRoutes } from 'actor-ts/management';
const routes = managementRoutes(system, cluster);
// TLS is a property of the HTTP backend — Fastify's `https` option.
const tlsBackend = new FastifyBackend({
https: {
cert: fs.readFileSync('./tls/mgmt.crt'),
key: fs.readFileSync('./tls/mgmt.key'),
},
});
await system.http(8558, { backend: tlsBackend }).bind(routes);

Der Management-Server ist standardmäßig nur intern — aber TLS hilft trotzdem:

  • Schützt vor lateraler Bewegung in einem kompromittierten Netzwerk.
  • Wird von manchen Compliance-Regimen unabhängig von der Netzwerk-Topologie verlangt.

Jeder Broker-Actor nimmt TLS in zwei getrennten Schritten entgegen, und die beiden zu verwechseln ist der übliche Grund dafür, dass eine Verbindung zwar TLS sagt, aber nicht das verifiziert, was gemeint war:

  • TLS einschalten macht das URL-Schema — amqps://, mqtts://, rediss://, wss:// — oder withSsl(true) bei Kafka. Der Treiber verifiziert den Broker dann gegen den System-Trust-Store, was für ein öffentlich vertrauenswürdiges Zertifikat genügt.
  • Zertifikatsmaterial übergeben macht withTls({ … }): eine private CA, der vertraut werden soll, oder ein Client-Zertifikat für mTLS. Es konfiguriert den Handshake, statt einen zu starten — das TLS-Schema bleibt also zusätzlich nötig.

Wie beim Cluster-Transport trägt jedes Feld das Material selbst, nie einen Pfad darauf — deshalb liest jedes Beispiel die Datei selbst. cert und key ergeben nur gemeinsam Sinn; eines ohne das andere wird beim Start des Actors abgelehnt, statt im Handshake zu werfen und als Verbindungsfehler endlos wiederholt zu werden.

Nichts davon ist aus HOCON lesbar, und das mit Absicht: Ein Private Key gehört nicht in eine Config-Datei. withTls gibt es nur im Code, und Kafkas ssl-Config-Leaf bleibt boolean-only.

Kafka hat kein URL-Schema, das TLS tragen könnte — deshalb erledigt ssl beide Aufgaben: true für den System-Trust-Store, oder das Material selbst:

const kafkaOptions = KafkaOptions.create()
.withBrokers(['kafka-1:9093'])
.withSsl({
ca: fs.readFileSync('./tls/ca.crt', 'utf8'),
cert: fs.readFileSync('./tls/client.crt', 'utf8'),
key: fs.readFileSync('./tls/client.key', 'utf8'),
})
.withSasl({
mechanism: 'scram-sha-512',
username: process.env.KAFKA_USER!,
password: process.env.KAFKA_PASS!,
});
new KafkaActor(kafkaOptions);
const mqttOptions = MqttOptions.create()
.withBrokerUrl('mqtts://mqtt.example.com:8883')
.withCredentials(process.env.MQTT_USER, process.env.MQTT_PASS)
.withTls({
ca: fs.readFileSync('./tls/ca.crt', 'utf8'),
});
new MqttActor(mqttOptions);

mqtts:// startet den Handshake; withTls entscheidet, wem er vertraut. Ohne withTls fällt die Verifizierung auf die Defaults des mqtt-Pakets zurück, also auf den System-Trust-Store.

const amqpOptions = AmqpOptions.create()
.withUrl('amqps://rabbitmq.example.com:5671')
.withTls({
ca: fs.readFileSync('./tls/ca.crt', 'utf8'),
});
new AmqpActor(amqpOptions);

amqps:// plus withTls, das amqplib als dessen socketOptions-Argument erreicht. AMQP-URL-Query-Parameter tragen heartbeat, frameMax, channelMax und locale — nie Zertifikatsmaterial; in der URL lässt sich das also gar nicht ausdrücken.

const natsOptions = NatsOptions.create()
.withServers(['nats://nats.example.com:4222'])
.withTls({
ca: fs.readFileSync('./tls/ca.crt', 'utf8'),
cert: fs.readFileSync('./tls/client.crt', 'utf8'),
key: fs.readFileSync('./tls/client.key', 'utf8'),
});
new NatsActor(natsOptions);

mTLS via withTls — üblich für Produktions-NATS. NATS hat kein tls-URL-Schema, deshalb ist withTls hier zugleich das, was den Treiber überhaupt zum TLS-Aushandeln bringt. JetStreamActor nimmt dieselbe Option.

const redisStreamsOptions = RedisStreamsOptions.create()
.withUrl('rediss://redis.example.com:6380')
.withTls({
ca: fs.readFileSync('./tls/ca.crt', 'utf8'),
});
new RedisStreamsActor(redisStreamsOptions);

rediss://-URL-Schema (beachte das doppelte s). Producer- und Consumer-Client werden aus demselben Material gebaut.

Der einzige Broker-Actor auf der eingehenden Seite. Er nimmt dasselbe Material wie der Cluster-Transport, und ein gesetztes ca schaltet die Client-Zertifikatsprüfung ein, sofern nicht requestClientCert: false etwas anderes sagt:

const tcpServerOptions = TcpServerOptions.create()
.withBindPort(9500)
.withTarget(handler)
.withTls({
cert: fs.readFileSync('./tls/server.crt', 'utf8'),
key: fs.readFileSync('./tls/server.key', 'utf8'),
ca: fs.readFileSync('./tls/ca.crt', 'utf8'),
});
new TcpServerActor(tcpServerOptions);
const grpcClientOptions = GrpcClientOptions.create()
.withEndpoint('orders.example.com:50051')
.withCredentials({
kind: 'tls',
rootCerts: fs.readFileSync('./tls/ca.crt'),
});
new GrpcClientActor(grpcClientOptions);

Für Mutual TLS cert + key ergänzen.

const webSocketClientOptions = WebsocketClientOptions.create().withUrl('wss://realtime.example.com/feed');
new WebsocketClientActor(webSocketClientOptions);

wss://-URL-Schema. Cert-Verifizierung folgt den TLS-Defaults der Runtime.

Zwei Client-Actors nehmen weiterhin kein Zertifikatsmaterial und erreichen einen Broker daher nur, wenn dessen Zertifikat auf eine öffentlich vertrauenswürdige Wurzel zurückführt:

  • WebsocketClientActor (oben).
  • TcpSocketActor — der ausgehende TCP-Client. Sein Listener-Pendant TcpServerActor nimmt Material; die wählende Hälfte nicht.

Hinter einer privaten CA heißt das: TLS an einem Proxy mit öffentlichem Zertifikat terminieren, oder die CA in den Trust-Store des Hosts aufnehmen. Es heißt nicht NODE_TLS_REJECT_UNAUTHORIZED=0 — das deaktiviert die Verifizierung für jede TLS-Verbindung des Prozesses, Cluster-Transport und sämtliche HTTP-Clients eingeschlossen.

Nicht zutreffend — lokaler Datei-Zugriff. TLS nicht anwendbar.
const cassandraJournalOptions = CassandraJournalOptions.create()
.withContactPoints(['cass-1.example.com:9042'])
.withClient(clientWithMtls);
new CassandraJournal(cassandraJournalOptions);
// mTLS-Cert-Material (cert / key / ca) wird auf dem via withClient()
// übergebenen cassandra-driver-Client konfiguriert

Cassandra-Cluster laufen in Produktion typischerweise mit mTLS.

const s3ObjectStorageOptions = S3ObjectStorageOptions.create().withRegion('eu-west-1');
const objectStorageDurableStateStoreOptions = ObjectStorageDurableStateStoreOptions.create().withBackend(new S3ObjectStorageBackend(s3ObjectStorageOptions));
new ObjectStorageDurableStateStore(objectStorageDurableStateStoreOptions);
// S3 nutzt per Default HTTPS

Cloud-Object-Storage (S3, GCS, Azure Blob) nutzt immer TLS. Keine Konfiguration nötig außer dem Endpoint.

Drei Muster in K8s-Produktion:

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

cert-manager erneuert Certs automatisch vor Ablauf. Die meisten Produktions-K8s-Setups nutzen ihn.

Vault Agent läuft als Sidecar; zieht Certs in das Pod-Filesystem; erneuert automatisch. Nützlich, wenn du schon HashiCorp Vault hast.

Für Nicht-K8s-Umgebungen geplante Cron-Jobs, die frische Certs aus einer internen CA ziehen + die betroffenen Services neu starten. Funktioniert, erfordert aber sorgfältige operative Disziplin.

ca: fs.readFileSync('./tls/ca.crt'),

Die meisten internen Setups: eine CA, signiert alle Client- + Server-Certs. Die Cert-Verifizierung braucht das CA-Cert auf beiden Enden.

Cloud-managed Certificates (Let’s Encrypt, AWS ACM): nutze öffentliche CA-Bundles — in den meisten Runtimes bereits vorhanden. Kein expliziter ca nötig.