Setting Up Secure TLS for Galera Cluster Communication
This procedure builds on the least-privilege ruleset in Network Security & Firewall Rules for Galera, and answers one focused question: how do you encrypt the node-to-node replication channels of a MariaDB Galera Cluster without triggering an eviction, an IST fallback, or a split-brain during the rollout? Firewall scoping decides who may reach ports 4567, 4568, and 4444; TLS decides whether the write-sets crossing those ports can be read or forged by anyone who does. Enabling it correctly means generating a shared trust chain, injecting the right socket.ssl* keys into the Galera provider, and rolling the change through the group one node at a time so quorum is never lost. This page is the authoritative walkthrough for database administrators and platform teams who need production-grade encryption on the Galera Communication System (GCS) layer, not just on client SQL.
Context: Why GCS-Layer TLS Is Different
The require_secure_transport and ssl-cert directives most operators know secure the client-to-server path on port 3306. They do nothing for the replication mesh. Galera’s synchronous replication runs its own transport — group communication, Incremental State Transfer, and State Snapshot Transfer all flow through the Galera provider (libgalera_smm.so), which opens its own sockets and has its own, separate TLS stack configured entirely inside wsrep_provider_options (wsrep.cnf configuration).
That distinction matters because the replication channel carries the raw write-sets that feed the write-set certification process on every peer. An attacker on the shared cluster subnet who can read 4567 sees every committed row change in plaintext; one who can inject on it can attempt state poisoning that certification alone will not catch if the frames appear well-formed. TLS closes both gaps by giving each member a cryptographic identity and encrypting the wire. The catch is that the provider validates peers strictly: a single mismatched certificate, an unshared CA, or a clock skew past the handshake window takes the node out of the group rather than degrading gracefully.
Solution: Enable TLS Across the Group
The workflow is four ordered stages: build one trust chain, issue per-node certificates with correct Subject Alternative Names, inject the provider options, then roll the change so the group never drops below quorum.
Step 1 — Generate one shared CA
Every member must chain to the same certificate authority, or the mesh will partition into nodes that trust each other and nodes that do not. Generate a single long-lived CA once and distribute its public certificate to every node:
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
-keyout galera-ca.key -out galera-ca.pem \
-subj "/CN=Galera-Cluster-CA"
Keep galera-ca.key offline on a build host or in a secrets manager — it is never deployed to a node. Only galera-ca.pem (the public CA) ships to every member as the shared trust anchor.
Step 2 — Issue node certificates with SAN, not just CN
Modern OpenSSL (3.x) validates peer identity against the Subject Alternative Name extension; Common Name matching is deprecated by RFC 2818 and ignored by the provider’s verify routine. Each certificate must list every DNS name and IP the node is reached by. Render an OpenSSL config per node:
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
CN = galera-node-01
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = galera-node-01.internal
DNS.2 = galera-node-01
IP.1 = 10.0.1.10
Create the key and CSR, sign it against the CA, then verify the chain before the certificate ever reaches a node:
openssl req -newkey rsa:2048 -nodes \
-keyout node-key.pem -out node.csr -config node-01.cnf
openssl x509 -req -in node.csr -CA galera-ca.pem -CAkey galera-ca.key \
-CAcreateserial -days 825 -sha256 \
-extfile node-01.cnf -extensions v3_req -out node-cert.pem
openssl verify -CAfile galera-ca.pem node-cert.pem
A missing SAN, a revoked certificate, or an expired chain surfaces at handshake time as GCS: SSL handshake failed or error:0A000086:SSL routines::certificate verify failed in the MariaDB error log, and replication halts until it is fixed.
Step 3 — Inject the provider options
TLS for the replication mesh lives entirely in the socket.ssl* keys of wsrep_provider_options, inside the [mysqld] (or [galera]) section. The value is one semicolon-delimited string, so every key must sit in a single declaration:
[mysqld]
wsrep_provider_options="socket.ssl=YES; socket.ssl_cert=/etc/mysql/ssl/node-cert.pem; socket.ssl_key=/etc/mysql/ssl/node-key.pem; socket.ssl_ca=/etc/mysql/ssl/galera-ca.pem; socket.ssl_cipher=AES256-GCM-SHA384:CHACHA20-POLY1305; socket.ssl_compression=NO"
Two lines are load-bearing. socket.ssl_compression=NO is mandatory to close the CRIME/BREACH compression-oracle class of attack. Restricting socket.ssl_cipher to AEAD suites blocks protocol downgrade to a weak cipher. File permissions are equally strict: 0600 for the private key, 0644 for the CA and certificate — a key readable by group or world triggers WSREP: Failed to load SSL certificate at provider load, before the node ever joins.
Because wsrep_provider_options is read only when the provider loads, it cannot be changed with SET GLOBAL; the value takes effect only after a systemctl restart mariadb.
Step 4 — Roll it out without losing quorum
Never flip TLS on the whole group at once. Enable it on one node, restart, wait for it to rejoin Synced, then move to the next. A member with socket.ssl=YES can still complete the handshake against a peer that has it enabled, so the group keeps quorum through the transition as long as a majority stays reachable. Restarting every node simultaneously loses quorum and forces a bootstrap.
The same rolling discipline covered under state transfer applies: as long as the joiner’s last seqno is still inside the donor’s gcache window, it rejoins over fast IST on the encrypted channel; only if those write-sets have been purged does it fall back to a full SST.
Provider Option Reference
These are the socket.* keys that govern Galera-layer TLS, all set inside the single wsrep_provider_options string.
| Option | Type | Default | Recommended | Purpose |
|---|---|---|---|---|
socket.ssl |
boolean | NO |
YES |
Master switch; enables TLS on the GCS/IST replication sockets. |
socket.ssl_cert |
path | — | node-cert.pem | This node’s public certificate presented in the handshake. |
socket.ssl_key |
path | — | node-key.pem | Matching private key; must be 0600 or the provider refuses to load. |
socket.ssl_ca |
path | — | galera-ca.pem | Shared CA every member validates peers against; identical on all nodes. |
socket.ssl_cipher |
string | (library) | AES256-GCM-SHA384:CHACHA20-POLY1305 |
Restricts negotiation to AEAD suites; blocks downgrade. |
socket.ssl_compression |
boolean | NO |
NO |
Keep off — TLS compression enables the CRIME/BREACH attack class. |
Note that socket.ssl_cert, socket.ssl_key, and socket.ssl_ca must all be present together — setting socket.ssl=YES without a full trio produces a provider that starts but cannot complete a handshake, and the node hangs in Initialized never reaching Synced.
Verification
After the rolling restart, confirm the running provider actually negotiated TLS and the group reconverged. First, check the group’s view of itself:
SHOW GLOBAL STATUS WHERE Variable_name IN (
'wsrep_cluster_size', -- must equal the node count
'wsrep_cluster_status', -- must be 'Primary'
'wsrep_local_state_comment', -- must be 'Synced'
'wsrep_provider_capabilities' -- should advertise SSL
);
Then confirm the value the provider loaded matches what you deployed — the whole point of keeping TLS keys in one declaration:
SHOW GLOBAL VARIABLES LIKE 'wsrep_provider_options';
Finally, prove the wire is actually encrypted from a peer. A plaintext port answers this probe with cleartext; a TLS-wrapped port completes a handshake:
openssl s_client -connect 10.0.1.11:4567 \
-CAfile galera-ca.pem -cipher AES256-GCM-SHA384 </dev/null
A small pre-deployment gate makes the certificate check a hard stop rather than a manual step. This Python 3.9+ routine validates the chain and rejects a certificate inside its last 24 hours of validity:
import subprocess
import sys
def validate_tls_config(cert_path: str, ca_path: str) -> None:
checks = [
["openssl", "verify", "-CAfile", ca_path, cert_path],
["openssl", "x509", "-in", cert_path, "-noout", "-checkend", "86400"],
]
for cmd in checks:
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"[FAIL] {' '.join(cmd)}: {result.stderr.strip()}", file=sys.stderr)
sys.exit(1)
print("[OK] Certificate chain and expiry validated.")
if __name__ == "__main__":
validate_tls_config("/etc/mysql/ssl/node-cert.pem",
"/etc/mysql/ssl/galera-ca.pem")
Edge Cases & Gotchas
Handshake failure that reads like a network fault. When a node cannot establish TLS, the log records WSREP: gcs backend failed to connect to wsrep provider followed by an SSL alert — the same symptom a firewall drop produces. Map the alert to its real cause before touching the network:
| Error signature | Root cause | Verification command |
|---|---|---|
sslv3 alert handshake failure |
Cipher or protocol-version mismatch between nodes | openssl s_client -connect <peer>:4567 -cipher AES256-GCM-SHA384 |
certificate verify failed |
SAN mismatch, missing intermediate, or an unshared CA | openssl x509 -in node-cert.pem -noout -text | grep -A2 "Subject Alternative Name" |
WSREP: Failed to load SSL certificate |
Key permissions looser than 0600, or SELinux/AppArmor denial |
ls -l /etc/mysql/ssl/ && getenforce |
GCS: SSL handshake failed |
Clock skew beyond the handshake window, or an expired certificate | chronyc tracking && openssl x509 -in node-cert.pem -noout -dates |
Clock skew evicts silently. TLS certificate validity is time-bounded, so a node whose clock drifts past the notBefore/notAfter window fails every handshake even with a perfect certificate. Run chrony or systemd-timesyncd on every member; a few minutes of drift is enough to break the group.
Cloud images and containers reuse identities. A Galera image baked with an embedded node certificate, or a Docker/Kubernetes deployment that clones one node’s key across replicas, gives multiple members the same identity and a SAN that matches none of their real addresses. Generate the certificate at first boot from the instance’s actual hostname and IP, not at image-build time, and mount keys as per-node secrets rather than layers.
Last-resort recovery from a stalled rollout. If enabling TLS costs the group its quorum and it cannot re-form, temporarily comment out socket.ssl=YES on the designated bootstrap node only, start the group in plaintext, confirm wsrep_cluster_size reaches the expected count, then re-enable TLS node by node with a rolling restart. Before that, verify the cipher is available everywhere with openssl ciphers -v 'AES256-GCM-SHA384:CHACHA20-POLY1305' — Galera 4 needs OpenSSL 1.1.1+ for AEAD, which every current distribution satisfies.
Related
- Network Security & Firewall Rules for Galera — the parent guide that scopes the ports this encryption rides on
wsrep.cnfConfiguration Deep Dive — how thewsrep_provider_optionsstring is loaded, layered, and validated- Understanding Galera Synchronous Replication — why the replication channel carries plaintext write-sets by default
- Initial Data Synchronization Methods — the IST/SST transfer that must also complete over the encrypted socket