Skip to content

Commit 31df0ea

Browse files
committed
feat: add TLS-only support
1 parent 5ae24fa commit 31df0ea

File tree

8 files changed

+521
-92
lines changed

8 files changed

+521
-92
lines changed

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,5 +325,28 @@ internal void MtlsHttpClientCreationFailed(Exception ex)
325325
Level = EventLevel.Error)]
326326
internal void MtlsHttpClientCreationFailed(string exception) =>
327327
this.WriteEvent(34, exception);
328+
329+
[Event(
330+
35,
331+
Message = "CA configured for server validation. Subject: '{0}'.",
332+
Level = EventLevel.Informational)]
333+
internal void CaCertificateConfigured(string subject) =>
334+
this.WriteEvent(35, subject);
335+
336+
[NonEvent]
337+
internal void SecureHttpClientCreationFailed(Exception ex)
338+
{
339+
if (Log.IsEnabled(EventLevel.Error, EventKeywords.All))
340+
{
341+
this.SecureHttpClientCreationFailed(ex.ToInvariantString());
342+
}
343+
}
344+
345+
[Event(
346+
36,
347+
Message = "Failed to create secure HttpClient. Exception: {0}",
348+
Level = EventLevel.Error)]
349+
internal void SecureHttpClientCreationFailed(string exception) =>
350+
this.WriteEvent(36, exception);
328351
#endif
329352
}

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpCertificateManager.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@
1010
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
1111

1212
/// <summary>
13-
/// Manages certificate loading, validation, and security checks for mTLS connections.
13+
/// Manages certificate loading, validation, and security checks for TLS connections.
1414
/// </summary>
15+
/// <remarks>
16+
/// This class provides functionality for both simple server certificate trust
17+
/// (for self-signed certificates) and mTLS client authentication scenarios.
18+
/// </remarks>
1519
internal static class OtlpCertificateManager
1620
{
17-
internal const string CaCertificateType = "CA certificate";
21+
internal const string CaCertificateType = "CA Certificate";
1822
internal const string ClientCertificateType = "Client certificate";
1923
internal const string ClientPrivateKeyType = "Client private key";
2024

@@ -218,6 +222,10 @@ public static bool ValidateCertificateChain(
218222
/// <param name="sslPolicyErrors">The SSL policy errors.</param>
219223
/// <param name="caCertificate">The CA certificate to validate against.</param>
220224
/// <returns>True if the certificate is valid; otherwise, false.</returns>
225+
/// <remarks>
226+
/// This method is used to validate server certificates against a CA.
227+
/// Common use case: connecting to a server with a self-signed certificate.
228+
/// </remarks>
221229
internal static bool ValidateServerCertificate(
222230
X509Certificate2 serverCert,
223231
X509Chain chain,

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSecureHttpClientFactory.cs

Lines changed: 111 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -8,66 +8,60 @@
88
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
99

1010
/// <summary>
11-
/// Factory for creating HttpClient instances configured with mTLS settings.
11+
/// Factory for creating HttpClient instances configured with TLS settings.
1212
/// </summary>
1313
internal static class OtlpSecureHttpClientFactory
1414
{
1515
/// <summary>
16-
/// Creates an HttpClient configured with mTLS settings.
16+
/// Creates an HttpClient configured with TLS settings based on the provided options.
1717
/// </summary>
18-
/// <param name="mtlsOptions">The mTLS configuration options.</param>
18+
/// <param name="tlsOptions">The TLS configuration options.</param>
1919
/// <param name="configureClient">Optional action to configure the client.</param>
20-
/// <returns>An HttpClient configured for mTLS.</returns>
21-
/// <exception cref="ArgumentNullException">Thrown when <paramref name="mtlsOptions"/> is null.</exception>
22-
/// <exception cref="InvalidOperationException">Thrown when mTLS is not enabled.</exception>
20+
/// <returns>An HttpClient configured for secure communication.</returns>
21+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="tlsOptions"/> is null.</exception>
22+
/// <exception cref="InvalidOperationException">Thrown when TLS is not enabled.</exception>
2323
public static HttpClient CreateSecureHttpClient(
24-
OtlpMtlsOptions mtlsOptions,
24+
OtlpTlsOptions tlsOptions,
2525
Action<HttpClient>? configureClient = null)
2626
{
27-
ArgumentNullException.ThrowIfNull(mtlsOptions);
27+
ArgumentNullException.ThrowIfNull(tlsOptions);
2828

29-
if (!mtlsOptions.IsEnabled)
29+
if (!tlsOptions.IsTlsEnabled && !tlsOptions.IsMtlsEnabled)
3030
{
31-
throw new InvalidOperationException("mTLS options must include a client or CA certificate path.");
31+
throw new InvalidOperationException(
32+
"TLS options must include at least a CA path or client certificate path.");
3233
}
3334

34-
HttpClientHandler? handler = null;
3535
X509Certificate2? caCertificate = null;
3636
X509Certificate2? clientCertificate = null;
37+
TlsHttpClientHandler? handler = null;
3738

3839
try
3940
{
40-
// Load certificates
41-
if (!string.IsNullOrEmpty(mtlsOptions.CaCertificatePath))
41+
if (!string.IsNullOrEmpty(tlsOptions.CaCertificatePath))
4242
{
4343
caCertificate = OtlpCertificateManager.LoadCaCertificate(
44-
mtlsOptions.CaCertificatePath);
44+
tlsOptions.CaCertificatePath);
4545

46-
if (mtlsOptions.EnableCertificateChainValidation)
46+
if (tlsOptions.EnableCertificateChainValidation)
4747
{
4848
OtlpCertificateManager.ValidateCertificateChain(
4949
caCertificate,
5050
OtlpCertificateManager.CaCertificateType);
5151
}
5252
}
5353

54-
if (!string.IsNullOrEmpty(mtlsOptions.ClientCertificatePath))
54+
if (tlsOptions is OtlpMtlsOptions mtlsOptions && mtlsOptions.IsMtlsEnabled)
5555
{
56-
if (string.IsNullOrEmpty(mtlsOptions.ClientKeyPath))
57-
{
58-
// Load certificate without separate key file (e.g., PKCS#12 format)
59-
clientCertificate = OtlpCertificateManager.LoadClientCertificate(
60-
mtlsOptions.ClientCertificatePath,
61-
null);
62-
}
63-
else
64-
{
65-
clientCertificate = OtlpCertificateManager.LoadClientCertificate(
66-
mtlsOptions.ClientCertificatePath,
56+
clientCertificate = string.IsNullOrEmpty(mtlsOptions.ClientKeyPath)
57+
? OtlpCertificateManager.LoadClientCertificate(
58+
mtlsOptions.ClientCertificatePath!,
59+
null)
60+
: OtlpCertificateManager.LoadClientCertificate(
61+
mtlsOptions.ClientCertificatePath!,
6762
mtlsOptions.ClientKeyPath);
68-
}
6963

70-
if (mtlsOptions.EnableCertificateChainValidation)
64+
if (tlsOptions.EnableCertificateChainValidation)
7165
{
7266
OtlpCertificateManager.ValidateCertificateChain(
7367
clientCertificate,
@@ -77,89 +71,133 @@ public static HttpClient CreateSecureHttpClient(
7771
OpenTelemetryProtocolExporterEventSource.Log.MtlsConfigurationEnabled(
7872
clientCertificate.Subject);
7973
}
74+
else if (caCertificate != null)
75+
{
76+
OpenTelemetryProtocolExporterEventSource.Log.CaCertificateConfigured(
77+
caCertificate.Subject);
78+
}
8079

81-
// Create HttpClientHandler with mTLS configuration
80+
// Create HttpClientHandler and apply TLS configuration
8281
#pragma warning disable CA2000 // Dispose objects before losing scope - HttpClientHandler is disposed by HttpClient
83-
handler = new MtlsHttpClientHandler(clientCertificate, caCertificate);
82+
handler = new TlsHttpClientHandler(caCertificate, clientCertificate);
8483
#pragma warning restore CA2000
85-
handler.CheckCertificateRevocationList = true;
8684

87-
// Handler now owns the certificates and will dispose them when disposed.
85+
// Handler now owns certificates
8886
caCertificate = null;
8987
clientCertificate = null;
9088

89+
#pragma warning disable CA5399 // CheckCertificateRevocationList is set in ConfigureTls.
9190
var client = new HttpClient(handler, disposeHandler: true);
91+
#pragma warning restore CA5399
9292

9393
configureClient?.Invoke(client);
9494

9595
return client;
9696
}
9797
catch (Exception ex)
9898
{
99-
// Dispose handler if something went wrong
99+
// Clean up on failure
100100
handler?.Dispose();
101-
102-
OpenTelemetryProtocolExporterEventSource.Log.MtlsHttpClientCreationFailed(ex);
103-
throw;
104-
}
105-
finally
106-
{
107-
// Dispose certificates as they are no longer needed after being added to the handler
108101
caCertificate?.Dispose();
109102
clientCertificate?.Dispose();
103+
104+
OpenTelemetryProtocolExporterEventSource.Log.SecureHttpClientCreationFailed(ex);
105+
throw;
110106
}
111107
}
112108

113-
private sealed class MtlsHttpClientHandler : HttpClientHandler
109+
/// <summary>
110+
/// Creates an HttpClient configured with mTLS settings.
111+
/// </summary>
112+
/// <param name="mtlsOptions">The mTLS configuration options.</param>
113+
/// <param name="configureClient">Optional action to configure the client.</param>
114+
/// <returns>An HttpClient configured for mTLS.</returns>
115+
/// <remarks>
116+
/// This method exists for backward compatibility. New code should use
117+
/// <see cref="CreateSecureHttpClient(OtlpTlsOptions, Action{HttpClient}?)"/>.
118+
/// </remarks>
119+
public static HttpClient CreateMtlsHttpClient(
120+
OtlpMtlsOptions mtlsOptions,
121+
Action<HttpClient>? configureClient = null)
122+
{
123+
return CreateSecureHttpClient(mtlsOptions, configureClient);
124+
}
125+
126+
/// <summary>
127+
/// HttpClientHandler that applies TLS configuration based on loaded certificates.
128+
/// </summary>
129+
private sealed class TlsHttpClientHandler : HttpClientHandler
114130
{
115131
private readonly X509Certificate2? caCertificate;
116132
private readonly X509Certificate2? clientCertificate;
133+
private bool disposed;
117134

118-
internal MtlsHttpClientHandler(
119-
X509Certificate2? clientCertificate,
120-
X509Certificate2? caCertificate)
135+
internal TlsHttpClientHandler(
136+
X509Certificate2? caCertificate,
137+
X509Certificate2? clientCertificate)
121138
{
122-
this.clientCertificate = clientCertificate;
123139
this.caCertificate = caCertificate;
124-
this.CheckCertificateRevocationList = true;
140+
this.clientCertificate = clientCertificate;
141+
142+
this.ConfigureTls();
143+
}
125144

126-
if (clientCertificate != null)
145+
protected override void Dispose(bool disposing)
146+
{
147+
if (disposing && !this.disposed)
127148
{
128-
this.ClientCertificates.Add(clientCertificate);
129-
this.ClientCertificateOptions = ClientCertificateOption.Manual;
149+
this.clientCertificate?.Dispose();
150+
this.caCertificate?.Dispose();
151+
this.disposed = true;
130152
}
131153

132-
if (caCertificate != null)
154+
base.Dispose(disposing);
155+
}
156+
157+
private void ConfigureTls()
158+
{
159+
this.CheckCertificateRevocationList = true;
160+
161+
this.ConfigureClientCertificate();
162+
this.ConfigureCaCertificateValidation();
163+
}
164+
165+
private void ConfigureClientCertificate()
166+
{
167+
if (this.clientCertificate == null)
133168
{
134-
this.ServerCertificateCustomValidationCallback = (
135-
httpRequestMessage,
136-
cert,
137-
chain,
138-
sslPolicyErrors) =>
139-
{
140-
if (cert == null || chain == null)
141-
{
142-
return false;
143-
}
144-
145-
return OtlpCertificateManager.ValidateServerCertificate(
146-
cert,
147-
chain,
148-
sslPolicyErrors,
149-
caCertificate);
150-
};
169+
return;
151170
}
171+
172+
this.ClientCertificates.Add(this.clientCertificate);
173+
this.ClientCertificateOptions = ClientCertificateOption.Manual;
152174
}
153175

154-
protected override void Dispose(bool disposing)
176+
private void ConfigureCaCertificateValidation()
155177
{
156-
if (disposing)
178+
if (this.caCertificate == null)
157179
{
158-
this.caCertificate?.Dispose();
159-
this.clientCertificate?.Dispose();
180+
return;
160181
}
161182

162-
base.Dispose(disposing);
183+
var caCert = this.caCertificate;
184+
this.ServerCertificateCustomValidationCallback = (
185+
httpRequestMessage,
186+
cert,
187+
chain,
188+
sslPolicyErrors) =>
189+
{
190+
if (cert == null || chain == null)
191+
{
192+
return false;
193+
}
194+
195+
return OtlpCertificateManager.ValidateServerCertificate(
196+
cert,
197+
chain,
198+
sslPolicyErrors,
199+
caCert);
200+
};
163201
}
164202
}
165203
}

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ internal OtlpExporterOptions(
7575
var timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds);
7676

7777
#if NET
78-
// If mTLS is configured, create an mTLS-enabled client
78+
// If TLS configuration is enabled (mTLS or CA only), create a secure client
7979
if (this.MtlsOptions?.IsEnabled == true)
8080
{
8181
return OtlpSecureHttpClientFactory.CreateSecureHttpClient(

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,48 @@
55

66
namespace OpenTelemetry.Exporter;
77

8-
internal sealed class OtlpMtlsOptions
8+
/// <summary>
9+
/// Represents mTLS (mutual TLS) configuration options for OTLP exporter.
10+
/// Extends <see cref="OtlpTlsOptions"/> with client certificate authentication.
11+
/// </summary>
12+
/// <remarks>
13+
/// mTLS is an authentication system in which both the client and server authenticate each other.
14+
/// This class provides client certificate configuration for scenarios requiring mutual authentication.
15+
/// For simple server certificate trust (e.g., self-signed certificates), use <see cref="OtlpTlsOptions"/> directly.
16+
/// </remarks>
17+
internal sealed class OtlpMtlsOptions : OtlpTlsOptions
918
{
10-
/// <summary>
11-
/// Gets or sets the path to the CA certificate file in PEM format.
12-
/// </summary>
13-
public string? CaCertificatePath { get; set; }
14-
1519
/// <summary>
1620
/// Gets or sets the path to the client certificate file in PEM format.
1721
/// </summary>
22+
/// <remarks>
23+
/// Corresponds to the OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE environment variable.
24+
/// This is used for client authentication in mTLS scenarios.
25+
/// </remarks>
1826
public string? ClientCertificatePath { get; set; }
1927

2028
/// <summary>
2129
/// Gets or sets the path to the client private key file in PEM format.
2230
/// </summary>
31+
/// <remarks>
32+
/// Corresponds to the OTEL_EXPORTER_OTLP_CLIENT_KEY environment variable.
33+
/// Required when the client certificate file does not include the private key.
34+
/// </remarks>
2335
public string? ClientKeyPath { get; set; }
2436

2537
/// <summary>
26-
/// Gets or sets a value indicating whether to enable certificate chain validation.
27-
/// When enabled, the exporter will validate the certificate chain and reject invalid certificates.
38+
/// Gets a value indicating whether mTLS (mutual TLS) is enabled.
2839
/// </summary>
29-
public bool EnableCertificateChainValidation { get; set; } = true;
40+
/// <remarks>
41+
/// Returns true when client certificate is configured for mutual authentication.
42+
/// Note: Having only <see cref="OtlpTlsOptions.CaCertificatePath"/> does not constitute mTLS.
43+
/// </remarks>
44+
public override bool IsMtlsEnabled =>
45+
!string.IsNullOrWhiteSpace(this.ClientCertificatePath);
3046

3147
/// <summary>
32-
/// Gets a value indicating whether mTLS is enabled.
33-
/// mTLS is considered enabled if at least the client certificate path or CA certificate path is provided.
48+
/// Gets a value indicating whether any TLS configuration is enabled.
49+
/// TLS is considered enabled if at least the client certificate path or CA path is provided.
3450
/// </summary>
3551
public bool IsEnabled =>
3652
!string.IsNullOrWhiteSpace(this.ClientCertificatePath)

0 commit comments

Comments
 (0)