Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[otlp] Add mTLS Support for OTLP Exporter #5918

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
@@ -0,0 +1,6 @@
OpenTelemetry.Exporter.OtlpExporterOptions.CertificateFile.get -> string!
OpenTelemetry.Exporter.OtlpExporterOptions.CertificateFile.set -> void
OpenTelemetry.Exporter.OtlpExporterOptions.ClientCertificateFile.get -> string!
OpenTelemetry.Exporter.OtlpExporterOptions.ClientCertificateFile.set -> void
OpenTelemetry.Exporter.OtlpExporterOptions.ClientKeyFile.get -> string!
OpenTelemetry.Exporter.OtlpExporterOptions.ClientKeyFile.set -> void
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
using OpenTelemetry.Internal;
using OpenTelemetry.Trace;
#if NET6_0_OR_GREATER
using System.Security.Cryptography.X509Certificates;
#endif

namespace OpenTelemetry.Exporter;

Expand All @@ -27,6 +30,9 @@ public class OtlpExporterOptions : IOtlpExporterOptions
internal const string DefaultGrpcEndpoint = "http://localhost:4317";
internal const string DefaultHttpEndpoint = "http://localhost:4318";
internal const OtlpExportProtocol DefaultOtlpExportProtocol = OtlpExportProtocol.Grpc;
internal const string CertificateFileEnvVarName = "OTEL_EXPORTER_OTLP_CERTIFICATE";
internal const string ClientKeyFileEnvVarName = "OTEL_EXPORTER_OTLP_CLIENT_KEY";
internal const string ClientCertificateFileEnvVarName = "OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE";

internal static readonly KeyValuePair<string, string>[] StandardHeaders = new KeyValuePair<string, string>[]
{
Expand Down Expand Up @@ -68,13 +74,80 @@ internal OtlpExporterOptions(

this.DefaultHttpClientFactory = () =>
{
#if NET6_0_OR_GREATER
// Create a new handler
var handler = new HttpClientHandler();

// Load server certificate
if (!string.IsNullOrEmpty(this.CertificateFile))
{
var trustedCertificate = X509Certificate2.CreateFromPemFile(this.CertificateFile);

handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>

Choose a reason for hiding this comment

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

Do you want users of this API being able to add their custom validation callbacks next to this one?

{
if (cert != null && chain != null)
{
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
chain.ChainPolicy.CustomTrustStore.Add(trustedCertificate);
return chain.Build(cert);
}

return false;
};
}

// Load client certificate for mTLS
if (!string.IsNullOrEmpty(this.ClientCertificateFile) && !string.IsNullOrEmpty(this.ClientKeyFile))
{
var clientCertificate = X509Certificate2.CreateFromPemFile(this.ClientCertificateFile, this.ClientKeyFile);
handler.ClientCertificates.Add(clientCertificate);
}

// Create and return the HttpClient
return new HttpClient(handler)
{
Timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds),
};
#else
// For earlier .NET versions
return new HttpClient
{
Timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds),
};
#endif
};

this.BatchExportProcessorOptions = defaultBatchOptions!;

// Load CertificateFile from environment variable
if (Environment.GetEnvironmentVariable(CertificateFileEnvVarName) is string certificateFile)
{
this.CertificateFile = certificateFile;
}
else
{
this.CertificateFile = string.Empty;
}

// Load ClientKeyFile from environment variable
if (Environment.GetEnvironmentVariable(ClientKeyFileEnvVarName) is string clientKeyFile)
{
this.ClientKeyFile = clientKeyFile;
}
else
{
this.ClientKeyFile = string.Empty;
}

// Load ClientCertificateFile from environment variable
if (Environment.GetEnvironmentVariable(ClientCertificateFileEnvVarName) is string clientCertificateFile)
{
this.ClientCertificateFile = clientCertificateFile;
}
else
{
this.ClientCertificateFile = string.Empty;
}
}

/// <inheritdoc/>
Expand Down Expand Up @@ -142,6 +215,21 @@ public Func<HttpClient> HttpClientFactory
}
}

/// <summary>
/// Gets or sets the trusted certificate to use when verifying a server's TLS credentials.
/// </summary>
public string CertificateFile { get; set; }

/// <summary>
/// Gets or sets the path to the private key to use in mTLS communication in PEM format.
/// </summary>
public string ClientKeyFile { get; set; }

/// <summary>
/// Gets or sets the path to the certificate/chain trust for client's private key to use in mTLS communication in PEM format.
/// </summary>
public string ClientCertificateFile { get; set; }

/// <summary>
/// Gets a value indicating whether or not the signal-specific path should
/// be appended to <see cref="Endpoint"/>.
Expand Down Expand Up @@ -220,6 +308,45 @@ internal OtlpExporterOptions ApplyDefaults(OtlpExporterOptions defaultExporterOp
return this;
}

internal HttpClient AddCertificatesToHttpClient(HttpClientHandler handler)
{
#if NET6_0_OR_GREATER
// Set up server certificate validation if CertificateFile is provided
if (!string.IsNullOrEmpty(this.CertificateFile))
{
// Load the certificate from the file
var trustedCertificate = X509Certificate2.CreateFromPemFile(this.CertificateFile);

// Set custom server certificate validation callback
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
{
if (cert != null && chain != null)
{
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
chain.ChainPolicy.CustomTrustStore.Add(trustedCertificate);
return chain.Build(cert);
}

return false;
};
}

// Add client certificate if both files are provided
if (!string.IsNullOrEmpty(this.ClientCertificateFile) && !string.IsNullOrEmpty(this.ClientKeyFile))
{
// Load the client certificate from PEM files
var clientCertificate = X509Certificate2.CreateFromPemFile(this.ClientCertificateFile, this.ClientKeyFile);
handler.ClientCertificates.Add(clientCertificate);
}

// Create and return an HttpClient with the modified handler
return new HttpClient(handler);
#else
// Handle alternative methods for earlier .NET versions
throw new PlatformNotSupportedException("mTLS support requires .NET 6.0 or later.");
#endif
}

private static string GetUserAgentString()
{
var assembly = typeof(OtlpExporterOptions).Assembly;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
using LogOtlpCollector = OpenTelemetry.Proto.Collector.Logs.V1;
using MetricsOtlpCollector = OpenTelemetry.Proto.Collector.Metrics.V1;
using TraceOtlpCollector = OpenTelemetry.Proto.Collector.Trace.V1;
#if NET6_0_OR_GREATER
using System.Security.Cryptography.X509Certificates;
#endif

namespace OpenTelemetry.Exporter;

Expand All @@ -33,7 +36,41 @@ public static Channel CreateChannel(this OtlpExporterOptions options)
throw new NotSupportedException($"Endpoint URI scheme ({options.Endpoint.Scheme}) is not supported. Currently only \"http\" and \"https\" are supported.");
}

#if NETSTANDARD2_1 || NET
#if NET6_0_OR_GREATER
var handler = new HttpClientHandler();

// Set up custom certificate validation if CertificateFile is provided
if (!string.IsNullOrEmpty(options.CertificateFile))
{
var trustedCertificate = X509Certificate2.CreateFromPemFile(options.CertificateFile);
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
{
if (cert != null && chain != null)
{
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
chain.ChainPolicy.CustomTrustStore.Add(trustedCertificate);
return chain.Build(cert);
}

return false;
};
}

// Set up client certificate if provided
if (!string.IsNullOrEmpty(options.ClientCertificateFile) && !string.IsNullOrEmpty(options.ClientKeyFile))
{
var clientCertificate = X509Certificate2.CreateFromPemFile(options.ClientCertificateFile, options.ClientKeyFile);
handler.ClientCertificates.Add(clientCertificate);
}

var grpcChannelOptions = new GrpcChannelOptions
{
HttpHandler = handler,
DisposeHttpClient = true,
};

return GrpcChannel.ForAddress(options.Endpoint, grpcChannelOptions);
#elif NETSTANDARD2_1 || NET
return GrpcChannel.ForAddress(options.Endpoint);
#else
ChannelCredentials channelCredentials;
Expand Down Expand Up @@ -289,6 +326,7 @@ public static void TryEnableIHttpClientFactoryIntegration(this OtlpExporterOptio
binder: null,
new Type[] { typeof(string) },
modifiers: null);

if (createClientMethod != null)
{
HttpClient? client = (HttpClient?)createClientMethod.Invoke(httpClientFactory, new object[] { httpClientName });
Expand All @@ -297,7 +335,45 @@ public static void TryEnableIHttpClientFactoryIntegration(this OtlpExporterOptio
{
client.Timeout = TimeSpan.FromMilliseconds(options.TimeoutMilliseconds);

return client;
// Set up a new HttpClientHandler to configure certificates and callbacks
var handler = new HttpClientHandler();

#if NET6_0_OR_GREATER
// Add server certificate validation if CertificateFile is specified
if (!string.IsNullOrEmpty(options.CertificateFile))
{
var trustedCertificate = X509Certificate2.CreateFromPemFile(options.CertificateFile);
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
{
if (cert != null && chain != null)
{
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
chain.ChainPolicy.CustomTrustStore.Add(trustedCertificate);
return chain.Build(cert);
}

return false;
};
}

// Add client certificate if ClientCertificateFile and ClientKeyFile are specified
if (!string.IsNullOrEmpty(options.ClientCertificateFile) && !string.IsNullOrEmpty(options.ClientKeyFile))
{
var clientCertificate = X509Certificate2.CreateFromPemFile(options.ClientCertificateFile, options.ClientKeyFile);
handler.ClientCertificates.Add(clientCertificate);
}

// Re-create HttpClient using the custom handler
return new HttpClient(handler) { Timeout = client.Timeout };

#else
// Throw only if certificates are required but the environment is unsupported
if (!string.IsNullOrEmpty(options.CertificateFile) ||
(!string.IsNullOrEmpty(options.ClientCertificateFile) && !string.IsNullOrEmpty(options.ClientKeyFile)))
{
throw new PlatformNotSupportedException("mTLS support requires .NET 6.0 or later.");
}
#endif
}
}
}
Expand Down
Loading