TLS encryption
By the end of this page you will have a certificate installed, ScramDB accepting encrypted connections on the PostgreSQL wire protocol, and a psql session you can confirm is actually encrypted.
What TLS covers
TLS encrypts the PostgreSQL wire-protocol connection between a client and ScramDB. The server presents a certificate; the client can verify it. ScramDB does not request or verify a client certificate, so there is no mutual TLS (mTLS) mode: this is server-side TLS only, the same shape as a typical PostgreSQL deployment without clientcert verification turned on.
Connections between the nodes of a cluster use mutual TLS of their own, configured separately: see Between cluster nodes.
Step by step: enable TLS
-
Generate a self-signed certificate and key. For production, use a certificate from your own CA or a public one instead; this command is enough to prove the setup works end to end:
openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \-keyout server.key -out server.crt \-subj "/CN=scramdb.internal"Expected output:
server.crtandserver.keyin the current directory, no prompts (the-nodesflag skips the passphrase). -
Place both files where ScramDB can read them, and set
tls_certandtls_keyunder[general]in your config TOML:[general]tls_cert = "/etc/scramdb/server.crt"tls_key = "/etc/scramdb/server.key"Both fields default to empty, which disables TLS entirely: setting either path is what turns encryption on.
-
Restart ScramDB and check the log for a clean startup with no TLS-related fatal error.
-
Connect with TLS required and confirm the handshake succeeds:
psql "host=localhost port=5432 user=scramdb dbname=scramdb sslmode=require"Once connected, confirm the connection is actually encrypted:
\conninfoExpected output includes
SSL connection (protocol: TLSv1.3, ...). If TLS were not active,\conninfowould report the connection as not using SSL, orsslmode=requirewould have refused to connect at all.
Failure modes
A missing file, an unreadable file, a malformed PEM, or a certificate/key pair that don't match one another all fail the same way: startup logs a FATAL line and the process exits without starting. ScramDB does not fall back to plaintext when TLS is misconfigured. If you see the server exit immediately after setting tls_cert/tls_key, check the startup log: it names both paths and the reason the TLS acceptor couldn't be built. Fix the certificate or key and restart.
Requiring TLS for specific hosts
Combine TLS with a host-based rule to require it only for connections you don't already trust, for example everything outside your private network:
# /etc/scramdb/pg_hba.conf
host all all 10.0.0.0/8 trust
hostssl all all 0.0.0.0/0 password
hostssl all all ::/0 password
hostssl matches only a connection already using TLS; hostnossl matches only a connection that is not. A client attempting a plaintext connection against a line that requires hostssl fails to match that rule (and, if no other line matches, is rejected). The full hba_file grammar, including how rules are evaluated, is covered in Authentication.
In Docker
The Docker deployment guide shows the same tls_cert/tls_key keys mounted from PEM files at /etc/scramdb/server.crt and /etc/scramdb/server.key; use that path convention if you're running the published image.
Between cluster nodes
The sections above cover the client connection. Connections between the nodes of a cluster are encrypted separately, with mutual TLS: set tls_cert, tls_key and tls_ca under [cluster], all three or none.
[cluster]
node_name = "node-1"
tls_cert = "/etc/scramdb/cluster-tls/node-1.pem"
tls_key = "/etc/scramdb/cluster-tls/node-1-key.pem"
tls_ca = "/etc/scramdb/cluster-tls/ca.pem"
Every connection between two nodes, on each of the three cluster ports, then presents a certificate on both sides and verifies the other side's against tls_ca. Without the three keys, cluster traffic is plaintext and the node logs one warning at startup saying so. A missing or unreadable file, or a certificate and key that do not match, stops the node at startup.
What each certificate carries
- The node's own name. A certificate signed by the cluster CA only proves that its holder belongs to the cluster. Which node it is comes from the name the node announces (
node_name, or its hostname whennode_nameis unset), and a node refuses any peer whose certificate does not carry that name as a DNS subject alternative name or as its common name. The refusal is logged asthe certificate presented for node 'node-9' does not carry that nameand counted inscramdb_cluster_peer_identity_refusals_total, and the connection is not used. - The DNS names of its seed entries. A node that dials a peer checks the peer's certificate against the peer's node name when it knows it (a peer that connected to it first and is dialed back, and the second and third cluster ports of a peer it already reached), and otherwise against the host of the
seedsentry it dialed. A DNS host inseedsmust therefore appear in the certificate of every node it can lead to. - An IP address only when a seed names the node by that address. An address a node learned from discovery or from a peer's own connection is not an identity: a pod's IP changes with every restart. There, the dialer verifies that the certificate chains to
tls_caand then that it carries the node name the peer announces, so the certificate needs no IP address. Only aseedsentry written as an IP address makes that address part of the check.
Generate the certificates
The repository ships scripts/cluster_tls_certs.sh, which creates a cluster CA once and then either a certificate per node or one certificate every node shares.
One certificate per node, carrying the node name as its common name and as a DNS name, plus the extra names you list:
scripts/cluster_tls_certs.sh ./cluster-tls \
node-1=DNS:node-1.db.internal \
node-2=DNS:node-2.db.internal \
node-3=DNS:node-3.db.internal
Expected output: ca.pem and ca-key.pem, then node-1.pem and node-1-key.pem and so on for each node, with a line per node naming what its certificate carries. A later run with new node names reuses the same CA, so new nodes join the same trust domain. Copy ca.pem and each node's own pair to that node, and keep ca-key.pem off the nodes.
One certificate for every node, as a Kubernetes StatefulSet mounts one Secret on every pod: list the node names (the pod names), and the pods' stable DNS names as one wildcard:
scripts/cluster_tls_certs.sh ./cluster-tls --shared scramdb \
scramdb-0,scramdb-1,scramdb-2 'DNS:*.scramdb-headless.db.svc.cluster.local'
Expected output: scramdb.pem and scramdb-key.pem, with a line naming the names it carries. The Helm chart's install notes print this command with the exact names of the release (cluster.tls.enabled). A pod added later needs a certificate that lists it: run the command again with the longer list, under the same CA, and replace the Secret. See Kubernetes.
Without the repository, the same certificate for one node takes three openssl commands, given a CA in ca.pem and ca-key.pem:
openssl ecparam -name prime256v1 -genkey -noout -out node-1-key.pem
openssl req -new -key node-1-key.pem -subj "/CN=node-1" -out node-1.csr
openssl x509 -req -in node-1.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial \
-days 825 -sha256 -out node-1.pem \
-extfile <(printf 'subjectAltName=DNS:node-1,DNS:node-1.db.internal\nextendedKeyUsage=serverAuth,clientAuth\n')
Check what a certificate carries before you deploy it:
openssl x509 -in node-1.pem -noout -subject -ext subjectAltName
Expected output: subject=CN=node-1 and a DNS:node-1 entry among the subject alternative names.
What the connection uses
Cluster connections use TLS 1.3 only (every node runs the same engine). Each of a connection's two directions keeps its own keys once the handshake is done, so reading and writing on the same connection never wait for each other; the keys are renewed through the protocol's own key update long before they could wear out.