TeamcenterKnowledge

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 curl failed
  • 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-revoke when testing with Windows curl, or use openssl s_client instead, 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.