-
Notifications
You must be signed in to change notification settings - Fork 847
[OTLP] Add mTLS Support for OTLP Exporter #6343
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
Open
sandy2008
wants to merge
39
commits into
open-telemetry:main
Choose a base branch
from
sandy2008:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,145
−10
Open
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
a363508
feat(): add mtls support
sandy2008 faba6ab
refactor(mtls): extract repeated string literals to constants in Otlp…
sandy2008 3044fc3
feat(): add mtls support
sandy2008 7368c9e
feat(): add mtls configurations for certs
sandy2008 c488394
fix(mtls): include filePath in FileNotFoundException for better diagn…
sandy2008 472205a
fix(mtls): use string.Equals to safely compare thumbprints and avoid …
sandy2008 2d69b7c
fix(): remove redundant comment
sandy2008 960037e
fix(): remove redundant comment
sandy2008 6226ac5
fix(): remove redundant comment
sandy2008 966d076
feat(): add support for client key w/ password
sandy2008 879cd6c
refactor(mtls): extract HttpClient default configuration to separate …
sandy2008 c1405cb
refactor(mtls): eliminate magic strings in OtlpMtlsHttpClientFactory
sandy2008 02b9240
feat(mtls): adjust error msg
sandy2008 6ae5efe
feat(mtls): adjust error msg
sandy2008 eadf9c2
feat(mtls): adjust error msg
sandy2008 2c759c6
chore(mtls): when mTLS file permission validation is skipped on unsup…
sandy2008 87633ba
chore(mtls); Remove preemptive file permission validation from mTLS
sandy2008 af26417
refactor(mtls): optimize mTLS HttpClient creation to reduce allocations
sandy2008 0bd7c5e
fix(mtls): format issue
sandy2008 487d517
fix(mtls): format issue
sandy2008 59cea03
chore(mtls): Make OtlpMtlsOptions sealed
sandy2008 b80cd21
fix(mtls): format issue
sandy2008 6e78148
fix(mtls): format issue
sandy2008 0fca40b
chore(README): table inline
sandy2008 fc25a73
fix(): clean up shipped file
sandy2008 c8a5a55
chore(mtls): fix README doc
sandy2008 464201b
chore(mtls): adjust return mtlsClient to 1-liner
sandy2008 ec0db21
chore(mtls): simplify new OtlpMtlsOptions()
sandy2008 84764e8
refactor(mtls): simplify null check and add comment for ExcludeRoot d…
sandy2008 3ec30f9
fix(mtls): Fix client certificate loading when ClientKeyPath is not p…
sandy2008 f30b6f8
chore(mtls): Improve certificate disposal in OtlpMtlsHttpClientFactory
sandy2008 b948ce1
fix(mtls): Add missing using Xunit statements to mTLS test files
sandy2008 3d75694
fix(mtls): suppress SYSLIB0057 warnings in certificate loading code
sandy2008 4d8b83e
fix(mtls): fix test
sandy2008 3611145
chore(): replace #if NET
sandy2008 3f7a6bb
chore(mtls): remove needless pragma warning restore SYSLIB0057
sandy2008 6135c33
fix(): Remove support for client key password and certificate revocat…
sandy2008 e8f122f
Merge branch 'main' into main
sandy2008 cabc172
Merge branch 'main' into main
sandy2008 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
297 changes: 297 additions & 0 deletions
297
...OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,297 @@ | ||
// Copyright The OpenTelemetry Authors | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
#if NET | ||
|
||
using System.Net.Security; | ||
using System.Security.Cryptography; | ||
using System.Security.Cryptography.X509Certificates; | ||
|
||
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; | ||
|
||
/// <summary> | ||
/// Manages certificate loading, validation, and security checks for mTLS connections. | ||
/// </summary> | ||
internal static class OtlpMtlsCertificateManager | ||
{ | ||
internal const string CaCertificateType = "CA certificate"; | ||
internal const string ClientCertificateType = "Client certificate"; | ||
internal const string ClientPrivateKeyType = "Client private key"; | ||
|
||
/// <summary> | ||
/// Loads a CA certificate from a PEM file. | ||
/// </summary> | ||
/// <param name="caCertificatePath">Path to the CA certificate file.</param> | ||
/// <returns>The loaded CA certificate.</returns> | ||
/// <exception cref="FileNotFoundException">Thrown when the certificate file is not found.</exception> | ||
/// <exception cref="InvalidOperationException">Thrown when the certificate cannot be loaded.</exception> | ||
public static X509Certificate2 LoadCaCertificate(string caCertificatePath) | ||
{ | ||
ValidateFileExists(caCertificatePath, CaCertificateType); | ||
|
||
try | ||
{ | ||
var caCertificate = X509Certificate2.CreateFromPemFile(caCertificatePath); | ||
|
||
OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoaded( | ||
CaCertificateType, | ||
caCertificatePath); | ||
|
||
return caCertificate; | ||
} | ||
catch (Exception ex) | ||
{ | ||
OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoadFailed( | ||
CaCertificateType, | ||
caCertificatePath, | ||
ex.Message); | ||
throw new InvalidOperationException( | ||
$"Failed to load CA certificate from '{caCertificatePath}': {ex.Message}", | ||
ex); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Loads a client certificate from a single file (e.g., PKCS#12 format) or from separate certificate and key files. | ||
/// </summary> | ||
/// <param name="clientCertificatePath">Path to the client certificate file.</param> | ||
/// <param name="clientKeyPath">Path to the client private key file. Can be null for single-file certificates.</param> | ||
/// <returns>The loaded client certificate with private key.</returns> | ||
/// <exception cref="FileNotFoundException">Thrown when the certificate file is not found.</exception> | ||
/// <exception cref="InvalidOperationException">Thrown when the certificate cannot be loaded.</exception> | ||
/// <exception cref="ArgumentException">Thrown when clientKeyPath is not null for single-file certificate loading.</exception> | ||
public static X509Certificate2 LoadClientCertificate( | ||
string clientCertificatePath, | ||
string? clientKeyPath) | ||
{ | ||
if (clientKeyPath == null) | ||
{ | ||
// Load certificate from a single file (e.g., PKCS#12 format) | ||
ValidateFileExists(clientCertificatePath, ClientCertificateType); | ||
|
||
try | ||
{ | ||
X509Certificate2 clientCertificate; | ||
|
||
// Try to load as PKCS#12 first, then as PEM | ||
try | ||
{ | ||
#if NET9_0_OR_GREATER | ||
clientCertificate = X509CertificateLoader.LoadPkcs12FromFile(clientCertificatePath, (string?)null); | ||
#else | ||
clientCertificate = new X509Certificate2(clientCertificatePath); | ||
#endif | ||
} | ||
catch (Exception ex) when (ex is CryptographicException || ex is InvalidDataException || ex is FormatException) | ||
{ | ||
// If PKCS#12 fails, try PEM format | ||
clientCertificate = X509Certificate2.CreateFromPemFile(clientCertificatePath); | ||
} | ||
|
||
if (!clientCertificate.HasPrivateKey) | ||
{ | ||
throw new InvalidOperationException( | ||
"Client certificate does not have an associated private key."); | ||
} | ||
|
||
OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoaded( | ||
ClientCertificateType, | ||
clientCertificatePath); | ||
|
||
return clientCertificate; | ||
} | ||
catch (Exception ex) | ||
{ | ||
OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoadFailed( | ||
ClientCertificateType, | ||
clientCertificatePath, | ||
ex.Message); | ||
throw new InvalidOperationException( | ||
$"Failed to load client certificate from '{clientCertificatePath}': {ex.Message}", | ||
ex); | ||
} | ||
} | ||
|
||
// Load certificate and key from separate files | ||
ValidateFileExists(clientCertificatePath, ClientCertificateType); | ||
ValidateFileExists(clientKeyPath, ClientPrivateKeyType); | ||
|
||
try | ||
{ | ||
X509Certificate2 clientCertificate = X509Certificate2.CreateFromPemFile( | ||
clientCertificatePath, | ||
clientKeyPath); | ||
|
||
if (!clientCertificate.HasPrivateKey) | ||
{ | ||
throw new InvalidOperationException( | ||
"Client certificate does not have an associated private key."); | ||
} | ||
|
||
OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoaded( | ||
ClientCertificateType, | ||
clientCertificatePath); | ||
|
||
return clientCertificate; | ||
} | ||
catch (Exception ex) | ||
{ | ||
OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoadFailed( | ||
ClientCertificateType, | ||
clientCertificatePath, | ||
ex.Message); | ||
throw new InvalidOperationException( | ||
$"Failed to load client certificate from '{clientCertificatePath}' and key from '{clientKeyPath}': {ex.Message}", | ||
ex); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Validates the certificate chain for a given certificate. | ||
/// </summary> | ||
/// <param name="certificate">The certificate to validate.</param> | ||
/// <param name="certificateType">Type description for logging (e.g., "Client certificate").</param> | ||
/// <returns>True if the certificate chain is valid; otherwise, false.</returns> | ||
public static bool ValidateCertificateChain( | ||
X509Certificate2 certificate, | ||
string certificateType) | ||
{ | ||
try | ||
{ | ||
using var chain = new X509Chain(); | ||
|
||
// Configure chain policy | ||
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; | ||
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online; | ||
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot; | ||
sandy2008 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
bool isValid = chain.Build(certificate); | ||
|
||
if (!isValid) | ||
{ | ||
var errors = chain | ||
.ChainStatus.Where(status => status.Status != X509ChainStatusFlags.NoError) | ||
.Select(status => $"{status.Status}: {status.StatusInformation}") | ||
.ToArray(); | ||
|
||
OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateChainValidationFailed( | ||
certificateType, | ||
certificate.Subject, | ||
string.Join("; ", errors)); | ||
|
||
// Check if certificate is expired - this should throw an exception | ||
bool isExpired = chain.ChainStatus.Any(status => | ||
status.Status == X509ChainStatusFlags.NotTimeValid || | ||
status.Status == X509ChainStatusFlags.NotTimeNested); | ||
|
||
if (isExpired) | ||
{ | ||
throw new InvalidOperationException( | ||
$"Certificate chain validation failed for {certificateType}: Certificate is expired. " + | ||
$"Errors: {string.Join("; ", errors)}"); | ||
} | ||
|
||
return false; | ||
} | ||
|
||
OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateChainValidated( | ||
certificateType, | ||
certificate.Subject); | ||
return true; | ||
} | ||
catch (Exception ex) | ||
{ | ||
OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateChainValidationFailed( | ||
certificateType, | ||
certificate.Subject, | ||
ex.Message); | ||
|
||
return false; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Validates a server certificate against the provided CA certificate. | ||
/// </summary> | ||
/// <param name="serverCert">The server certificate to validate.</param> | ||
/// <param name="chain">The certificate chain.</param> | ||
/// <param name="sslPolicyErrors">The SSL policy errors.</param> | ||
/// <param name="caCertificate">The CA certificate to validate against.</param> | ||
/// <returns>True if the certificate is valid; otherwise, false.</returns> | ||
internal static bool ValidateServerCertificate( | ||
X509Certificate2 serverCert, | ||
X509Chain chain, | ||
SslPolicyErrors sslPolicyErrors, | ||
X509Certificate2 caCertificate) | ||
{ | ||
try | ||
{ | ||
// If there are no SSL policy errors, accept the certificate | ||
if (sslPolicyErrors == SslPolicyErrors.None) | ||
{ | ||
return true; | ||
} | ||
|
||
// If the only error is an untrusted root, validate against our CA | ||
if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors) | ||
{ | ||
// Add our CA certificate to the chain | ||
chain.ChainPolicy.ExtraStore.Add(caCertificate); | ||
chain.ChainPolicy.VerificationFlags = | ||
X509VerificationFlags.AllowUnknownCertificateAuthority; | ||
|
||
bool isValid = chain.Build(serverCert); | ||
|
||
if (isValid) | ||
{ | ||
// Verify that the chain terminates with our CA | ||
var rootCert = chain.ChainElements[^1].Certificate; | ||
if ( | ||
string.Equals( | ||
rootCert.Thumbprint, | ||
caCertificate.Thumbprint, | ||
StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
OpenTelemetryProtocolExporterEventSource.Log.MtlsServerCertificateValidated( | ||
serverCert.Subject); | ||
return true; | ||
} | ||
} | ||
} | ||
|
||
OpenTelemetryProtocolExporterEventSource.Log.MtlsServerCertificateValidationFailed( | ||
serverCert.Subject, | ||
sslPolicyErrors.ToString()); | ||
|
||
return false; | ||
} | ||
catch (Exception ex) | ||
{ | ||
OpenTelemetryProtocolExporterEventSource.Log.MtlsServerCertificateValidationFailed( | ||
serverCert.Subject, | ||
ex.Message); | ||
|
||
return false; | ||
} | ||
} | ||
|
||
private static void ValidateFileExists(string filePath, string fileType) | ||
{ | ||
if (string.IsNullOrEmpty(filePath)) | ||
{ | ||
throw new ArgumentException( | ||
$"{fileType} path cannot be null or empty.", | ||
nameof(filePath)); | ||
} | ||
|
||
if (!File.Exists(filePath)) | ||
{ | ||
OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateFileNotFound( | ||
fileType, | ||
filePath); | ||
throw new FileNotFoundException($"{fileType} file not found at path: {filePath}", filePath); | ||
} | ||
} | ||
} | ||
|
||
#endif |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.