Everybody installs the certificate on the server. The handshake fails everywhere else.

TLS trust in an Extended ECM landscape is not one setting. OpenJDK, Content Server's own bundled JRE, SAP STRUST and the Windows certificate store each keep a separate list, and a handshake error is always fixed in the component that made the call.

The certificate was renewed on Saturday, properly, by somebody who knew what they were doing. On Monday morning most things work. The browser is happy, the site shows a padlock, and the person who did the work has every reason to believe the job is finished.

Then the tickets start. The document pipeline has stopped importing. Directory Services can no longer bind. SAP cannot reach the archive. Every log says roughly the same thing about a failed SSL handshake or a path that cannot be built, and everybody goes and looks at the server where the certificate was installed, because that is obviously where a certificate problem lives.

It is not. The certificate on that server is fine. What is missing is trust, and trust is not a property of the machine presenting the certificate. It is a property of every single thing that calls it, and in an Extended ECM landscape there are more of those, keeping more separate lists, than almost anybody expects. This is reputedly one of the hardest things to pin down in this stack. It is actually one of the easiest, once you know where the lists are.

What the error is actually saying

A TLS handshake is short. The server presents its certificate along with any intermediate certificates above it. The caller then tries to build a path from that certificate up to something it already trusts, held in its own local list. If it can build the path, the connection proceeds. If it cannot, it refuses and reports an error.

Two things have to be true, and they live on opposite ends of the wire.

The server has to present a complete chain: its own certificate plus the intermediates above it. A server that sends only its own certificate will work for some callers and fail for others, purely by luck, depending on whether each one happens to have the intermediate already.

And the caller has to trust the issuer, not the certificate. This is the part that gets done wrong most often, because importing the server certificate itself into a Java truststore does make the error go away. It works right up until the certificate is renewed, at which point everything breaks again and somebody spends another Saturday. Import the root and the intermediate instead and renewal becomes invisible: the new certificate is signed by the same issuer you already trust, and nothing anywhere has to change.

That single distinction is the difference between doing this once and doing it every year.

The golden rule

A handshake error is fixed in the application that made the request. Always. Assuming the certificate on the other side is correctly installed, the component that reported the error is the component whose trust store is missing something.

It sounds obvious written down. It is routinely ignored in practice, because the error appears in the pipeline log and the pipeline is not where anybody expects a certificate problem to be, so the investigation goes to the archive instead and stays there for two days.

A handshake error is reported by the side that did not trust, and fixed on the side that did not trust. It is one of the few error messages that tells you exactly which server to log into.

— three days spent on the wrong machine

The rule has one honest condition attached, which is the assumption about the other side. If the server is presenting an incomplete chain, no amount of work on the caller will produce a reliable result. So confirm that first, once, from any machine:

openssl s_client -connect archive.my.domain:443 -showcerts

If the output shows the server certificate and the intermediate above it, the far end is doing its job and everything that follows is a trust store question. If it shows only the server certificate, fix that before touching anything else.

Where trust actually lives

Here is the part that makes this feel harder than it is. A single Windows server can hold several completely independent trust stores, and adding a root to one of them does nothing for the others. When somebody says "but the server trusts it", the machine does. The Java process running on that machine does not.

ComponentWhere its trust lives
Archive Center Intelligent Viewing, Document Pipeline, Directory ServicesThe OpenJDK cacerts store
Content ServerIts own bundled JRE under <root_cs>\jre\
Content Server link and SPI layer<root_cs>\config\ecmlink-spi.cacerts
SAPSTRUST
Windows servers, and every desktopThe Windows certificate manager

Content Server deserves the double entry. It ships and runs its own JRE rather than using whatever Java is installed on the machine, so the cacerts you carefully updated for Archive Center is not the one Content Server reads. On top of that it keeps a separate store in config for its link layer. Two files, same product, same server.

Three things follow from that table, and they are the ones worth remembering.

Java never reads the Windows store. Not by default, not because it is on the same server, not because an administrator can see the certificate in certlm.msc. This is the source of the classic symptom where the certificate opens perfectly in a browser on the archive server and the document pipeline running on that same server refuses to connect.

Patching Java resets the list. A JRE or JDK upgrade generally installs a fresh cacerts, and a fresh cacerts does not contain your internal root. The same is true of a Content Server update that replaces its bundled JRE. Certificates are not usually the first thing anyone checks after a patch, which is exactly why this produces such confusing incidents.

Firefox keeps its own store too. On a desktop where everything else works, Firefox can still refuse, because it does not use the Windows certificate manager unless it has been policy-configured to. Worth knowing before you conclude that one user's machine is broken.

Self-signed always, internal usually, public sometimes

How much of this work you have depends entirely on who signed the certificate, and the three cases behave very differently.

A self-signed certificate is trusted by nothing, anywhere, by definition. Every store in the table above needs it, without exception. If you are running self-signed certificates internally, the full distribution exercise is not an edge case, it is the standard cost of that choice, every time one is issued.

An internal certificate authority is the interesting case, and the one that produces the most confusion. In a Windows domain, the internal root is distributed to the Windows certificate stores automatically by Group Policy, so from the Windows side everything looks correct and nobody has anything to do. No Group Policy has ever updated a cacerts file. The result is a landscape where the browsers all work, the Java components all fail, and the difference is invisible unless you know to look for it.

A public certificate authority usually needs nothing, because the public roots ship inside every store already. Usually. An older JRE whose cacerts predates a newer root, or a CA that has rotated to a new intermediate, will fail exactly like an internal certificate would. It is rare enough that people stop suspecting it, which makes it expensive when it happens.

While you are checking protocols, the neighbouring trap is worth a glance: a page served over https cannot load anything over plain http, which produces a blank frame rather than a handshake error and is covered in the post on documents that will not display.

Doing it once so it stays done

Adding a root to a Java store is one command, run against each store that needs it:

keytool -importcert -trustcacerts -alias my-internal-root ^
        -file root.cer -keystore "<path>\cacerts" -storepass changeit

Repeat it for the intermediate under a second alias. The default password on a cacerts file is changeit, which is not a joke and is very often still the password. To confirm what a store already holds before or after:

keytool -list -keystore "<path>\cacerts" -storepass changeit

Every one of these takes effect at process start, so the component has to be restarted before anything changes. A store that contains the right root and has not been restarted looks identical, from the log, to a store that does not.

The part that actually saves you time, though, is not the command. It is writing down the list. Every truststore in the landscape, with its full path, on one page: the OpenJDK cacerts, the Content Server JRE, ecmlink-spi.cacerts, STRUST, the Windows stores on each server, anything else specific to your estate. That page turns the next certificate renewal from an investigation into a checklist, and it turns the next Java patch from a mystery into a known follow-up task.


Once every server, every client and every application holds the root and the intermediate, handshake errors stop. Not mostly, not for the components you remembered. They stop, and they stay stopped through the next renewal, because you trusted the issuer rather than the certificate.

The reason this has a reputation for being difficult is that the evidence is scattered across components owned by different teams, and the error surfaces in the one place nobody thinks to look, which is the caller. The rule is short enough to keep in your head. Whoever reported the error is whoever needs the certificate, the thing they need is the root and the intermediate, never the server certificate, and there are only five or six places in the entire landscape where it has to go.

Portrait of Ruud Palmen
Written by

Ruud Palmen — ECM Solution Architect

15+ years of OpenText. More background on the about page, or follow along on LinkedIn.