You ran keytool -importcert into /usr/lib/jvm/java-25-openjdk-amd64/lib/security/cacerts, restarted the service, and the app still throws PKIX path building failed. That pattern is common on servers where the product ships its own Java runtime. Jira, Confluence, Bitbucket, Boomi, Informatica, and many IBM-style middleware installs ignore the system OpenJDK cacerts bundle entirely.
This guide walks through a lab with two separate truststore files — system JDK versus bundled app JRE — imports a private CA into the wrong file, proves the app still fails, then imports the correct store and gets HTTP status: 200. For finding which file a running JVM opens, start with Find the correct Java cacerts file.
Tested on: Ubuntu 26.04 LTS; OpenJDK 25.0.3; kernel 7.0.0-27-generic.
Prerequisites
- OpenJDK with
keytoolandjava— Install keytool on Ubuntu — see keytool command. opensslfor the lab HTTPS server (OpenSSL).
Why system cacerts is the wrong target
| What admins often edit | What the app actually loads |
|---|---|
/usr/lib/jvm/default-java/lib/security/cacerts |
/opt/atlassian/jira/jre/lib/security/cacerts |
/etc/ssl/certs/java/cacerts (Ubuntu symlink) |
$INSTALL_DIR/java/lib/security/cacerts inside the product |
keytool -cacerts from your SSH session |
-Djavax.net.ssl.trustStore=/opt/app/trust.p12 from setenv.sh |
Atlassian documents that Java trusts outbound HTTPS through the JVM truststore at $JAVA_HOME/lib/security/cacerts (or jre/lib/security/cacerts on older layouts), and that a custom -Djavax.net.ssl.trustStore replaces that default. The same pattern appears when integration platforms connect to internal APIs over TLS: the fix is almost always importing into the JDK that runs the product, not the one behind your login shell.
Common user questions this page answers:
- Certificate is in
cacertsbut PKIX still fails — wrong Java home. - Which
keytooland which-keystorepath for Jira or Confluence? - Do I need root and intermediate CA certificates?
- Does
apt upgrade openjdk-*affect a bundled app truststore?
Find the app's Java home before you import
Do not guess from echo $JAVA_HOME in your SSH session. Use the running process — replace the grep command pattern with your product name:
Find the blocking PID with ps aux piped to grep; the ps command explains BSD versus UNIX options and filtering command lines.
ps aux | grep '[j]ira\|[c]atalina\|[b]oomi' | head -3
PID=<app-java-pid>
readlink -f /proc/$PID/exe
APP_JAVA_HOME=$(dirname "$(dirname "$(readlink -f /proc/$PID/exe)")")
ls -la "$APP_JAVA_HOME/lib/security/cacerts"
readlink -f "$APP_JAVA_HOME/lib/security/cacerts"
ls -l "$APP_JAVA_HOME/lib/security/jssecacerts" "$APP_JAVA_HOME/lib/security/cacerts" 2>/dev/nulljira 28451 ... /opt/atlassian/jira/jre/bin/java ...
/opt/atlassian/jira/jre/bin/java
lrwxrwxrwx 1 jira jira 48 Jan 15 10:00 /opt/atlassian/jira/jre/lib/security/cacerts -> /opt/atlassian/jira/jre/lib/security/cacerts
/opt/atlassian/jira/jre/lib/security/cacerts
-rw-r--r-- 1 jira jira 123456 Jan 15 10:00 /opt/atlassian/jira/jre/lib/security/cacertsThe path under /opt/atlassian/jira/jre/ is the file you must import into — not /usr/lib/jvm/default-java/lib/security/cacerts from your SSH session.
When javax.net.ssl.trustStore is not set, JSSE checks trust files in this order: jssecacerts (if it exists), then cacerts. If jssecacerts is present, verify whether the application loads that file before you import only into cacerts.
Check startup scripts for overrides — a custom trustStore replaces both default files:
grep -r 'javax.net.ssl.trustStore' /opt/atlassian/ /etc/default/ 2>/dev/null | head -5/opt/atlassian/jira/bin/setenv.sh:export CATALINA_OPTS="$CATALINA_OPTS -Djavax.net.ssl.trustStore=/var/atlassian/trust.p12"When that property is set, import into the path it names. When it is absent, import into $APP_JAVA_HOME/lib/security/jssecacerts if that file exists; otherwise use $APP_JAVA_HOME/lib/security/cacerts.
If jcmd is available, confirm what the running JVM uses:
jcmd "$PID" VM.command_line
jcmd "$PID" VM.system_properties | grep -E 'java.home|javax.net.ssl.trustStore|javax.net.ssl.trustStoreType'java.home=/opt/atlassian/jira/jre
javax.net.ssl.trustStore=/var/atlassian/trust.p12
javax.net.ssl.trustStoreType=PKCS12When javax.net.ssl.trustStore appears in jcmd output, edit that file — importing into cacerts under java.home has no effect on the running JVM.
Run jcmd as the same user as the Java process, or with sufficient privileges; otherwise it may not attach.
On Tomcat-based products, the binary is often /usr/lib/jvm/default-java/bin/java while java.home resolves to a specific OpenJDK build — see the Tomcat example in Find the correct Java cacerts file. What matters is the truststore file that JVM opens, not the env var alone.
Typical bundled-app truststore paths
Paths vary by vendor and version — always confirm with readlink -f on your server:
| Product style | Where to look |
|---|---|
| Atlassian Data Center / Server | <install>/jre/lib/security/cacerts or bundled JDK under the product directory |
| Tomcat service (distro package) | java.home from /proc/$PID/exe, often shared with system OpenJDK on Ubuntu |
| Boomi / Atom | JRE inside the Atom installation directory; check atom.vmoptions or service wrapper |
| Informatica | $INFA_HOME/java/jre/lib/security/cacerts (confirm in node configuration) |
| Tarball Elasticsearch / Logstash | jdk/lib/security/cacerts inside the distribution |
Search when documentation is unclear:
find /opt/myproduct -path '*/lib/security/*' \( -name cacerts -o -name jssecacerts \) 2>/dev/nullLab: wrong truststore import, then the correct one
This lab simulates a system JDK truststore copy and a bundled app with its own cacerts file. A private CA signs a local HTTPS server with OpenSSL (create a root CA on Linux walks through the same signing steps); a small Java client shows PKIX failure until you import into the store the app actually loads.
Create the lab layout and copy two independent cacerts files:
LAB=~/app-trust-lab
mkdir -p "$LAB/bundled-jre/lib/security" "$LAB/certs"
cd "$LAB"
openssl genrsa -out certs/lab-ca.key 4096 2>/dev/null
openssl req -new -x509 -days 365 -key certs/lab-ca.key -out certs/lab-ca.crt \
-subj "/CN=App Lab Internal CA/O=App Lab/C=US" 2>/dev/null
openssl genrsa -out certs/server.key 2048 2>/dev/null
openssl req -new -key certs/server.key -out certs/server.csr \
-subj "/CN=internal-api.lab.local/O=App Lab/C=US" 2>/dev/null
cat > certs/server.ext << 'EOF'
subjectAltName = IP:127.0.0.1,DNS:internal-api.lab.local
extendedKeyUsage = serverAuth
EOF
openssl x509 -req -in certs/server.csr -CA certs/lab-ca.crt -CAkey certs/lab-ca.key \
-CAcreateserial -out certs/server.crt -days 365 -extfile certs/server.ext 2>/dev/null
openssl s_server -accept 8443 -cert certs/server.crt -key certs/server.key -www \
</dev/null >/dev/null 2>&1 &
SERVER_PID=$!
sleep 1
SYS_CACERTS=$(readlink -f "$(java -XshowSettings:properties -version 2>&1 | awk -F' = ' '/java.home/{print $2; exit}')/lib/security/cacerts")
cp "$SYS_CACERTS" "$LAB/system-cacerts"
cp "$SYS_CACERTS" "$LAB/bundled-jre/lib/security/cacerts"Both copies start with 121 entries:
keytool -list -keystore "$LAB/system-cacerts" -storepass changeit | grep 'contains'
keytool -list -keystore "$LAB/bundled-jre/lib/security/cacerts" -storepass changeit | grep 'contains'Your keystore contains 121 entries
Your keystore contains 121 entriesImport the lab CA into the system copy only — simulating the mistake of editing the wrong JDK:
keytool -importcert -alias app-lab-ca -file certs/lab-ca.crt \
-keystore "$LAB/system-cacerts" -storepass changeit -noprompt
keytool -list -alias app-lab-ca -keystore "$LAB/system-cacerts" -storepass changeit | head -2
keytool -list -alias app-lab-ca -keystore "$LAB/bundled-jre/lib/security/cacerts" -storepass changeitCertificate was added to keystore
app-lab-ca, Jul 3, 2026, trustedCertEntry,
Certificate fingerprint (SHA-256): 02:34:1F:C8:AB:30:53:00:90:91:37:DD:65:8E:C1:46:F3:82:5A:2E:87:7F:0B:37:EA:FF:84:87:76:C0:64:0B
keytool error: java.lang.Exception: Alias <app-lab-ca> does not existThe alias exists in the system file only. The bundled app file is unchanged.
Compile a TLS client that connects to https://127.0.0.1:8443/ — the server certificate includes IP:127.0.0.1 in the SAN extension, so no hostname verifier override is needed:
cat > TlsClient.java << 'EOF'
import javax.net.ssl.HttpsURLConnection;
import java.net.URL;
public class TlsClient {
public static void main(String[] args) throws Exception {
try {
HttpsURLConnection c = (HttpsURLConnection) new URL("https://127.0.0.1:8443/").openConnection();
c.setConnectTimeout(5000);
c.setReadTimeout(5000);
c.connect();
System.out.println("HTTP status: " + c.getResponseCode());
} catch (Exception e) {
String msg = e.getMessage();
if (msg != null && msg.contains("\n")) msg = msg.split("\n")[0];
System.out.println("FAILED: " + e.getClass().getSimpleName() + ": " + msg);
}
}
}
EOF
javac TlsClient.javaRun as the bundled app would — pointing at its private cacerts without the CA import:
java -Djavax.net.ssl.trustStore="$LAB/bundled-jre/lib/security/cacerts" \
-Djavax.net.ssl.trustStorePassword=changeit TlsClientFAILED: SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested targetThe system copy with the import succeeds — proving you edited a different file than the app uses:
java -Djavax.net.ssl.trustStore="$LAB/system-cacerts" \
-Djavax.net.ssl.trustStorePassword=changeit TlsClientHTTP status: 200Import into the bundled app truststore and retry:
keytool -importcert -alias app-lab-ca -file certs/lab-ca.crt \
-keystore "$LAB/bundled-jre/lib/security/cacerts" -storepass changeit -noprompt
keytool -list -keystore "$LAB/bundled-jre/lib/security/cacerts" -storepass changeit | grep 'contains'
java -Djavax.net.ssl.trustStore="$LAB/bundled-jre/lib/security/cacerts" \
-Djavax.net.ssl.trustStorePassword=changeit TlsClientCertificate was added to keystore
Your keystore contains 122 entries
HTTP status: 200Clean up:
kill $SERVER_PID 2>/dev/null
rm -rf ~/app-trust-labImport into the production app truststore
Once you know the absolute path, import with explicit -keystore — do not rely on keytool -cacerts from your shell:
APP_CACERTS=/opt/atlassian/jira/jre/lib/security/cacerts
sudo cp "$APP_CACERTS" "${APP_CACERTS}.bak.$(date +%F)"
sudo keytool -importcert -alias corp-internal-ca -file /tmp/corp-ca.crt \
-keystore "$APP_CACERTS" -storepass changeit -nopromptIf the alias already exists from a previous import or CA renewal, inspect it before replacing:
sudo keytool -list -alias corp-internal-ca -keystore "$APP_CACERTS" -storepass changeit -vDelete and re-import only when you are sure it is the old CA:
sudo keytool -delete -alias corp-internal-ca -keystore "$APP_CACERTS" -storepass changeit
sudo keytool -importcert -alias corp-internal-ca -file /tmp/corp-ca.crt \
-keystore "$APP_CACERTS" -storepass changeit -nopromptVerify before restart:
keytool -list -alias corp-internal-ca -keystore "$APP_CACERTS" -storepass changeitcorp-internal-ca, <date>, trustedCertEntry,
Certificate fingerprint (SHA-256): ...Restart the application service so the JVM reloads the file. On systemd:
Control the running service with systemctl start, stop, or restart; see the systemctl command for try-restart and dependency behavior.
sudo systemctl restart jira.servicecacerts before the first edit. On production Atlassian nodes, schedule the same import on every cluster member or copy the updated file consistently — Atlassian notes that mixed truststores across nodes cause confusing PKIX errors.
When the app uses a custom PKCS12 truststore instead of cacerts, import there and confirm javax.net.ssl.trustStoreType matches the file format. Details are in Custom truststore vs cacerts and Java truststore in Docker and Kubernetes.
keytool from the wrong JDK
Your shell keytool may target a different JDK than the application:
readlink -f "$(command -v keytool)"/usr/lib/jvm/java-25-openjdk-amd64/bin/keytoolThat binary is fine as long as you pass the app's -keystore path explicitly. On some systems, bundled-jre/bin/keytool may resolve to the same executable as /usr/bin/keytool; on others, it may be a separate bundled binary. Either way, the important part is the explicit -keystore path. The failure mode is running keytool -importcert -cacerts without thinking — that edits the JDK behind your login PATH, not the bundled product JRE.
-Djava.home at a folder that contains only lib/security/cacerts. A partial tree without lib/modules and the rest of the JRE layout can fail at startup with NoSuchFileException: .../lib/modules. Import into the cacerts file with an absolute -keystore path, or set -Djavax.net.ssl.trustStore to that file. Symlinking only bundled-jre/bin/java to the system binary does not retarget java.home — the JVM still loads trust from the runtime's own java.home until you update the bundled cacerts or set trustStore explicitly.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Imported into cacerts, but Java still ignores it |
jssecacerts exists and is checked before cacerts |
Check $APP_JAVA_HOME/lib/security/jssecacerts and import there if that is the active JSSE truststore |
keytool shows alias; app still fails |
Service not restarted | Restart app or container after import |
Import into file A; jcmd shows file B |
Wrong PID or node in a cluster | Attach to the failing node's JVM |
| Custom truststore path correct; still fails | Wrong trustStoreType or password |
Match javax.net.ssl.trustStoreType; keytool -list with same type |
Set -Djava.home to product folder; JVM won't start |
Incomplete JRE tree (only lib/security) |
Use -Djavax.net.ssl.trustStore=<absolute-cacerts-path> instead |
Keystore was tampered with on app cacerts |
Vendor changed default password | See fix keystore tampered or password incorrect |
For the PKIX error text and chain-import steps, see Fix Java PKIX path building failed.
References
- Java Secure Socket Extension JSSE Reference Guide — default truststore lookup order:
javax.net.ssl.trustStore,jssecacerts, thencacerts - How to import a public SSL certificate into a JVM — Atlassian guidance on
$JAVA_HOME/lib/security/cacertsand custom truststores - Unable to connect to SSL services due to PKIX path building failed — wrong truststore and multi-node consistency
- keytool documentation —
-importcertand-keystore
Summary
Enterprise Java apps often load a private cacerts under their bundled JRE, not the system OpenJDK you edit from SSH. Find the path from /proc/$PID/exe, startup scripts, or javax.net.ssl.trustStore, import with keytool -importcert -keystore <absolute-path>, verify the alias, restart the service, and confirm with TLS debug or a test client. Importing into the wrong file leaves PKIX unchanged — the lab above shows HTTP status: 200 only after the bundled truststore receives the CA.

