SSO Setup
Certificate trust, client and server side
Serving TLS and being trusted are two separate jobs. This page is the second one, and it is where the time goes.
★★ The rule that catches everyone: a JVM does not read the Windows
certificate store. Importing a CA into LocalMachine\Root fixes browsers and
leaves every Java client failing, with an error (PKIX path building failed)
that says nothing about Windows. Teamcenter's own server-side calls are JVM, so
this is not an edge case here.
The trust matrix
Four stores had to be satisfied. Two were already done for Keycloak; two were added for this work.
| # | Store | Who reads it | Status |
|---|---|---|---|
| 1 | Guest LocalMachine\Root |
Browsers on the guest, .NET, PowerShell, Python | Pre-existing (Keycloak work) |
| 2 | Guest JDK cacerts |
Every JVM client on the guest, incl. Teamcenter itself | Pre-existing (Keycloak work) |
| 3 | Host LocalMachine\Root |
Host browsers, host curl, tc-mcp | Added for this work |
| 4 | DC truststore (dc-truststore.p12) |
Deployment Center utilities | Created for this work |
1 and 2: the guest, server side
Already in place before this work, and the reason reusing the CA was worth it:
Guest LocalMachine\Root thumbprint 1A81166F2979426C0706B63FB7D10BC737C7ECA6
Guest JDK cacerts alias xcelerator-local-ca
C:\apps\jdk-21.0.11.10\lib\security\cacerts
A backup of the pre-CA truststore was kept as
cacerts.bak-2026-08-15-pre-xcelerator-ca. That backup turns out to be the
single most useful artefact for verification: it is a truststore identical to
the live one except that it lacks our CA, which makes it a perfect negative
control. See verification.
3: the host, client side
This is the one that was missing, and it blocked three things at once:
- host browsers warned on both Active Workspace and Deployment Center
- host
curlfailed - tc-mcp failed with
CERTIFICATE_VERIFY_FAILED
One import fixed all three:
Import-Certificate -FilePath "...\keycloak-sso\certs\ca.crt" `
-CertStoreLocation Cert:\LocalMachine\Root
⚠ This must be run from an elevated, interactive shell. From a
non-interactive session it fails with UI is not allowed in this operation, and
Cert:\CurrentUser\Root is no easier: the root store wants a confirmation
dialog that a headless session cannot display.
★ Why one import fixed tc-mcp too, which is not obvious. tc_client uses
urllib, not requests, so REQUESTS_CA_BUNDLE does nothing at all. Python on
Windows loads the Windows ROOT store through ssl.create_default_context() and load_default_certs(),
so trusting the CA in Windows is exactly what Python needed. Chasing a
Python-specific CA bundle would have been wasted work.
4: the Deployment Center truststore
The Deployment Center guide says that if the keystore certificate is not signed by a publicly trusted CA, you may supply an associated truststore. Ours is a local CA, so one was built:
C:\apps\DC\webserver\config\dc-truststore.p12 PKCS12, 1 entry: xcelerator-local-ca
It is wired into dcserver.properties as DC_TRUSTSTORE_PATH and into Jetty as
jetty.sslContext.trustStorePath. The password is recorded in
dcserver.properties on the guest and is deliberately not reproduced here.
Client-side results, measured
After the host import:
| Client | Result |
|---|---|
Invoke-WebRequest (.NET/WinHTTP) |
HTTP 200, no flags |
curl --ssl-no-revoke |
HTTP 200, no --cacert needed |
curl plain |
fails, see below |
Guest JVM (java.net.http) |
HTTP 200 with default cacerts |
| tc-mcp | works once the profile points at https://siemensdc |
⚠ The instrument trap: schannel and revocation
Windows curl.exe uses the schannel backend, and after a successful trust
import it still fails:
curl: (60) schannel: the revocation status is unknown
That is a revocation complaint, not a trust complaint, and it reads exactly like a trust failure. A private CA publishes no CRL or OCSP endpoint, so schannel cannot check revocation and refuses by default. Two consequences:
- Add
--ssl-no-revokewhen testing with Windows curl, or useopenssl s_clientinstead, which validates properly and reports honestly. - schannel also cannot do SNI against a bare IP address, so testing by IP produces a second, unrelated-looking failure.
Neither affects browsers or the JVM. Diagnosing the server because curl said 60 would have been a wasted afternoon.