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

Open
wants to merge 46 commits into
base: main
Choose a base branch
from

Conversation

sandy2008
Copy link
Contributor

@sandy2008 sandy2008 commented Oct 23, 2024

Fixes #2009
Based on PR

Changes

This pull request introduces support for mutual TLS (mTLS) in the OpenTelemetry Protocol (OTLP) Exporter, allowing secure gRPC connections over HTTPS and mTLS supports for HTTP Protocol.

Key Changes:

  • Certificate file. OTEL_EXPORTER_OTLP_CERTIFICATE
  • Client key file. OTEL_EXPORTER_OTLP_CLIENT_KEY
  • Client certificate file. OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE

Merge requirement checklist

  • CONTRIBUTING guidelines followed (license requirements, nullable enabled, static analysis, etc.)
  • Unit tests added/updated
  • Appropriate CHANGELOG.md files updated for non-trivial changes
  • Changes in public API reviewed (if applicable)

@github-actions github-actions bot added the pkg:OpenTelemetry.Exporter.OpenTelemetryProtocol Issues related to OpenTelemetry.Exporter.OpenTelemetryProtocol NuGet package label Oct 23, 2024
@sandy2008 sandy2008 changed the title feat(): Add mTLS Support for OTLP Exporter [otlp] Add mTLS Support for OTLP Exporter Oct 23, 2024
@@ -28,6 +28,26 @@ protected override HttpContent CreateHttpContent(OtlpCollector.ExportTraceServic
return new ExportRequestContent(exportRequest);
}

private static HttpClient ModifyHttpClient(OtlpExporterOptions options, HttpClient httpClient)
Copy link
Member

Choose a reason for hiding this comment

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

We can't just throw away the HttpClient and make a new one. Users are able to configure SSL/TLS today using factory:

This change will break any user doing that or doing anything else to the HttpClient they are intending to use here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I modified a little bit, does it look fine? @CodeBlanch

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the design looks ok, I will start to write tests :)

Copy link

codecov bot commented Oct 27, 2024

Codecov Report

Attention: Patch coverage is 73.07692% with 35 lines in your changes missing coverage. Please review.

Project coverage is 85.12%. Comparing base (84e6afb) to head (2006fbf).
Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
...orter.OpenTelemetryProtocol/OtlpExporterOptions.cs 68.65% 21 Missing ⚠️
...TelemetryProtocol/OtlpExporterOptionsExtensions.cs 77.77% 14 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #5918      +/-   ##
==========================================
- Coverage   85.15%   85.12%   -0.03%     
==========================================
  Files         272      272              
  Lines       12420    12548     +128     
==========================================
+ Hits        10576    10682     +106     
- Misses       1844     1866      +22     
Flag Coverage Δ
unittests-Project-Experimental 85.06% <73.07%> (-0.09%) ⬇️
unittests-Project-Stable 85.03% <73.07%> (-0.11%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...TelemetryProtocol/OtlpExporterOptionsExtensions.cs 85.77% <77.77%> (-2.76%) ⬇️
...orter.OpenTelemetryProtocol/OtlpExporterOptions.cs 87.50% <68.65%> (-11.59%) ⬇️

... and 5 files with indirect coverage changes

@sandy2008
Copy link
Contributor Author

Just for your info, AddCertificatesToHttpClient is not needed, so will delete it in the next commit.
@alanwest @CodeBlanch @sokoide Does other changes look ok?

@rajkumar-rangaraj
Copy link
Contributor

Just for your info, AddCertificatesToHttpClient is not needed, so will delete it in the next commit. @alanwest @CodeBlanch @sokoide Does other changes look ok?

@sandy2008 I recommend putting this on hold for a month. We are working on a custom serialization (#5730) and also we plan to handle GRPC without relying on Grpc.Net packages and removing dependencies on Google Protobuf and GRPC libraries. This will impact the part where you are adding support for mTLS.

@sandy2008
Copy link
Contributor Author

Just for your info, AddCertificatesToHttpClient is not needed, so will delete it in the next commit. @alanwest @CodeBlanch @sokoide Does other changes look ok?

@sandy2008 I recommend putting this on hold for a month. We are working on a custom serialization (#5730) and also we plan to handle GRPC without relying on Grpc.Net packages and removing dependencies on Google Protobuf and GRPC libraries. This will impact the part where you are adding support for mTLS.

Hi :) @rajkumar-rangaraj
Since there is also http part as well, shall I change this PR to support HTTP first, and add GRPC support afterwards?

@rajkumar-rangaraj
Copy link
Contributor

Since there is also http part as well, shall I change this PR to support HTTP first, and add GRPC support afterwards?

Today, we had a discussion on this topic in the SIG meeting. We all feel it is better to hold off on this work as this space is expected to change drastically in the coming weeks. We thought it would be beneficial if you could join the next SIG meeting to discuss it further.

@sandy2008
Copy link
Contributor Author

Since there is also http part as well, shall I change this PR to support HTTP first, and add GRPC support afterwards?

Today, we had a discussion on this topic in the SIG meeting. We all feel it is better to hold off on this work as this space is expected to change drastically in the coming weeks. We thought it would be beneficial if you could join the next SIG meeting to discuss it further.

Thank you for the update. @rajkumar-rangaraj

Could you please let me know the time and participation details for the next SIG meeting? I think it’s important for @sokoide , as a co-author, to attend as well, since I might not be able to cover everything alone.

@rajkumar-rangaraj
Copy link
Contributor

Could you please let me know the time and participation details for the next SIG meeting? I think it’s important for @sokoide , as a co-author, to attend as well, since I might not be able to cover everything alone.

Please check the details here - https://github.com/open-telemetry/opentelemetry-dotnet?tab=readme-ov-file#contributing

@sandy2008
Copy link
Contributor Author

Could you please let me know the time and participation details for the next SIG meeting? I think it’s important for @sokoide , as a co-author, to attend as well, since I might not be able to cover everything alone.

Please check the details here - https://github.com/open-telemetry/opentelemetry-dotnet?tab=readme-ov-file#contributing

Thank you for the details! We’ll join the SIG meeting scheduled for November 5 at 16:00 PT (November 6, 11:00 JST). Looking forward to it!

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

@sandy2008
Copy link
Contributor Author

sandy2008 commented Nov 8, 2024

@rajkumar-rangaraj @alanwest

    public static void TryEnableIHttpClientFactoryIntegration(this OtlpExporterOptions options, IServiceProvider serviceProvider, string httpClientName)
    {
        if (serviceProvider != null
            && options.Protocol == OtlpExportProtocol.HttpProtobuf
            && options.HttpClientFactory == options.DefaultHttpClientFactory)
        {
            options.HttpClientFactory = () =>
            {
                Type? httpClientFactoryType = Type.GetType("System.Net.Http.IHttpClientFactory, Microsoft.Extensions.Http", throwOnError: false);
                if (httpClientFactoryType != null)
                {
                    object? httpClientFactory = serviceProvider.GetService(httpClientFactoryType);
                    if (httpClientFactory != null)
                    {
                        MethodInfo? createClientMethod = httpClientFactoryType.GetMethod(
                            "CreateClient",
                            BindingFlags.Public | BindingFlags.Instance,
                            binder: null,
                            new Type[] { typeof(string) },
                            modifiers: null);

                        if (createClientMethod != null)
                        {
                            HttpClient? client = (HttpClient?)createClientMethod.Invoke(httpClientFactory, new object[] { httpClientName });

                            if (client != null)
                            {
                                client.Timeout = TimeSpan.FromMilliseconds(options.TimeoutMilliseconds);

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

    #if NET6_0_OR_GREATER
                                // Add server certificate validation
                                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 (!string.IsNullOrEmpty(options.ClientCertificateFile) && !string.IsNullOrEmpty(options.ClientKeyFile))
                                {
                                    var clientCertificate = X509Certificate2.CreateFromPemFile(options.ClientCertificateFile, options.ClientKeyFile);
                                    handler.ClientCertificates.Add(clientCertificate);
                                }
    #else
                                throw new PlatformNotSupportedException("mTLS support requires .NET 6.0 or later.");
    #endif

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

                return options.();
            };
        }
    }

Something like this would work?

@rajkumar-rangaraj
Copy link
Contributor

@sandy2008 Sorry, I don't have the bandwidth to review this now. As we discussed in the SIG, I will get to this part once the dependencies on Google.Protobuf and Grpc.Net are removed. Additionally, until we have a solution in OpenTelemetry, you could implement a custom solution for HTTP/protobuf.

@sandy2008
Copy link
Contributor Author

@rajkumar-rangaraj Hi! Does the correct logics look good?

@rajkumar-rangaraj rajkumar-rangaraj requested review from Copilot and removed request for psmulovics February 25, 2025 02:47

Choose a reason for hiding this comment

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

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

Comments suppressed due to low confidence (2)

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs:39

  • Reading certificate files using File.ReadAllText without any error handling may lead to unhandled exceptions if the file is missing or inaccessible. Consider adding try/catch blocks or validating file existence before reading.
if (!string.IsNullOrEmpty(options.ClientCertificateFile) && !string.IsNullOrEmpty(options.ClientKeyFile))

test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs:230

  • The mTLS branch is not exercised by any test cases since none of the InlineData provide a true value for 'useMtls'. Consider adding an InlineData test case that explicitly sets useMtls to true.
if (useMtls)
@sandy2008
Copy link
Contributor Author

I will fix the comments by Copilot first :) @rajkumar-rangaraj Thank you!

@rajkumar-rangaraj
Copy link
Contributor

@rajkumar-rangaraj Hi! Does the correct logics look good?

Could you also update the documentation on how it can be used? Additionally, updating the PR description with testing steps for the OpenTelemetry Collector would be helpful.

@@ -0,0 +1,6 @@
OpenTelemetry.Exporter.OtlpExporterOptions.CertificateFile.get -> string!
Copy link
Contributor

Choose a reason for hiding this comment

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

Shall we keep this internal and rely only on the environment variable? If there is a request, we can work on exposing these as public APIs.

client.Timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds);
return client;
#else
// For earlier .NET versions
Copy link
Contributor

Choose a reason for hiding this comment

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

This is incorrect; only the latest .NET versions use HttpClient. .NET Framework and older versions of .NET Core still rely on Grpc.Core.

if (!string.IsNullOrEmpty(this.CertificateFile))
{
// Load the certificate from the file
var trustedCertificate = X509Certificate2.CreateFromPemFile(this.CertificateFile);
Copy link
Contributor

Choose a reason for hiding this comment

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

The implementation does not verify if CertificateFile, ClientCertificateFile, or ClientKeyFile exist before attempting to load them.

@@ -220,6 +255,41 @@ internal OtlpExporterOptions ApplyDefaults(OtlpExporterOptions defaultExporterOp
return this;
}

#if NET6_0_OR_GREATER
internal HttpClient AddCertificatesToHttpClient(HttpClientHandler handler)
Copy link
Contributor

Choose a reason for hiding this comment

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

How you plan to handle errors within this method?

{
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
chain.ChainPolicy.CustomTrustStore.Add(trustedCertificate);
return chain.Build(cert);
Copy link
Contributor

Choose a reason for hiding this comment

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

chain.Build(cert) might fail if the certificate chain is incomplete or missing necessary intermediate certificates. Consider checking chain.ChainStatus for errors before returning true. Also, logging failures can help diagnose why validation failed

var trustedCertificate = X509Certificate2.CreateFromPemFile(this.CertificateFile);

// Set custom server certificate validation callback
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Using a custom server certificate validation callback can introduce security risks if not handled properly. Can you list what measures have been taken to mitigate potential risks, such as unintended trust, proper error handling, revocation checks, and restricting trust scope?

// Add client certificate if both files are provided
if (!string.IsNullOrEmpty(this.ClientCertificateFile) && !string.IsNullOrEmpty(this.ClientKeyFile))
{
var clientCertificate = X509Certificate2.CreateFromPemFile(this.ClientCertificateFile, this.ClientKeyFile);
Copy link
Contributor

Choose a reason for hiding this comment

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

Loading a client certificate from a PEM file with a private key should be done cautiously. How do you ensure or inform customers that the private key file should be stored securely and access-restricted?

Copy link
Contributor

@rajkumar-rangaraj rajkumar-rangaraj left a comment

Choose a reason for hiding this comment

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

This PR introduces potential security risks, particularly in certificate validation and private key handling, which need further clarification. There are concerns about trusting unverified certificates and missing security measures for private key storage. Before proceeding, clear guidance on mitigating these risks and best practices for secure implementation should be provided. Until these concerns are addressed, I am blocking this PR to ensure security is not compromised. Looking forward to further clarifications.

@sandy2008
Copy link
Contributor Author

var clientCertificate = X509Certificate2.CreateFromPemFile(this.ClientCertificateFile, this.ClientKeyFile);

Thank you for your reply! Will review on security :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg:OpenTelemetry.Exporter.OpenTelemetryProtocol Issues related to OpenTelemetry.Exporter.OpenTelemetryProtocol NuGet package
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add OTLP Exporter TLS/mTLS configuration options
4 participants