The PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target error is one of the most common issues when deploying ForgeRock Directory Services (DS) in production. It means the Java runtime cannot verify the TLS certificate chain — and until you fix it, LDAPS connections, replication, and AM-to-DS communication will all fail.

Clone the companion repo: All diagnostic and fix scripts from this guide are available at IAMDevBox/forgerock-ds-cert-troubleshoot. Clone it, configure config.env, and run ./scripts/diagnose.sh ds.example.com 1636 for instant diagnosis.

Understanding the Error

The full stack trace typically looks like this:

javax.net.ssl.SSLHandshakeException: PKIX path building failed:
  sun.security.provider.certpath.SunCertPathBuilderException:
    unable to find valid certification path to requested target
    at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:131)
    at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:378)
    at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:321)
    at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(...)

This happens at the TLS handshake layer, before any LDAP protocol exchange occurs. The client (AM, IDM, another DS, or any Java application) tried to connect over TLS and could not build a certificate chain from the server’s certificate to a trusted root CA in its truststore.

Where This Error Occurs in ForgeRock

ConnectionProtocolDefault PortTypical Trigger
AM → DS (user store)LDAPS1636Self-signed DS cert not in AM truststore
AM → DS (config store)LDAPS1636Certificate renewed without updating AM
AM → DS (CTS)LDAPS1636New DS node with different certificate
DS → DS (replication)TLS8989Peer certificate not in local truststore
IDM → DS (connector)LDAPS1636Connector truststore not configured
External LDAP client → DSLDAPS1636Client missing CA certificate
DS admin tools → DSTLS4444Admin port requires trusted cert

Step-by-Step Diagnosis

Step 1: Identify the Exact Certificate

Use openssl to see what certificate the DS server is presenting:

# Check the certificate chain DS is presenting
openssl s_client -connect ds.example.com:1636 -showcerts </dev/null 2>/dev/null

# Extract just the server certificate details
openssl s_client -connect ds.example.com:1636 </dev/null 2>/dev/null | \
  openssl x509 -text -noout

# Check expiration date
openssl s_client -connect ds.example.com:1636 </dev/null 2>/dev/null | \
  openssl x509 -noout -dates

Key things to look for in the output:

Certificate chain
 0 s:CN=ds.example.com
   i:CN=ds.example.com          ← Self-signed (subject == issuer)

---OR---

Certificate chain
 0 s:CN=ds.example.com
   i:CN=Example Intermediate CA  ← CA-signed
 1 s:CN=Example Intermediate CA
   i:CN=Example Root CA           ← Full chain present

If the chain shows subject == issuer, this is a self-signed certificate — the most common cause of PKIX errors.

Step 2: Check the DS Keystore

Use dskeymgr (ForgeRock DS 7.x+) to list certificates in the DS keystore:

# List all certificates in DS keystore
dskeymgr list-certificates \
  --hostname localhost \
  --port 4444 \
  --bindDN "uid=admin" \
  --bindPassword "password" \
  --trustAll

# For DS 6.x and earlier, use dsconfig
dsconfig get-key-manager-provider-prop \
  --provider-name "Default Key Manager" \
  --hostname localhost \
  --port 4444 \
  --bindDN "uid=admin" \
  --bindPassword "password" \
  --trustAll

Step 3: Check the Client Truststore

Determine which truststore the client (AM, IDM, etc.) is using:

# Check JVM default truststore
echo $JAVA_HOME
ls -la $JAVA_HOME/lib/security/cacerts

# List trusted certificates
keytool -list -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit

# Search for a specific alias
keytool -list -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit | grep -i forgerock

# Check if DS certificate is trusted
keytool -list -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit -alias forgerock-ds

If the DS certificate (or its CA) is not listed, that’s your problem.

Step 4: Verify the Chain

# Download the full chain
openssl s_client -connect ds.example.com:1636 -showcerts </dev/null 2>/dev/null | \
  awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/{print}' > full-chain.pem

# Verify the chain
openssl verify -CAfile /path/to/ca-bundle.pem full-chain.pem

# Check for missing intermediates
openssl verify full-chain.pem
# If this fails with "unable to get local issuer certificate", you're missing an intermediate CA

Cause 1: Self-Signed Certificate (Most Common)

ForgeRock DS generates a self-signed certificate during installation. This works for --trustAll connections but fails for any client doing proper certificate validation.

Fix: Import the Self-Signed Certificate

# 1. Export the DS certificate
openssl s_client -connect ds.example.com:1636 </dev/null 2>/dev/null | \
  openssl x509 -out ds-cert.pem

# 2. Verify you got the right certificate
openssl x509 -in ds-cert.pem -text -noout | head -20

# 3. Import into the client's JVM truststore
keytool -importcert \
  -alias forgerock-ds \
  -file ds-cert.pem \
  -keystore $JAVA_HOME/lib/security/cacerts \
  -storepass changeit \
  -noprompt

# 4. Verify the import
keytool -list -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit -alias forgerock-ds

# 5. Restart the client application (AM, IDM, etc.)

Fix for Docker/Kubernetes Deployments

# Add to AM or IDM Dockerfile
COPY ds-cert.pem /tmp/ds-cert.pem
RUN keytool -importcert \
  -alias forgerock-ds \
  -file /tmp/ds-cert.pem \
  -keystore $JAVA_HOME/lib/security/cacerts \
  -storepass changeit \
  -noprompt && \
  rm /tmp/ds-cert.pem

For Kubernetes with cert-manager:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: ds-cert
  namespace: forgerock
spec:
  secretName: ds-tls
  issuerRef:
    name: ca-issuer
    kind: ClusterIssuer
  dnsNames:
    - ds.forgerock.svc.cluster.local
    - ds-0.ds.forgerock.svc.cluster.local
    - ds-1.ds.forgerock.svc.cluster.local
  duration: 8760h    # 1 year
  renewBefore: 720h  # Renew 30 days before expiry

Cause 2: Missing Intermediate CA Certificate

If DS uses a CA-signed certificate but the intermediate CA is not in the client’s truststore:

Certificate chain
 0 s:CN=ds.example.com
   i:CN=Example Intermediate CA    ← Client needs this CA
                                     but only has Example Root CA

Fix: Import the Intermediate CA

# 1. Extract the intermediate CA from the chain
openssl s_client -connect ds.example.com:1636 -showcerts </dev/null 2>/dev/null | \
  awk '/BEGIN CERTIFICATE/{count++} count==2{print}' > intermediate-ca.pem

# 2. Import it
keytool -importcert \
  -alias intermediate-ca \
  -file intermediate-ca.pem \
  -keystore $JAVA_HOME/lib/security/cacerts \
  -storepass changeit \
  -noprompt

# 3. Restart client

Fix: Configure DS to Send the Full Chain

The better fix is to configure DS to include the intermediate CA in its certificate chain:

# Import the full chain into DS keystore
dskeymgr import-certificate \
  --hostname localhost \
  --port 4444 \
  --bindDN "uid=admin" \
  --bindPassword "password" \
  --certificate-file server-cert.pem \
  --ca-certificate-file intermediate-ca.pem \
  --ca-certificate-file root-ca.pem \
  --alias server-cert \
  --trustAll

Cause 3: Expired Certificate

# Check expiration
openssl s_client -connect ds.example.com:1636 </dev/null 2>/dev/null | \
  openssl x509 -noout -dates

# Output:
# notBefore=Jan  1 00:00:00 2025 GMT
# notAfter=Jan  1 00:00:00 2026 GMT    ← EXPIRED

Fix: Renew the Certificate

# Generate a new CSR using the existing key
dskeymgr create-certificate-request \
  --hostname localhost \
  --port 4444 \
  --bindDN "uid=admin" \
  --bindPassword "password" \
  --alias server-cert \
  --subject-dn "CN=ds.example.com,O=Example,C=US" \
  --output-file ds-csr.pem \
  --trustAll

# After getting the signed certificate from your CA, import it
dskeymgr import-certificate \
  --hostname localhost \
  --port 4444 \
  --bindDN "uid=admin" \
  --bindPassword "password" \
  --certificate-file new-ds-cert.pem \
  --alias server-cert \
  --trustAll

# Restart DS
bin/stop-ds && bin/start-ds

After renewal, update all clients that had the old certificate imported.

Cause 4: DS Replication Certificate Mismatch

When DS instances replicate, each peer must trust the other’s certificate. After certificate renewal or adding a new node:

# On each DS peer, export the certificate
openssl s_client -connect ds-1.example.com:8989 </dev/null 2>/dev/null | \
  openssl x509 -out ds-1-cert.pem

openssl s_client -connect ds-2.example.com:8989 </dev/null 2>/dev/null | \
  openssl x509 -out ds-2-cert.pem

# On ds-1: import ds-2's certificate
dskeymgr import-certificate \
  --hostname localhost \
  --port 4444 \
  --bindDN "uid=admin" \
  --bindPassword "password" \
  --certificate-file ds-2-cert.pem \
  --alias ds-2-replication \
  --trustAll

# On ds-2: import ds-1's certificate
dskeymgr import-certificate \
  --hostname localhost \
  --port 4444 \
  --bindDN "uid=admin" \
  --bindPassword "password" \
  --certificate-file ds-1-cert.pem \
  --alias ds-1-replication \
  --trustAll

Best practice: Use a shared CA certificate instead of self-signed certs. Then each DS instance only needs the CA in its truststore, and certificate renewal doesn’t require updating peers.

Real-World Scenario: DS + RS Co-Located on the Same Host

A common production pattern is running both Directory Server and Replication Server on the same host. When the replication server tries to connect to itself or a peer, you see:

encountered an unexpected error while connecting to replication server
ds-node01.corp.example.com:8989 for domain "cn=schema":
ValidatorException: PKIX path building failed:
sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested target

encountered an unexpected error while connecting to replication server
ds-node01.corp.example.com:8989 for domain "dc=example,dc=com":
ValidatorException: PKIX path building failed: ...

encountered an unexpected error while connecting to replication server
ds-node01.corp.example.com:8989 for domain "ou=tokens":
ValidatorException: PKIX path building failed: ...

Since PKIX is a TLS-level failure, every replication domain reports the same error — cn=schema, user data (dc=...), tokens (ou=tokens), identities, admin data, and so on. The replication server cannot verify the TLS certificate of the peer (or itself) on port 8989, so no domain can establish a connection. When DS and RS share the same JVM, the replication listener uses the same keystore but may present a different certificate alias than the one trusted by the peer.

Root causes for co-located DS+RS:

  1. Self-signed certificate regenerated during upgrade: When you upgrade ForgeRock DS (e.g., from DS 6.5 to DS 7.x, or DS 7.3 to DS 7.5), the upgrade process may automatically regenerate the self-signed certificate in the keystore. This happens because:

    • The new DS version enforces stronger key requirements (e.g., minimum 2048-bit RSA, or SHA-256 signature algorithm instead of SHA-1)
    • The upgrade command detects the old certificate does not meet the new requirements and silently replaces it
    • The new certificate has a different fingerprint and public key than the old one

    The result: all peer DS instances still trust the old certificate. When the upgraded node tries to replicate, peers reject the new certificate because it is not in their truststore. Every replication domain fails simultaneously because TLS handshake happens before any domain-level protocol exchange.

    How to detect this:

    # Compare the certificate fingerprint on the upgraded node vs what peers trust
    # On the UPGRADED node — get the current certificate fingerprint:
    openssl s_client -connect ds-node01.corp.example.com:8989 </dev/null 2>/dev/null | \
      openssl x509 -noout -fingerprint -sha256
    # Output: SHA256 Fingerprint=AA:BB:CC:...  (NEW fingerprint after upgrade)
    
    # On a PEER node — check what certificate it has stored for the upgraded node:
    dskeymgr list-certificates \
      --hostname localhost \
      --port 4444 \
      --bindDN "uid=admin" \
      --bindPassword "password" \
      --trustAll
    # Look for the alias that was imported from ds-node01 — its fingerprint will be DIFFERENT (OLD)
    
    # Also check the DS upgrade log for certificate regeneration:
    grep -i "certificate\|keystore\|regenerat" /path/to/ds/logs/upgrade.log
    

    How to fix — re-exchange certificates after upgrade:

    # Step 1: On the UPGRADED node, export the NEW certificate
    openssl s_client -connect localhost:8989 </dev/null 2>/dev/null | \
      openssl x509 -out /tmp/upgraded-node-new-cert.pem
    
    # Verify it's the new one:
    openssl x509 -in /tmp/upgraded-node-new-cert.pem -noout -fingerprint -sha256 -dates
    
    # Step 2: On EACH PEER, remove the old trusted certificate and import the new one
    dskeymgr delete-certificate \
      --hostname localhost \
      --port 4444 \
      --bindDN "uid=admin" \
      --bindPassword "password" \
      --alias "repl-peer-node01-old" \
      --trustAll
    
    dskeymgr import-certificate \
      --hostname localhost \
      --port 4444 \
      --bindDN "uid=admin" \
      --bindPassword "password" \
      --certificate-file /tmp/upgraded-node-new-cert.pem \
      --alias "repl-peer-node01" \
      --trustAll
    
    # Step 3: Restart the peer DS instances to pick up the new truststore
    bin/stop-ds && bin/start-ds
    
    # Step 4: Verify replication recovers
    dsrepl status \
      --hostname localhost \
      --port 4444 \
      --bindDN "uid=admin" \
      --bindPassword "password" \
      --trustAll
    

    Prevention — upgrade checklist:

    • Before upgrade: Export and save the current certificate fingerprint from all nodes
    • After upgrade on each node: Compare the new fingerprint — if it changed, immediately re-export and distribute to all peers
    • Best long-term fix: Migrate from self-signed to CA-signed certificates (see Option B below). CA-signed certificates survive upgrades because DS trusts the CA, not the individual server certificate
  2. Keystore has multiple certificates and RS picks the wrong one: When the DS keystore contains multiple certificate entries, the replication listener may select a different certificate than what was originally configured. Check which alias the replication listener uses:

# Check which certificate alias the replication server is using
dsconfig get-replication-server-prop \
  --hostname localhost \
  --port 4444 \
  --bindDN "uid=admin" \
  --bindPassword "password" \
  --provider-name "Multimaster Synchronization" \
  --property ssl-cert-nickname \
  --trustAll
  1. Admin connector certificate vs replication certificate: The admin connector (port 4444) and the replication server (port 8989) may use different certificate aliases. If you renewed one but not the other:
# Check admin connector certificate
dsconfig get-administration-connector-prop \
  --hostname localhost \
  --port 4444 \
  --bindDN "uid=admin" \
  --bindPassword "password" \
  --property ssl-cert-nickname \
  --trustAll

# Both should use the same alias, or both certificates must be in each peer's truststore

Diagnosis for co-located DS+RS:

# Step 1: Check what certificate the replication port is presenting
openssl s_client -connect ds-node01.corp.example.com:8989 </dev/null 2>/dev/null | \
  openssl x509 -noout -subject -issuer -dates -fingerprint -sha256

# Step 2: Compare with what the LDAPS port presents
openssl s_client -connect ds-node01.corp.example.com:1636 </dev/null 2>/dev/null | \
  openssl x509 -noout -subject -issuer -dates -fingerprint -sha256

# If the fingerprints differ, the replication and LDAP listeners use different certificates

# Step 3: List all certificates in the DS keystore
dskeymgr list-certificates \
  --hostname localhost \
  --port 4444 \
  --bindDN "uid=admin" \
  --bindPassword "password" \
  --trustAll

# Step 4: Check which peers are configured for replication
dsrepl status \
  --hostname localhost \
  --port 4444 \
  --bindDN "uid=admin" \
  --bindPassword "password" \
  --trustAll

Fix for co-located DS+RS:

# Option A: Re-export and exchange certificates between all peers
# On EACH DS node, export the replication certificate:
openssl s_client -connect localhost:8989 </dev/null 2>/dev/null | \
  openssl x509 -out /tmp/local-repl-cert.pem

# On EACH peer, import the other peers' certificates:
dskeymgr import-certificate \
  --hostname localhost \
  --port 4444 \
  --bindDN "uid=admin" \
  --bindPassword "password" \
  --certificate-file /tmp/peer-repl-cert.pem \
  --alias "repl-peer-node02" \
  --trustAll

# Option B (recommended): Replace self-signed with CA-signed certificate
# This permanently fixes the trust issue across all peers
dskeymgr create-certificate-request \
  --hostname localhost \
  --port 4444 \
  --bindDN "uid=admin" \
  --bindPassword "password" \
  --alias server-cert \
  --subject-dn "CN=ds-node01.corp.example.com,O=Corp,C=US" \
  --subject-alternative-name "DNS:ds-node01.corp.example.com" \
  --output-file ds-node01.csr \
  --trustAll

# After CA signs it, import with full chain:
dskeymgr import-certificate \
  --hostname localhost \
  --port 4444 \
  --bindDN "uid=admin" \
  --bindPassword "password" \
  --certificate-file signed-cert.pem \
  --ca-certificate-file ca-chain.pem \
  --alias server-cert \
  --trustAll

# Restart DS — replication will automatically recover
bin/stop-ds && bin/start-ds

All replication domains are affected: The PKIX error is a TLS-level failure — it happens before any LDAP/replication protocol exchange. This means every replication domain will fail with the same error: cn=schema, dc=example,dc=com (user data), cn=tokens (CTS), cn=admin data, and any other configured domains. You will typically see multiple log entries like:

connecting to replication server ds-node01.corp.example.com:8989 for domain "cn=schema": PKIX path building failed...
connecting to replication server ds-node01.corp.example.com:8989 for domain "dc=example,dc=com": PKIX path building failed...
connecting to replication server ds-node01.corp.example.com:8989 for domain "ou=tokens": PKIX path building failed...
connecting to replication server ds-node01.corp.example.com:8989 for domain "ou=identities": PKIX path building failed...

The cn=schema error is often noticed first because DS initializes schema replication before data domains, but the root cause is the same across all domains — the TLS certificate trust is broken at the transport layer. Fixing the certificate once resolves all domains simultaneously.

Cause 5: Wrong Truststore Configured

ForgeRock AM and IDM can use a custom truststore instead of the JVM default:

AM Truststore Configuration

Check $AM_HOME/config/boot.json for custom truststore settings:

{
  "stores": {
    "trustStore": {
      "location": "/path/to/am-truststore.jks",
      "password": "changeit"
    }
  }
}

If a custom truststore is configured, import the DS certificate there — not into the JVM’s cacerts:

keytool -importcert \
  -alias forgerock-ds \
  -file ds-cert.pem \
  -keystore /path/to/am-truststore.jks \
  -storepass changeit \
  -noprompt

IDM Truststore Configuration

Check $IDM_HOME/conf/system.properties:

javax.net.ssl.trustStore=/path/to/idm-truststore.jks
javax.net.ssl.trustStorePassword=changeit

JVM System Properties

For any Java client, you can set the truststore via JVM arguments:

# Start AM/IDM with explicit truststore
java -Djavax.net.ssl.trustStore=/path/to/truststore.jks \
     -Djavax.net.ssl.trustStorePassword=changeit \
     -jar application.jar

# Enable TLS debugging to see exactly what's happening
java -Djavax.net.ssl.debug=ssl,handshake \
     -jar application.jar

If the certificate’s CN or SAN doesn’t match the hostname used in the connection:

java.security.cert.CertificateException: No subject alternative names matching
IP address 10.0.0.5 found

Fix: Generate Certificate with Correct SANs

dskeymgr create-certificate-request \
  --hostname localhost \
  --port 4444 \
  --bindDN "uid=admin" \
  --bindPassword "password" \
  --alias server-cert \
  --subject-dn "CN=ds.example.com,O=Example,C=US" \
  --subject-alternative-name "DNS:ds.example.com" \
  --subject-alternative-name "DNS:ds-0.ds.forgerock.svc.cluster.local" \
  --subject-alternative-name "DNS:localhost" \
  --subject-alternative-name "IP:10.0.0.5" \
  --output-file ds-csr.pem \
  --trustAll

Automated Certificate Health Check

This script checks all common certificate issues at once:

#!/bin/bash
# ForgeRock DS Certificate Health Check
# Usage: ./cert-health-check.sh <ds-host> <ldaps-port> [admin-port]

DS_HOST="${1:?Usage: cert-health-check.sh <ds-host> <ldaps-port> [admin-port]}"
DS_PORT="${2:?Usage: cert-health-check.sh <ds-host> <ldaps-port>}"
ADMIN_PORT="${3:-4444}"
ERRORS=0

echo "========================================"
echo "ForgeRock DS Certificate Health Check"
echo "Target: ${DS_HOST}:${DS_PORT}"
echo "========================================"

# Check 1: Can we connect?
echo -e "\n--- Check 1: TLS Connection ---"
CONNECT_OUTPUT=$(openssl s_client -connect "${DS_HOST}:${DS_PORT}" </dev/null 2>&1)
if echo "$CONNECT_OUTPUT" | grep -q "CONNECTED"; then
  echo "PASS: TLS connection established"
else
  echo "FAIL: Cannot establish TLS connection"
  ((ERRORS++))
fi

# Check 2: Certificate details
echo -e "\n--- Check 2: Certificate Details ---"
CERT_TEXT=$(echo "$CONNECT_OUTPUT" | openssl x509 -text -noout 2>/dev/null)
if [ -n "$CERT_TEXT" ]; then
  SUBJECT=$(echo "$CERT_TEXT" | grep "Subject:" | head -1)
  ISSUER=$(echo "$CERT_TEXT" | grep "Issuer:" | head -1)
  echo "Subject: ${SUBJECT}"
  echo "Issuer:  ${ISSUER}"

  # Self-signed check
  if [ "$SUBJECT" = "$ISSUER" ]; then
    echo "WARNING: Certificate is self-signed"
  fi
else
  echo "FAIL: Cannot read certificate"
  ((ERRORS++))
fi

# Check 3: Expiration
echo -e "\n--- Check 3: Expiration ---"
DATES=$(echo "$CONNECT_OUTPUT" | openssl x509 -noout -dates 2>/dev/null)
if [ -n "$DATES" ]; then
  echo "$DATES"
  NOT_AFTER=$(echo "$DATES" | grep "notAfter" | cut -d= -f2)
  EXPIRY_EPOCH=$(date -j -f "%b %d %H:%M:%S %Y %Z" "$NOT_AFTER" +%s 2>/dev/null || \
                 date -d "$NOT_AFTER" +%s 2>/dev/null)
  NOW_EPOCH=$(date +%s)
  DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))

  if [ "$DAYS_LEFT" -lt 0 ]; then
    echo "FAIL: Certificate EXPIRED ${DAYS_LEFT#-} days ago"
    ((ERRORS++))
  elif [ "$DAYS_LEFT" -lt 30 ]; then
    echo "WARNING: Certificate expires in ${DAYS_LEFT} days"
  else
    echo "PASS: Certificate valid for ${DAYS_LEFT} days"
  fi
fi

# Check 4: Chain completeness
echo -e "\n--- Check 4: Certificate Chain ---"
CHAIN_COUNT=$(echo "$CONNECT_OUTPUT" | grep -c "BEGIN CERTIFICATE")
echo "Certificates in chain: ${CHAIN_COUNT}"
if [ "$CHAIN_COUNT" -lt 2 ]; then
  echo "WARNING: Chain may be incomplete (no intermediate CA sent)"
fi

VERIFY_RESULT=$(echo "$CONNECT_OUTPUT" | grep "Verify return code")
echo "$VERIFY_RESULT"
if echo "$VERIFY_RESULT" | grep -q "0 (ok)"; then
  echo "PASS: Chain verification successful"
else
  echo "FAIL: Chain verification failed"
  ((ERRORS++))
fi

# Check 5: SANs
echo -e "\n--- Check 5: Subject Alternative Names ---"
SANS=$(echo "$CERT_TEXT" | grep -A1 "Subject Alternative Name" | tail -1)
if [ -n "$SANS" ]; then
  echo "$SANS"
else
  echo "WARNING: No SANs found (only CN-based matching)"
fi

# Check 6: JVM truststore
echo -e "\n--- Check 6: JVM Truststore ---"
if [ -n "$JAVA_HOME" ]; then
  TRUSTSTORE="$JAVA_HOME/lib/security/cacerts"
  if [ -f "$TRUSTSTORE" ]; then
    echo "Truststore: ${TRUSTSTORE}"
    DS_IN_TRUST=$(keytool -list -keystore "$TRUSTSTORE" -storepass changeit 2>/dev/null | grep -ci "forgerock\|ds\.")
    echo "ForgeRock-related entries: ${DS_IN_TRUST}"
  else
    echo "WARNING: Default truststore not found at ${TRUSTSTORE}"
  fi
else
  echo "WARNING: JAVA_HOME not set"
fi

# Summary
echo -e "\n========================================"
echo "Summary: ${ERRORS} error(s) found"
if [ "$ERRORS" -gt 0 ]; then
  echo "STATUS: NEEDS ATTENTION"
else
  echo "STATUS: HEALTHY"
fi
echo "========================================"

Prevention: Certificate Management Best Practices

1. Use CA-Signed Certificates in Production

Self-signed certificates cause maintenance headaches at scale. Use an internal CA (or cert-manager in Kubernetes) for all DS instances:

# Generate DS key and CSR
dskeymgr create-certificate-request \
  --hostname localhost \
  --port 4444 \
  --bindDN "uid=admin" \
  --bindPassword "password" \
  --alias server-cert \
  --subject-dn "CN=ds.example.com,O=Example,C=US" \
  --subject-alternative-name "DNS:ds.example.com" \
  --subject-alternative-name "DNS:*.ds.forgerock.svc.cluster.local" \
  --key-algorithm RSA \
  --key-size 2048 \
  --output-file ds-csr.pem \
  --trustAll

# Sign with your CA, then import the signed certificate + chain
dskeymgr import-certificate \
  --hostname localhost \
  --port 4444 \
  --bindDN "uid=admin" \
  --bindPassword "password" \
  --certificate-file signed-ds-cert.pem \
  --ca-certificate-file intermediate-ca.pem \
  --ca-certificate-file root-ca.pem \
  --alias server-cert \
  --trustAll

2. Monitor Certificate Expiration

Add certificate expiration to your monitoring system:

# Prometheus-compatible check (returns days until expiry)
EXPIRY=$(openssl s_client -connect ds.example.com:1636 </dev/null 2>/dev/null | \
  openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
DAYS=$(( ($(date -d "$EXPIRY" +%s) - $(date +%s)) / 86400 ))
echo "forgerock_ds_cert_days_remaining{host=\"ds.example.com\"} $DAYS"

3. Automate Certificate Rotation

For Kubernetes deployments with cert-manager:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: ds-server-cert
spec:
  secretName: ds-tls-secret
  duration: 8760h       # 1 year
  renewBefore: 720h     # Auto-renew 30 days before expiry
  issuerRef:
    name: internal-ca
    kind: ClusterIssuer
  commonName: ds.forgerock.svc.cluster.local
  dnsNames:
    - ds.forgerock.svc.cluster.local
    - "*.ds.forgerock.svc.cluster.local"

Quick Reference

TaskCommand
Check DS certificateopenssl s_client -connect host:1636 </dev/null 2>/dev/null | openssl x509 -text -noout
Check expirationopenssl s_client -connect host:1636 </dev/null 2>/dev/null | openssl x509 -noout -dates
List DS keystoredskeymgr list-certificates --hostname host --port 4444 --bindDN uid=admin --bindPassword pass --trustAll
Export DS certopenssl s_client -connect host:1636 </dev/null 2>/dev/null | openssl x509 -out ds.pem
Import to JVMkeytool -importcert -alias ds -file ds.pem -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit -noprompt
List JVM trustkeytool -list -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit
Debug TLSjava -Djavax.net.ssl.debug=ssl,handshake -jar app.jar
Generate CSRdskeymgr create-certificate-request --alias server-cert --subject-dn "CN=ds.example.com" --output-file ds.csr