Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,8 @@ public boolean validate() {
}

if (isNullOrEmpty(serverCert)) {
LOG.warning(
"Credential \"" + CRED_OTLP_SERVER_CERT + "\" not found. Skipping cloud-logging exporter configuration");
return false;
LOG.info("Credential \"" + CRED_OTLP_SERVER_CERT
+ "\" not present in service binding. Falling back to the JVM default trust store for the ingest endpoint.");
}
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.CloudFoundryServiceInstance;
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.CloudLoggingServicesProvider;
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.config.ExtensionConfigurations.EXPORTER;
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls.BindingServerCertificateSource;
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls.SystemTrustAnchorSource;
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls.TrustedCertificatesJoiner;
import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter;
import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporterBuilder;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
Expand All @@ -23,15 +26,21 @@ public class CloudLoggingLogsExporterProvider implements ConfigurableLogRecordEx

private final Function<ConfigProperties, Stream<CloudFoundryServiceInstance>> servicesProvider;
private final CloudLoggingCredentials.Parser credentialParser;
private final Function<CloudLoggingCredentials, byte[]> trustedCertificatesProvider;

public CloudLoggingLogsExporterProvider() {
this(config -> new CloudLoggingServicesProvider(config).get(), CloudLoggingCredentials.parser());
this(config -> new CloudLoggingServicesProvider(config).get(),
CloudLoggingCredentials.parser(),
credentials -> TrustedCertificatesJoiner.toPemBytes(new SystemTrustAnchorSource(),
new BindingServerCertificateSource(credentials.getServerCert())));
Comment on lines +34 to +35

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?

}

CloudLoggingLogsExporterProvider(Function<ConfigProperties, Stream<CloudFoundryServiceInstance>> serviceProvider,
CloudLoggingCredentials.Parser credentialParser) {
CloudLoggingCredentials.Parser credentialParser,
Function<CloudLoggingCredentials, byte[]> trustedCertificatesProvider) {
this.servicesProvider = serviceProvider;
this.credentialParser = credentialParser;
this.trustedCertificatesProvider = trustedCertificatesProvider;
}

private static String getCompression(ConfigProperties config) {
Expand Down Expand Up @@ -65,7 +74,8 @@ private LogRecordExporter createExporter(ConfigProperties config, CloudFoundrySe
OtlpGrpcLogRecordExporterBuilder builder = OtlpGrpcLogRecordExporter.builder();
builder.setEndpoint(credentials.getEndpoint()).setCompression(getCompression(config))
.setClientTls(credentials.getClientKey(), credentials.getClientCert())
.setTrustedCertificates(credentials.getServerCert()).setRetryPolicy(RetryPolicy.getDefault());
.setTrustedCertificates(trustedCertificatesProvider.apply(credentials))
.setRetryPolicy(RetryPolicy.getDefault());

Duration timeOut = getTimeOut(config);
if (timeOut != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.CloudFoundryServiceInstance;
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.CloudLoggingServicesProvider;
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.config.ExtensionConfigurations.EXPORTER;
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls.BindingServerCertificateSource;
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls.SystemTrustAnchorSource;
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls.TrustedCertificatesJoiner;
import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter;
import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporterBuilder;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
Expand Down Expand Up @@ -32,15 +35,21 @@ public class CloudLoggingMetricsExporterProvider implements ConfigurableMetricEx

private final Function<ConfigProperties, Stream<CloudFoundryServiceInstance>> servicesProvider;
private final CloudLoggingCredentials.Parser credentialParser;
private final Function<CloudLoggingCredentials, byte[]> trustedCertificatesProvider;

public CloudLoggingMetricsExporterProvider() {
this(config -> new CloudLoggingServicesProvider(config).get(), CloudLoggingCredentials.parser());
this(config -> new CloudLoggingServicesProvider(config).get(),
CloudLoggingCredentials.parser(),
credentials -> TrustedCertificatesJoiner.toPemBytes(new SystemTrustAnchorSource(),
new BindingServerCertificateSource(credentials.getServerCert())));
}

CloudLoggingMetricsExporterProvider(Function<ConfigProperties, Stream<CloudFoundryServiceInstance>> serviceProvider,
CloudLoggingCredentials.Parser credentialParser) {
CloudLoggingCredentials.Parser credentialParser,
Function<CloudLoggingCredentials, byte[]> trustedCertificatesProvider) {
this.servicesProvider = serviceProvider;
this.credentialParser = credentialParser;
this.trustedCertificatesProvider = trustedCertificatesProvider;
}

private static String getCompression(ConfigProperties config) {
Expand Down Expand Up @@ -114,7 +123,8 @@ private MetricExporter createExporter(ConfigProperties config, CloudFoundryServi
OtlpGrpcMetricExporterBuilder builder = OtlpGrpcMetricExporter.builder();
builder.setEndpoint(credentials.getEndpoint()).setCompression(getCompression(config))
.setClientTls(credentials.getClientKey(), credentials.getClientCert())
.setTrustedCertificates(credentials.getServerCert()).setRetryPolicy(RetryPolicy.getDefault())
.setTrustedCertificates(trustedCertificatesProvider.apply(credentials))
.setRetryPolicy(RetryPolicy.getDefault())
.setAggregationTemporalitySelector(getAggregationTemporalitySelector(config))
.setDefaultAggregationSelector(getDefaultAggregationSelector(config));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.CloudFoundryServiceInstance;
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.CloudLoggingServicesProvider;
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.config.ExtensionConfigurations.EXPORTER;
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls.BindingServerCertificateSource;
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls.SystemTrustAnchorSource;
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls.TrustedCertificatesJoiner;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
Expand All @@ -23,15 +26,21 @@ public class CloudLoggingSpanExporterProvider implements ConfigurableSpanExporte

private final Function<ConfigProperties, Stream<CloudFoundryServiceInstance>> servicesProvider;
private final CloudLoggingCredentials.Parser credentialParser;
private final Function<CloudLoggingCredentials, byte[]> trustedCertificatesProvider;

public CloudLoggingSpanExporterProvider() {
this(config -> new CloudLoggingServicesProvider(config).get(), CloudLoggingCredentials.parser());
this(config -> new CloudLoggingServicesProvider(config).get(),
CloudLoggingCredentials.parser(),
credentials -> TrustedCertificatesJoiner.toPemBytes(new SystemTrustAnchorSource(),
new BindingServerCertificateSource(credentials.getServerCert())));
}

CloudLoggingSpanExporterProvider(Function<ConfigProperties, Stream<CloudFoundryServiceInstance>> serviceProvider,
CloudLoggingCredentials.Parser credentialParser) {
CloudLoggingCredentials.Parser credentialParser,
Function<CloudLoggingCredentials, byte[]> trustedCertificatesProvider) {
this.servicesProvider = serviceProvider;
this.credentialParser = credentialParser;
this.trustedCertificatesProvider = trustedCertificatesProvider;
}

private static String getCompression(ConfigProperties config) {
Expand Down Expand Up @@ -65,7 +74,8 @@ private SpanExporter createExporter(ConfigProperties config, CloudFoundryService
OtlpGrpcSpanExporterBuilder builder = OtlpGrpcSpanExporter.builder();
builder.setEndpoint(credentials.getEndpoint()).setCompression(getCompression(config))
.setClientTls(credentials.getClientKey(), credentials.getClientCert())
.setTrustedCertificates(credentials.getServerCert()).setRetryPolicy(RetryPolicy.getDefault());
.setTrustedCertificates(trustedCertificatesProvider.apply(credentials))
.setRetryPolicy(RetryPolicy.getDefault());

Duration timeOut = getTimeOut(config);
if (timeOut != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls;

import java.io.ByteArrayInputStream;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;

/**
* Supplies the X.509 certificate carried by a Cloud Foundry service binding's
* {@code server-ca} field (or an equivalent field name for other bindings).
*
* <p>The input is expected to be the raw PEM bytes as they arrive in the binding.
* A {@code null} or empty input yields an empty stream, so the source can be used
* unconditionally with {@link TrustedCertificatesJoiner}.</p>
*/
public class BindingServerCertificateSource implements X509CertificateSource {

private static final Logger LOG = Logger.getLogger(BindingServerCertificateSource.class.getName());

private final byte[] pemBytes;

public BindingServerCertificateSource(byte[] pemBytes) {
this.pemBytes = pemBytes;
}

@Override
public Stream<X509Certificate> stream() {
if (pemBytes == null || pemBytes.length == 0) {
return Stream.empty();
}
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.

return Stream.of(certificate);
} catch (CertificateException e) {
LOG.log(Level.WARNING, e, () -> "Failed to parse server-ca from service binding; it will be omitted from the trust anchors.");
return Stream.empty();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls;

import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;

/**
* Supplies the X.509 leaf certificate returned by {@link ServerCertificateDownloader}
* for a fixed OTLP endpoint URL.
*
* <p>This is the fallback path used when a service binding does not carry a
* {@code server-ca} field. It connects to the endpoint with TLS (validation disabled)
* and reads back the leaf certificate the server presents.</p>
*/
public class DownloadedServerCertificateSource implements X509CertificateSource {

private static final Logger LOG = Logger.getLogger(DownloadedServerCertificateSource.class.getName());

private final ServerCertificateDownloader downloader;
private final String endpointUrl;

public DownloadedServerCertificateSource(ServerCertificateDownloader downloader, String endpointUrl) {
this.downloader = downloader;
this.endpointUrl = endpointUrl;
}

@Override
public Stream<X509Certificate> stream() {
String pem = downloader.download(endpointUrl);
if (pem == null || pem.isEmpty()) {
return Stream.empty();
}
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();
}
Comment on lines +38 to +47

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.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls;

import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Base64;

/**
* Encodes X.509 certificates as PEM strings.
*
* <p>Line separator is {@code "\n"} and Base64 body is wrapped at 64 characters,
* matching the format expected by OpenTelemetry's {@code setTrustedCertificates(byte[])}.</p>
*/
final class PemEncoder {

private static final String LINE_SEPARATOR = "\n";
private static final Base64.Encoder BASE64_ENCODER =
Base64.getMimeEncoder(64, LINE_SEPARATOR.getBytes(StandardCharsets.UTF_8));

private PemEncoder() {
}

/**
* Returns the PEM encoding of the given certificate, including the
* {@code BEGIN CERTIFICATE} / {@code END CERTIFICATE} armor and a trailing newline.
*/
static String encode(X509Certificate certificate) throws CertificateEncodingException {
return "-----BEGIN CERTIFICATE-----" + LINE_SEPARATOR
+ BASE64_ENCODER.encodeToString(certificate.getEncoded()) + LINE_SEPARATOR
+ "-----END CERTIFICATE-----" + LINE_SEPARATOR;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,18 @@
import javax.net.ssl.*;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.logging.Level;
import java.util.logging.Logger;

public class ServerCertificateDownloader {

private static final Logger LOG = Logger.getLogger(ServerCertificateDownloader.class.getName());
private static final byte[] LINE_SEPARATOR = "\n".getBytes(StandardCharsets.UTF_8);
private static final Base64.Encoder BASE64_ENCODER = Base64.getMimeEncoder(64, LINE_SEPARATOR);

private final SSLSocketFactory sslSocketFactory;

Expand Down Expand Up @@ -78,12 +74,7 @@ public String download(String endpointUrl) {
return null;
}

X509Certificate x509Cert = (X509Certificate) serverCertificates[0];
byte[] encoded = x509Cert.getEncoded();
return "-----BEGIN CERTIFICATE-----\n" //
+ BASE64_ENCODER.encodeToString(encoded) //
+ "\n-----END CERTIFICATE-----\n";

return PemEncoder.encode((X509Certificate) serverCertificates[0]);
}
} catch (CertificateEncodingException | IOException e) {
LOG.log(Level.WARNING, e, () -> "Failed to download server certificate from " + endpointUrl);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls;

import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;

/**
* Supplies the X.509 trust anchors known to the JVM's default trust store
* (typically {@code $JAVA_HOME/lib/security/cacerts}).
*
* <p>This is the same set the platform uses to validate ordinary HTTPS connections,
* so any endpoint whose server certificate chains to a public root is trusted without
* additional configuration.</p>
*
* <p>The stream aggregates the accepted issuers of <em>every</em>
* {@link X509TrustManager} returned by the default {@link TrustManagerFactory}, so a
* runtime with multiple configured trust managers contributes all of them.</p>
*/
public class SystemTrustAnchorSource implements X509CertificateSource {

private static final Logger LOG = Logger.getLogger(SystemTrustAnchorSource.class.getName());

@Override
public Stream<X509Certificate> stream() {
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?

.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?

} catch (NoSuchAlgorithmException | KeyStoreException e) {
LOG.log(Level.WARNING, e, () -> "Failed to enumerate JVM default trust anchors; system trust anchors will be omitted.");
return Stream.empty();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls;

import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Concatenates the PEM encodings of the certificates from one or more
* {@link X509CertificateSource} instances into a single byte array suitable for
* {@code OtlpGrpc*ExporterBuilder.setTrustedCertificates(byte[])}.
*
* <p>Sources are consumed in the order given; certificates that fail to encode
* are logged and skipped so a single bad certificate does not break the exporter.</p>
*/
public final class TrustedCertificatesJoiner {

private static final Logger LOG = Logger.getLogger(TrustedCertificatesJoiner.class.getName());

private TrustedCertificatesJoiner() {
}

/**
* Encodes every certificate produced by the given sources as PEM and returns the
* concatenated bytes (UTF-8). Empty sources contribute nothing; the returned array
* is empty if no source yields a certificate.
*/
public static byte[] toPemBytes(X509CertificateSource... sources) {
StringBuilder pem = new StringBuilder();
for (X509CertificateSource source : sources) {
source.stream().forEach(cert -> appendPem(pem, cert));
}
return pem.toString().getBytes(StandardCharsets.UTF_8);
}

private static void appendPem(StringBuilder pem, X509Certificate cert) {
try {
pem.append(PemEncoder.encode(cert));
} catch (CertificateEncodingException e) {
LOG.log(Level.WARNING, e, () -> "Failed to PEM-encode a trust anchor; it will be omitted.");
}
}
}
Loading
Loading