How to Import Certificates into App-Specific Java Truststores

Fix PKIX after importing into the wrong cacerts. Find the Java truststore your app actually uses, including bundled JRE cacerts, and import with keytool.

Published

Updated

Read time 10 min read

Reviewed byDeepak Prasad

Import certificate into app-specific Java truststore banner with bundled JRE and system JDK paths

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


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 cacerts but PKIX still fails — wrong Java home.
  • Which keytool and which -keystore path 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.

bash
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/null
text
jira  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/cacerts

The 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:

bash
grep -r 'javax.net.ssl.trustStore' /opt/atlassian/ /etc/default/ 2>/dev/null | head -5
text
/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:

bash
jcmd "$PID" VM.command_line
jcmd "$PID" VM.system_properties | grep -E 'java.home|javax.net.ssl.trustStore|javax.net.ssl.trustStoreType'
text
java.home=/opt/atlassian/jira/jre
javax.net.ssl.trustStore=/var/atlassian/trust.p12
javax.net.ssl.trustStoreType=PKCS12

When 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:

bash
find /opt/myproduct -path '*/lib/security/*' \( -name cacerts -o -name jssecacerts \) 2>/dev/null

Lab: 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:

bash
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:

bash
keytool -list -keystore "$LAB/system-cacerts" -storepass changeit | grep 'contains'
keytool -list -keystore "$LAB/bundled-jre/lib/security/cacerts" -storepass changeit | grep 'contains'
text
Your keystore contains 121 entries
Your keystore contains 121 entries

Import the lab CA into the system copy only — simulating the mistake of editing the wrong JDK:

bash
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 changeit
text
Certificate 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 exist

The 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:

bash
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.java

Run as the bundled app would — pointing at its private cacerts without the CA import:

bash
java -Djavax.net.ssl.trustStore="$LAB/bundled-jre/lib/security/cacerts" \
     -Djavax.net.ssl.trustStorePassword=changeit TlsClient
text
FAILED: SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

The system copy with the import succeeds — proving you edited a different file than the app uses:

bash
java -Djavax.net.ssl.trustStore="$LAB/system-cacerts" \
     -Djavax.net.ssl.trustStorePassword=changeit TlsClient
text
HTTP status: 200

Import into the bundled app truststore and retry:

bash
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 TlsClient
text
Certificate was added to keystore
Your keystore contains 122 entries
HTTP status: 200

Clean up:

bash
kill $SERVER_PID 2>/dev/null
rm -rf ~/app-trust-lab

Import into the production app truststore

Once you know the absolute path, import with explicit -keystore — do not rely on keytool -cacerts from your shell:

bash
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 -noprompt

If the alias already exists from a previous import or CA renewal, inspect it before replacing:

bash
sudo keytool -list -alias corp-internal-ca -keystore "$APP_CACERTS" -storepass changeit -v

Delete and re-import only when you are sure it is the old CA:

bash
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 -noprompt

Verify before restart:

bash
keytool -list -alias corp-internal-ca -keystore "$APP_CACERTS" -storepass changeit
text
corp-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.

bash
sudo systemctl restart jira.service
NOTE
Back up cacerts 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:

bash
readlink -f "$(command -v keytool)"
text
/usr/lib/jvm/java-25-openjdk-amd64/bin/keytool

That 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.

NOTE
Do not point -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


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.


Frequently Asked Questions

1. Why does PKIX fail after I imported the certificate into system cacerts?

The application likely runs on a bundled JRE or a different JDK than the one whose cacerts you edited. Import into the file from javax.net.ssl.trustStore if it is set. Otherwise check java.home/lib/security/jssecacerts first, and use java.home/lib/security/cacerts when jssecacerts is absent.

2. Which keytool should I use for a bundled app?

Use keytool from the same JDK that runs the app when possible, but the critical part is the -keystore path, not which keytool binary you invoke. Always pass the absolute path to the app bundled cacerts file. Shell keytool that edits /usr/lib/jvm/default-java/lib/security/cacerts does not help an app that loads /opt/atlassian/jira/jre/lib/security/cacerts.

3. Do I need to restart the application after importing?

Yes. The JVM loads the truststore at startup. keytool changes on disk are invisible to a running process until you restart the service or container.

4. Should I import the leaf certificate or the CA?

Import the signing CA (root or intermediate) as a trustedCertEntry unless you have a specific reason to trust only one leaf. For private PKI, your PKI team usually provides ca.crt or intermediate-ca.crt PEM files.

5. What if the app sets javax.net.ssl.trustStore?

Import into that exact file, not into cacerts under java.home. When trustStore is set, the JVM does not merge with default cacerts. See java-custom-truststore-vs-cacerts for building a composite truststore that keeps public CAs plus your private CA.

6. What is the default cacerts password for bundled OpenJDK?

changeit on stock OpenJDK bundles unless the vendor changed it during installation. If keytool reports Keystore was tampered with or password was incorrect, ask the app vendor or check the product SSL documentation before guessing.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …