Ir al contenido
Español

TLS everywhere

Esta página aún no está disponible en tu idioma.

Production-secure means TLS on every network surface. Each component of the framework has its own TLS configuration; this page is the catalog of where to enable it and what cert material each needs.

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 between cluster nodes. See cluster security for the full discussion.

Every field here takes the certificate material, never a path to it — which is why the example reads the files itself. Nothing in the transport touches the filesystem, and none of Bun, Node or Deno accepts a filename in these fields.

cert and key are required together on a listener. A tls object with only one of them used to make the listener conclude “no TLS” and bind in plaintext, while still dialling out over TLS — so the cluster formed and nothing looked wrong. That combination is now refused at bind time, which also means a node mis-configured this way stops silently downgrading and starts failing loudly.

Don’t skip this — even on internal networks, defense in depth applies.

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);

For HTTPS at the application layer. Often terminated at the load balancer instead — in K8s, the Service/Ingress handles TLS, the app speaks plain HTTP internally. Pick by your infrastructure shape.

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);

The management server is internal-only by default — but TLS still helps:

  • Protects against lateral movement inside a compromised network.
  • Required by some compliance regimes regardless of network topology.

Each broker actor takes TLS in two separate steps, and confusing them is the usual reason a connection that says TLS turns out not to verify what you meant:

  • Turning TLS on is the URL scheme — amqps://, mqtts://, rediss://, wss:// — or withSsl(true) on Kafka. The driver then verifies the broker against the system trust store, which is all a publicly-trusted certificate needs.
  • Supplying certificate material is withTls({ … }): a private CA to trust, or a client certificate for mTLS. It configures the handshake rather than starting one, so keep the TLS scheme as well.

As on the cluster transport, every field carries the material itself, never a path to it — which is why each example reads the file. cert and key only mean anything together; supplying one without the other is refused when the actor starts, rather than throwing inside the handshake and being retried forever as a connection failure.

None of this is readable from HOCON, deliberately: a private key does not belong in a config file. withTls is code-only on every broker, and Kafka’s ssl config leaf stays boolean-only.

Kafka has no URL scheme to carry TLS, so ssl does both jobs — true for the system trust store, or the material itself:

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:// starts the handshake; withTls decides what it trusts. Drop withTls and verification falls back to the mqtt package’s defaults, which is the 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, which reaches amqplib as its socketOptions argument. AMQP URL query parameters carry heartbeat, frameMax, channelMax and locale — never certificate material, so there is no way to express this in the URL.

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 — common for production NATS. NATS has no tls URL scheme, so here withTls is also what asks the driver to negotiate TLS at all. JetStreamActor takes the identical option.

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

rediss:// URL scheme (note the double-s). Both the producer and the consumer client are built from the same material.

The one broker actor on the inbound side. It takes the same material as the cluster transport, and supplying ca turns on client-certificate verification unless requestClientCert: false says otherwise:

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);

For mutual TLS, add cert + key.

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

wss:// URL scheme. Cert verification follows the runtime’s TLS defaults.

Two client actors still take no certificate material, so they can reach a broker only when its certificate chains to a publicly trusted root:

  • WebsocketClientActor (above).
  • TcpSocketActor — the outbound TCP client. Its listener sibling TcpServerActor does take material; the dialling half does not.

Behind a private CA, that means terminating TLS at a proxy holding a public certificate, or adding the CA to the host’s trust store. It does not mean NODE_TLS_REJECT_UNAUTHORIZED=0, which disables verification for every TLS connection the process makes — the cluster transport and every HTTP client included.

N/A — local-file access. TLS not applicable.
const cassandraJournalOptions = CassandraJournalOptions.create()
.withContactPoints(['cass-1.example.com:9042'])
.withClient(clientWithMtls);
new CassandraJournal(cassandraJournalOptions);
// mTLS cert material (cert / key / ca) is configured on the
// cassandra-driver client passed via withClient()

Cassandra clusters typically run with mTLS in production.

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

Cloud object storage (S3, GCS, Azure Blob) always uses TLS. No configuration needed beyond pointing at the endpoint.

Three patterns in K8s production:

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 auto-renews certs before expiry. Most production K8s setups use it.

Vault Agent runs as a sidecar; pulls certs into the pod’s filesystem; renews automatically. Useful when you already have HashiCorp Vault.

For non-K8s environments, scheduled cron jobs that pull fresh certs from an internal CA + restart the affected services. Works but requires careful operational discipline.

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

Most internal setups: one CA, signs all client + server certs. The cert verification needs the CA cert at both ends.

Cloud-managed certificates (Let’s Encrypt, AWS ACM): use public CA bundles — already present in most runtimes. No explicit ca needed.