TLS Configuration¶
This guide explains how to configure TLS for Neo4j deployments and work with the certificates the operator manages — both as a cluster operator (configuring the operator's TLS surface) and as a client (connecting to a TLS-enabled deployment).
Overview¶
When you enable TLS using cert-manager, the Neo4j operator automatically:
- Creates certificate requests
- Stores certificates in Kubernetes secrets
- Mounts certificates in Neo4j pods
- Configures Neo4j SSL policies (Bolt, HTTPS, and intra-cluster)
- Renews certificates via cert-manager
Prerequisites¶
- cert-manager installed in your cluster (v1.20+ recommended)
- A cert-manager-compatible issuer (
ClusterIssuer,Issuer, or a third-party external issuer) - Neo4j Enterprise 5.26 LTS or any CalVer release (2025.x, 2026.x, ...)
Enabling TLS¶
Set spec.tls.mode: cert-manager and point at an issuer. That single block is enough for the operator to wire up the rest:
apiVersion: neo4j.neo4j.com/v1beta1
kind: Neo4jEnterpriseCluster
metadata:
name: secure-cluster
spec:
acceptLicenseAgreement: "eval"
# image and storage omitted for brevity (both are required in a real manifest)
topology:
servers: 3
tls:
mode: cert-manager
issuerRef:
name: ca-cluster-issuer
kind: ClusterIssuer
# Optional: customize cert lifetime
duration: 8760h # 1 year
renewBefore: 720h # 30 days
What the operator configures¶
When TLS is enabled, the operator automatically:
- Creates a
Certificateresource via cert-manager. - Configures three Neo4j SSL policies:
bolt— encrypted database connectionshttps— encrypted web interfacecluster— encrypted intra-cluster communication- Mounts certificates at
/ssl/in Neo4j containers. - Sets
server.bolt.tls_level=REQUIRED(plainbolt://connections are rejected).
DNS names baked into the cert¶
The operator includes every name the cluster needs in the certificate SANs:
- Service names (
{cluster}-client,{cluster}-internals,{cluster}-headless) - Pod FQDNs for every server in the cluster
- Short names and fully qualified names
You generally don't need to add anything yourself.
TLS and Cluster Formation (Strict Peer Validation)¶
TLS-enabled clusters use mutual TLS (mTLS) on intra-cluster links by default. When spec.tls.mode: cert-manager and spec.tls.strictPeerValidation: true (the default), the operator emits Neo4j's canonical production cluster SSL policy:
# Automatically set by the operator
dbms.ssl.policy.cluster.enabled=true
dbms.ssl.policy.cluster.base_directory=/ssl
dbms.ssl.policy.cluster.private_key=tls.key
dbms.ssl.policy.cluster.public_certificate=tls.crt
dbms.ssl.policy.cluster.trust_all=false # validate peers against ca.crt
dbms.ssl.policy.cluster.client_auth=REQUIRE # mutual TLS
dbms.ssl.policy.cluster.verify_hostname=true # peer cert must match FQDN
dbms.ssl.policy.cluster.tls_versions=TLSv1.3,TLSv1.2
The trust anchor is the cert-manager-issued Secret's ca.crt, projected to /ssl/trusted/ca.crt (Neo4j's expected trusted_dir). All cluster servers present a cert signed by the same CA, so peer validation works without any extra configuration.
Opting out: strictPeerValidation: false¶
The opt-out exists for installations whose external issuer (e.g. some custom AWSPCAClusterIssuer setups) does not populate ca.crt in the Secret it issues. Without ca.crt the trust anchor is missing and strict validation rejects every peer. The operator detects this at reconcile time and refuses to apply the strict config — status.phase flips to Failed with a message naming the issuer. Two paths forward:
- Recommended: fix the issuer to include the CA in its Secret output.
- Escape hatch: set
spec.tls.strictPeerValidation: false. The operator usestrust_all=true+client_auth=NONE, which Neo4j's own docs flag as "debugging only, since it does not offer security."
Hands off the SSL policy keys¶
Do not set
dbms.ssl.policy.*keys inspec.config. The operator owns the SSL policy surface end-to-end. The cluster validator rejects anydbms.ssl.policy.*/server.bolt.tls_level/server.directories.certificateskey inspec.configwith aForbiddenerror at apply time, because the operator runs Neo4j withserver.config.strict_validation.enabled=true— a duplicate key fromspec.configwould make Neo4j fail to start, so the validator blocks it before it can wedge the pod.
Bolt client-certificate auth is not exposed today¶
Bolt and HTTPS SSL policies are managed with client_auth=NONE so standard Neo4j drivers — none of which ship with operator-issued client certs — can connect. There is no typed field for Bolt mTLS today, and no design committed for one. The blocker isn't the operator (which can emit any dbms.ssl.policy.bolt.client_auth value) — it's that none of the Neo4j Bolt drivers expose a clean way to load a client cert from a K8s Secret in the application namespace, so the app-side story is the harder half. If you need this for a specific compliance regime, open an issue describing the driver/language combination.
Certificate Storage¶
Secret Names¶
- Cluster:
<cluster-name>-tls-secret - Standalone:
<standalone-name>-tls-secret
Certificate Files¶
Inside the secret, you'll find:
tls.crt- The server certificatetls.key- The private key (DO NOT SHARE)ca.crt- The Certificate Authority certificate
Retrieving Certificates for Clients¶
# Extract the CA certificate for client trust stores
kubectl get secret <deployment-name>-tls-secret \
-o jsonpath='{.data.ca\.crt}' | base64 -d > ca.crt
# Extract the server certificate
kubectl get secret <deployment-name>-tls-secret \
-o jsonpath='{.data.tls\.crt}' | base64 -d > server.crt
# Inspect (SANs, validity)
openssl x509 -in server.crt -text -noout
Client Configuration Examples¶
Neo4j Browser¶
-
With Self-Signed Certificates:
-
With Trusted Certificates: Configure cert-manager with a proper issuer (Let's Encrypt, internal CA)
Neo4j Drivers¶
Python Driver¶
from neo4j import GraphDatabase
import ssl
# For self-signed certificates (development)
driver = GraphDatabase.driver(
"bolt+ssc://localhost:7687",
auth=("neo4j", "password")
)
# For trusted certificates with CA verification
ssl_context = ssl.create_default_context()
ssl_context.load_verify_locations("neo4j-ca.crt")
driver = GraphDatabase.driver(
"bolt+s://localhost:7687",
auth=("neo4j", "password"),
ssl_context=ssl_context
)
Java Driver¶
import org.neo4j.driver.*;
import java.io.File;
// For self-signed certificates (development)
Driver driver = GraphDatabase.driver(
"bolt+ssc://localhost:7687",
AuthTokens.basic("neo4j", "password")
);
// For trusted certificates with custom trust store
Config config = Config.builder()
.withTrustStrategy(Config.TrustStrategy.trustCustomCertificateSignedBy(
new File("neo4j-ca.crt")
))
.build();
Driver driver = GraphDatabase.driver(
"bolt+s://localhost:7687",
AuthTokens.basic("neo4j", "password"),
config
);
JavaScript Driver¶
const neo4j = require('neo4j-driver');
const fs = require('fs');
// For self-signed certificates (development)
const driver = neo4j.driver(
'bolt+ssc://localhost:7687',
neo4j.auth.basic('neo4j', 'password')
);
// For trusted certificates with CA
const driver = neo4j.driver(
'bolt+s://localhost:7687',
neo4j.auth.basic('neo4j', 'password'),
{
encrypted: 'ENCRYPTION_ON',
trust: 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES',
trustedCertificates: [fs.readFileSync('neo4j-ca.crt', 'utf8')]
}
);
.NET Driver¶
using Neo4j.Driver;
using System.Security.Cryptography.X509Certificates;
// For self-signed certificates (development)
var driver = GraphDatabase.Driver(
"bolt+ssc://localhost:7687",
AuthTokens.Basic("neo4j", "password")
);
// For trusted certificates with CA
var certificate = new X509Certificate2("neo4j-ca.crt");
var driver = GraphDatabase.Driver(
"bolt+s://localhost:7687",
AuthTokens.Basic("neo4j", "password"),
o => o.WithTrustManager(TrustManager.CreateCustomTrustManager(certificate))
);
Creating Java Keystore¶
For Java applications, you may need to import the CA certificate into a keystore:
# Create a keystore with the CA certificate
keytool -import -file neo4j-ca.crt \
-alias neo4j-ca \
-keystore neo4j-truststore.jks \
-storepass changeit \
-noprompt
# Use in Java application
System.setProperty("javax.net.ssl.trustStore", "neo4j-truststore.jks");
System.setProperty("javax.net.ssl.trustStorePassword", "changeit");
Cypher Shell¶
# With self-signed certificates
cypher-shell -a bolt+ssc://localhost:7687 -u neo4j -p password
# With CA certificate
cypher-shell -a bolt+s://localhost:7687 -u neo4j -p password \
--encryption=true \
--trusted-ca=neo4j-ca.crt
Production Best Practices¶
1. Use Proper Certificate Issuers¶
Instead of self-signed certificates, configure cert-manager with:
spec:
tls:
mode: cert-manager
issuerRef:
name: letsencrypt-prod # or your internal CA
kind: ClusterIssuer
2. Certificate Rotation¶
The operator handles certificate renewal automatically through cert-manager. To manually trigger renewal:
# Delete the certificate to force regeneration
kubectl delete certificate <deployment-name>-tls
# The operator will recreate it automatically
3. External Access with Valid Certificates¶
For external access with valid certificates:
spec:
service:
ingress:
enabled: true
className: nginx
host: neo4j.example.com
tlsSecretName: neo4j-tls # Managed by cert-manager
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
4. Use a Third-Party / External Issuer¶
The operator supports any cert-manager-compatible issuer. Set kind to the issuer's resource kind and group to its API group:
AWS Private Certificate Authority:
spec:
tls:
mode: cert-manager
issuerRef:
name: aws-pca-issuer
kind: AWSPCAClusterIssuer
group: awspca.cert-manager.io
HashiCorp Vault (cert-manager built-in — no separate CRD; configure via spec.vault on a standard Issuer/ClusterIssuer):
spec:
tls:
mode: cert-manager
issuerRef:
name: vault-issuer
kind: ClusterIssuer
# group defaults to cert-manager.io
Google Certificate Authority Service:
spec:
tls:
mode: cert-manager
issuerRef:
name: google-cas-issuer
kind: GoogleCASClusterIssuer
group: cas-issuer.jetstack.io
The kind field is intentionally unrestricted — the operator passes it straight through to cert-manager's Certificate resource, which supports any registered external issuer CRD.
5. Override the CA Bundle (trustedCASecret)¶
By default, the operator automatically loads ca.crt from the cert-manager-generated TLS Secret ({cluster-name}-tls-secret) to verify Neo4j connections from inside the operator (admin Cypher calls, diagnostics, etc.). This works out of the box for issuers that populate ca.crt.
If your issuer doesn't include ca.crt (or you need to trust a different CA bundle), point at a separate Secret:
spec:
tls:
mode: cert-manager
issuerRef:
name: my-issuer
kind: ClusterIssuer
trustedCASecret: my-ca-bundle # Secret must contain key "ca.crt"
When trustedCASecret is set it takes priority over the auto-discovered cert-manager CA.
6. Customize Certificate Duration¶
spec:
tls:
mode: cert-manager
issuerRef:
name: ca-cluster-issuer
kind: ClusterIssuer
duration: 17520h # 2 years
renewBefore: 1440h # 60 days
7. Monitor Certificate Expiry¶
# Check certificate expiration
kubectl get certificate <deployment-name>-tls -o jsonpath='{.status.notAfter}'
# Set up alerts for certificates expiring within 30 days
Troubleshooting¶
Certificate Not Found¶
# Check if certificate was created
kubectl get certificate -n <namespace>
# Check cert-manager logs
kubectl logs -n cert-manager deployment/cert-manager
Connection Refused with TLS¶
-
Verify certificate is mounted:
-
Check Neo4j SSL configuration:
-
Verify ports are exposed:
Certificate Validation Errors¶
- Hostname Mismatch: Ensure you're connecting to a hostname that matches the certificate
- Expired Certificate: Check certificate validity dates
- Unknown CA: Import the CA certificate into your client's trust store
Quick Reference¶
Connection Schemes¶
| Scheme | Description | Use Case |
|---|---|---|
bolt:// |
Unencrypted, direct | Dev only, TLS-disabled deployments. Rejected by the server when TLS is enabled (server.bolt.tls_level=REQUIRED). |
bolt+s:// |
Encrypted with CA validation | Production with trusted certs |
bolt+ssc:// |
Encrypted, self-signed cert | Development with TLS |
neo4j:// |
Auto-negotiation, unencrypted | Cluster routing, TLS-disabled deployments only |
neo4j+s:// |
Auto-negotiation with TLS | Production cluster routing |
neo4j+ssc:// |
Auto-negotiation, self-signed | Development cluster routing with TLS |
Common Commands¶
# Export certificates
kubectl get secret <name>-tls-secret -o jsonpath='{.data.ca\.crt}' | base64 -d > ca.crt
# Test connection
openssl s_client -connect localhost:7687 -CAfile ca.crt -servername <cluster-name>-client
# View certificate chain
openssl s_client -showcerts -connect localhost:7687 < /dev/null
# Verify certificate
openssl verify -CAfile ca.crt server.crt
Outbound TLS — trusting external CAs from inside Neo4j¶
The sections above cover inbound TLS — clients connecting to Neo4j over Bolt/HTTPS. Neo4j also makes outbound TLS calls (OIDC providers, LDAPS, plugin downloads, cross-cluster replication peers, Aura Fleet Management). For those, Neo4j needs to trust the remote endpoint's CA.
The operator exposes three fields for this:
| Field | When to use |
|---|---|
spec.trustedCASecrets (list of {name, key}) |
Most cases. Adds each Secret's CA to Neo4j's JVM-default truststore. Works for OIDC, LDAPS, generic outbound HTTPS. Cert-manager-issued Secrets reference directly — default key ca.crt matches. Does not cover cross-cluster replication. |
spec.tls.additionalClusterTrustCAs (list of {name, key}) |
A peer cluster's CA for the operator-managed cluster SSL policy — needed for network-mode cross-cluster replication, set on both clusters. Each CA is projected as its own file into /ssl/trusted/ next to this cluster's ca.crt. Only relevant with strictPeerValidation: true. |
spec.extraVolumes + spec.extraVolumeMounts |
When a custom Neo4j SSL policy needs a CA at a specific filesystem path via dbms.ssl.policy.<name>.truststore_path. Mount it at a path of your own — see the limitation below. |
strictPeerValidation: false also changes how the operator verifies the server
The reason to set strictPeerValidation: false is an issuer that does not
populate ca.crt in the TLS Secret. That same ca.crt is what the
operator uses to verify the Neo4j server certificate on its own Bolt
connection — the connection it authenticates over as the admin user.
With no CA available, the operator uses certificate pinning: it reads
tls.crt from the same Secret — the server's own certificate — and trusts
exactly that one certificate, nothing else. The connection is verified, and
more narrowly than CA verification would be: another certificate from the
same issuer is refused too.
It is not silent: the operator logs Neo4j TLS: no usable 'ca.crt';
verifying the server by PINNING its certificate from 'tls.crt' with the
pinned certificate's SHA-256 fingerprint, SANs and expiry, on every affected
connection. The fingerprint is directly comparable to
openssl x509 -in tls.crt -noout -fingerprint -sha256.
What pinning costs you is rotation tolerance: if the server is restarted
onto a certificate that is not the one in the Secret, the operator's
connection is refused (x509: certificate signed by unknown authority)
until the Secret catches up.
Prefer an issuer that populates ca.crt (cert-manager's CA and
self-signed issuers both do) over turning this off.
This cannot add trust to the operator-managed cluster SSL policy
spec.extraVolumes is not a way to make the built-in cluster policy
trust an additional CA.
The cluster policy reads its trust anchors from /ssl/trusted/, which the
operator projects from the cluster's own cert-manager Secret with exactly one
entry (ca.crt → trusted/ca.crt). Two things block augmenting it:
/sslis a reserved mount path and is rejected by the validator./ssl/trustedis not itself reserved, so the validator permits it — but mounting a volume there shadows the operator's owntrusted/ca.crtand breaks intra-cluster TLS. Do not do this.
So spec.trustedCASecrets covers outbound TLS from Neo4j-the-server (it
builds a JVM truststore at /truststore/truststore.jks), and
spec.extraVolumes covers SSL policies you define at paths you choose.
Neither can add a peer CA to the operator-managed cluster policy.
For cross-cluster replication, use spec.tls.additionalClusterTrustCAs
instead: the operator projects each listed CA into /ssl/trusted/ itself,
alongside its own trusted/ca.crt. Network-mode replication needs mutual
trust, so list the other cluster's CA on both. Backup-based replication needs
no cross-cluster TLS trust at all. See
Cross-cluster replication.
The deprecated singular spec.auth.trustStore still works and is folded
into the same JKS at reconcile time.
# cert-manager Certificate produces a Secret with ca.crt; reference it directly
trustedCASecrets:
- name: corp-oidc-ca
Full prose in Security Best Practices Guide § JVM TrustStore for Internal CAs.