From a PEM to a Running Stack — Part 2: The Keystore Java Refused to Read
Part 2 of the series. In Part 1 we sorted out the certificate formats. Now we build a PKCS#12 truststore from them — and run into two hurdles that strike even when the file is technically correct.
Keycloak (a Java-based authentication server) wanted a PKCS#12 truststore: a file holding the public CA certificate the service should trust. A truststore deliberately contains no private key — only the public certificate of a CA. With it you tell the service: "Anything signed by this CA, you may trust."
My clean CA certificate was ready. The obvious command:
openssl pkcs12 -export -nokeys -in ca.crt -out root.p12 -passout pass:<TRUSTSTORE_PW>
-nokeys, because a truststore doesn't need a key. It should work. And it did — seemingly.
The file was created, had the right password, and could be opened with OpenSSL. But Java reported the truststore was empty. The proof, with Java's own tool keytool:
keytool -list -keystore root.p12 -storetype PKCS12 -storepass <TRUSTSTORE_PW> # -> Your keystore contains 0 entries
EVIDENCE: zero entries — even though OpenSSL didn't complain when creating the file and dutifully showed a Certificate bag when inspecting it.
The cause is subtle: when you package a bare certificate with no associated private key using openssl pkcs12 -export -nokeys, it lands in the container as a so-called bag without an alias (also called "friendlyName"). And Java/keytool simply doesn't list entries without an alias as usable entries. From Java's point of view: empty.
The fix is to build the truststore with Java's own tool, because keytool -importcert is made for exactly this — importing a trusted certificate with an alias:
keytool -importcert -noprompt \ -alias corp-ca \ -file ca.crt \ -keystore root.p12 \ -storetype PKCS12 \ -storepass <TRUSTSTORE_PW>
Now keytool -list dutifully shows 1 entry with trustedCertEntry. Java sees the certificate.
VERDICT: for Java truststores, keytool is the right choice, not the OpenSSL export. What OpenSSL considers valid is by no means guaranteed to be usable in the Java world.
No sooner had the empty keystore been solved than the next classic arrived:
ERROR: keystore password was incorrect javax.crypto.BadPaddingException: Given final block not properly padded
The treacherous part: I could open the very same file with OpenSSL and exactly this password without a problem:
openssl pkcs12 -info -in root.p12 -passin pass:<TRUSTSTORE_PW> -noout # runs through cleanly
EVIDENCE: when OpenSSL opens the file with the password but Java says "wrong password," the problem is not the password — it's the encryption algorithm.
The background: OpenSSL 3.x packages PKCS#12 files by default with modern PBES2 / AES-256 / SHA-256. Older Java versions (like the JDK 17 in many container images) don't understand this algorithm when decrypting — and misleadingly report it as "password was incorrect," even though the password is perfectly correct.
You recognize the modern format from the OpenSSL output:
openssl pkcs12 -info -in root.p12 -passin pass:<TRUSTSTORE_PW> -noout # PKCS7 Encrypted data: PBES2, PBKDF2, AES-256-CBC ... <- Java doesn't like this # MAC: sha256
Java, by contrast, wants the older legacy format:
PKCS7 Encrypted data: pbeWithSHA1And3-KeyTripleDES-CBC ... <- Java understands this MAC: sha1
VERDICT: "password was incorrect" from Java, when OpenSSL opens the file fine, almost always means the wrong PKCS#12 encryption algorithm — not the wrong password.
The catch-22 — and its resolution
So I was stuck in a trap that ran through the entire project:
- OpenSSL export in legacy format → Java-readable, but an empty keystore (no alias).
- keytool import → a filled keystore with an alias, but the modern AES format that Java rejects.
I needed both at once: an entry with an alias and the legacy format.
The solution was to use keytool for building (that solves the alias problem) and simultaneously force it into the legacy format via JDK system properties:
keytool -importcert -noprompt -alias corp-ca \ -file ca.crt \ -keystore root.p12 -storetype PKCS12 -storepass <TRUSTSTORE_PW> \ -J-Dkeystore.pkcs12.certProtectionAlgorithm=PBEWithSHA1AndDESede \ -J-Dkeystore.pkcs12.certPbeIterationCount=2048 \ -J-Dkeystore.pkcs12.macAlgorithm=HmacPBESHA1 \ -J-Dkeystore.pkcs12.macIterationCount=2048
The four -J-D options pass the instruction "use the old 3DES/SHA1 format" through to the JVM that runs keytool. Result: an entry with an alias, in the Java-readable format. Both conditions satisfied in one shot.
When the file has to be built inside the container
A practical stumbling block on the side: which keytool you use is not irrelevant. The most reliable method is to build the keystore with exactly the Java that will later read it — that is, inside the container itself. Then it's guaranteed that the produced format is compatible.
Because the certificate directory in the container is often mounted read-only (read-only), you build the file in /tmp and stream it out:
docker compose run --rm -v /host/certs:/out:ro --entrypoint sh keycloak -c '
keytool -importcert -noprompt -alias corp-ca \
-file /out/ca.crt \
-keystore /tmp/root.p12 -storetype PKCS12 -storepass <TRUSTSTORE_PW> \
-J-Dkeystore.pkcs12.certProtectionAlgorithm=PBEWithSHA1AndDESede \
-J-Dkeystore.pkcs12.macAlgorithm=HmacPBESHA1 && cat /tmp/root.p12
' > /host/certs/root.p12
The trick: keytool writes to /tmp (always writable), dumps the result to standard output with cat, and the shell on the host redirects it with > into the target file — where you have write permissions, not the container.
What Part 2 taught me
- "0 entries" despite successful creation = the certificate has no alias. Use
keytool -importcertwith-alias. - "password was incorrect" even though the password is right = in truth an algorithm problem. OpenSSL 3 writes AES-256, old Java can't read it.
- Java needs the legacy format
pbeWithSHA1And3-KeyTripleDES-CBC/MAC: sha1. keytool+-J-Doptions produce filled and legacy in one go.- Always verify with
keytool, not just OpenSSL — only that shows you the Java view. - Build keystores in the target container, then the format is guaranteed to fit.
In the next part, things get organizationally treacherous: what happens when several services expect different passwords for the same file — and how to keep three different password variables apart.