Skip to content

Make server-ca optional in Cloud Logging credentials - #432

Draft
JannikBrand wants to merge 4 commits into
SAP:mainfrom
JannikBrand:optional-server-ca
Draft

Make server-ca optional in Cloud Logging credentials#432
JannikBrand wants to merge 4 commits into
SAP:mainfrom
JannikBrand:optional-server-ca

Conversation

@JannikBrand

@JannikBrand JannikBrand commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

What

Make server-ca optional in the auto-configured OTLP exporters, and when it is present, add it to the default Node.js trust store (rather than replacing the trust store with it).

Why

SAP Cloud Logging's OTLP ingest endpoint presents a server certificate that chains to a public root (Let's Encrypt ISRG Root X1). Java's (e.g. Sapmachine) cacerts may already trusts ISRG X1 and X2. The server-ca field in the binding response is a convenience for runtimes that don't already trust public roots.

Under the previous "replace" semantics:

  • The server-ca value from the binding became the sole trust anchor. If the endpoint's certificate ever chained to a different public root than the one pinned in the binding it would fail.
  • The system trust store was effectively dead code inside our SDK, which contradicts the guidance in the paired docs update (SAP-docs/btp-cloud-logging#61) that "the runtime's system trust store is sufficient in the common case".

We add server-ca rather than replace the system trust store with it because bindings today always ship server-ca. So a replacing it would never exercise the system trust store and would break on CA rotations.

Testing

  • mvn -pl cf-java-logging-support-opentelemetry-agent-extension test — 274 tests pass, 0 failures (SapMachine 17.0.20.1).
  • Not yet exercised end-to-end against a real Cloud Logging tenant with the Java agent — happy to run through a testing checklist or coordinate with maintainers on a canary tenant.
  • No changes to public API.

Companion changes

Signed-off-by: Jannik Brand <jannik.brand@sap.com>
@cla-assistant

cla-assistant Bot commented Sep 10, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@KarstenSchnitter KarstenSchnitter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please comment whether you tested this with a real application and the Java agent. Up to now, the agent would not create a connection without an explicit server certificate to trust. The agent would just not connect and create errors. This change can only work if the implementation of the agent changed.
Note, that other exporters circumvent this restriction by downloading the server certificate if it was not provided.


byte[] serverCert = credentials.getServerCert();
if (serverCert != null && serverCert.length > 0) {
builder.setTrustedCertificates(serverCert);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this conditionally is known to break the Java agent. It would not establish the connection. You may want to change the TLS setup using builder.setSslContext() instead. This would also be a way to exchange client key and certificate without restart in future.

@JannikBrand
JannikBrand marked this pull request as draft September 10, 2026 08:36
Signed-off-by: Jannik Brand <jannik.brand@sap.com>

@KarstenSchnitter KarstenSchnitter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original issue is unresolved. When no server-ca is given this implementation will crash.

@KarstenSchnitter

KarstenSchnitter commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Quite honestly, I would like to see a new interface being introduced based on Supplier<X509Certificate>. That could be implemented by the server-ca from the credentials, the ServerCertificateDownloader or this approach using the system certificates. Using a new mapper / joiner class, this should generate the input to setTrustedCertificates.

@KarstenSchnitter KarstenSchnitter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please look for other places in this extension, where the PemEncoder could be used.

.setClientTls(credentials.getClientKey(), credentials.getClientCert())
.setTrustedCertificates(credentials.getServerCert()).setRetryPolicy(RetryPolicy.getDefault());
.setTrustedCertificates(TrustedCertificatesJoiner.toPemBytes(
new SystemTrustAnchorSource(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would actually expect, that an explicit certificate takes precedence over the system trust store. Furthermore, if the server-ca is provided explicitly, why would you still trust the default certs?

- Inject Function<CloudLoggingCredentials, byte[]> into all three exporter providers so the joiner can be replaced in tests.
- Rename X509CertificateSource#get() to stream().
- SystemTrustAnchorSource: merge accepted issuers from every X509TrustManager returned by the default TrustManagerFactory.
- PemEncoder: use UTF-8 for the base64 line separator.
- Tests: share the loaded certificate as a constant in PemEncoderTest; use Assumptions.assumeThat for the resource-not-null precondition in the tls tests; fold the join-order assertion into the concatenation test.

@KarstenSchnitter KarstenSchnitter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is still some code duplication. But the major question is why to use the system trust store when an explicit server-ca was provided.

Comment on lines +34 to +35
credentials -> TrustedCertificatesJoiner.toPemBytes(new SystemTrustAnchorSource(),
new BindingServerCertificateSource(credentials.getServerCert())));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would actually expect, that an explicit certificate takes precedence over the system trust store. Furthermore, if the server-ca is provided explicitly, why would you still trust the default certs?

try {
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore) null);
return Arrays.stream(tmf.getTrustManagers())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can tmf.getTrustManagers be null?

return Arrays.stream(tmf.getTrustManagers())
.filter(X509TrustManager.class::isInstance)
.map(X509TrustManager.class::cast)
.flatMap(tm -> Arrays.stream(tm.getAcceptedIssuers()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can tm.get.getAcceptedIssuers be null?

Comment on lines +38 to +47
try {
CertificateFactory factory = CertificateFactory.getInstance("X.509");
X509Certificate certificate = (X509Certificate) factory
.generateCertificate(new ByteArrayInputStream(pem.getBytes(StandardCharsets.UTF_8)));
return Stream.of(certificate);
} catch (CertificateException e) {
LOG.log(Level.WARNING, e, () -> "Failed to parse server certificate downloaded from " + endpointUrl
+ "; it will be omitted from the trust anchors.");
return Stream.empty();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is largely duplicate code to the BindingServerCertificateSource.

}
try {
CertificateFactory factory = CertificateFactory.getInstance("X.509");
X509Certificate certificate = (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(pemBytes));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use generateCertificates (in plural) to get the full certificate chain that might be provided within the pemBytes.

@JannikBrand

JannikBrand commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

There is still some code duplication. But the major question is why to use the system trust store when an explicit server-ca was provided.

Optimally, I would agree and wanted a replacing instead of additive mechanism here.
This is what other OTEL SKDs are doing, but I also found other examples (e.g. Loggregator) where the behavior would be additive.
However, a best practice in general would be to rely on the system trust store whenever it is possible and only pin the ca when the system trust store does not include it. The server-ca in the binding is only a snapshot so it might be outdated eventually.
Since Cloud Logging bindings always return the server-ca field we would always pin it, which is why I don't want to leave out the system trust store.

(This is similar to the contribution in opentelemetry-exporter-for-sap-cloud-logging-for-nodejs)

We add server-ca rather than replace the system trust store with it because bindings today always ship server-ca. So a replacing it would never exercise the system trust store and would break on CA rotations.

Happy to get more feedback on this.
We could also expose a setting where users could configure additive or replacing behavior, but I intended to keep things simple for now, since this PR is already including quite some changes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants