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

Fix #197 - AWSEKSResourceDetector fails to detect resources due to exception "The SSL connection could not be established" #208

Merged
merged 16 commits into from
May 12, 2022
Merged
Show file tree
Hide file tree
Changes from 15 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 @@ -62,6 +62,12 @@ public void FailedToExtractResourceAttributes(string format, string exception)
this.WriteEvent(3, format, exception);
}

[Event(4, Message = "Failed to validate certificate in format: '{0}', error: '{1}'.", Level = EventLevel.Warning)]
public void FailedToValidateCertificate(string format, string error)
{
this.WriteEvent(4, format, error);
}

/// <summary>
/// Returns a culture-independent string representation of the given <paramref name="exception"/> object,
/// appropriate for diagnostics tracing.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net452;netstandard2.0</TargetFrameworks>
<Description>OpenTelemetry extensions for AWS X-Ray.</Description>
Expand All @@ -15,6 +15,8 @@
<Compile Remove="Resources\AWSECSResourceDetector.cs" />
<Compile Remove="Resources\AWSEKSResourceDetector.cs" />
<Compile Remove="Resources\AWSLambdaResourceDetector.cs" />
<Compile Remove="Resources\Http\Handler.cs" />
swetharavichandrancisco marked this conversation as resolved.
Show resolved Hide resolved
<Compile Remove="Resources\Http\ServerCertificateValidationProvider.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using OpenTelemetry.Contrib.Extensions.AWSXRay.Resources.Http;
using OpenTelemetry.Contrib.Extensions.AWSXRay.Resources.Models;

namespace OpenTelemetry.Contrib.Extensions.AWSXRay.Resources
Expand All @@ -41,13 +41,15 @@ public class AWSEKSResourceDetector : IResourceDetector
public IEnumerable<KeyValuePair<string, object>> Detect()
{
var credentials = this.GetEKSCredentials(AWSEKSCredentialPath);
if (credentials == null || !this.IsEKSProcess(credentials))
var httpClientHandler = Handler.Create(AWSEKSCertificatePath);

if (credentials == null || !this.IsEKSProcess(credentials, httpClientHandler))
{
return null;
}

return this.ExtractResourceAttributes(
this.GetEKSClusterName(credentials),
this.GetEKSClusterName(credentials, httpClientHandler),
this.GetEKSContainerId(AWSEKSMetadataFilePath));
}

Expand Down Expand Up @@ -125,11 +127,11 @@ internal AWSEKSClusterInformationModel DeserializeResponse(string response)
return ResourceDetectorUtils.DeserializeFromString<AWSEKSClusterInformationModel>(response);
}

private string GetEKSClusterName(string credentials)
private string GetEKSClusterName(string credentials, HttpClientHandler httpClientHandler)
{
try
{
var clusterInfo = this.GetEKSClusterInfo(credentials);
var clusterInfo = this.GetEKSClusterInfo(credentials, httpClientHandler);
return this.DeserializeResponse(clusterInfo)?.Data?.ClusterName;
}
catch (Exception ex)
Expand All @@ -140,12 +142,11 @@ private string GetEKSClusterName(string credentials)
return null;
}

private bool IsEKSProcess(string credentials)
private bool IsEKSProcess(string credentials, HttpClientHandler httpClientHandler)
{
string awsAuth = null;
try
{
var httpClientHandler = this.CreateHttpClientHandler();
awsAuth = ResourceDetectorUtils.SendOutRequest(AWSAuthUrl, "GET", new KeyValuePair<string, string>("Authorization", credentials), httpClientHandler).Result;
}
catch (Exception ex)
Expand All @@ -156,17 +157,9 @@ private bool IsEKSProcess(string credentials)
return !string.IsNullOrEmpty(awsAuth);
}

private string GetEKSClusterInfo(string credentials)
private string GetEKSClusterInfo(string credentials, HttpClientHandler httpClientHandler)
{
var httpClientHandler = this.CreateHttpClientHandler();
return ResourceDetectorUtils.SendOutRequest(AWSClusterInfoUrl, "GET", new KeyValuePair<string, string>("Authorization", credentials), httpClientHandler).Result;
}

private HttpClientHandler CreateHttpClientHandler()
{
var httpClientHandler = new HttpClientHandler();
httpClientHandler.ClientCertificates.Add(new X509Certificate2(AWSEKSCertificatePath));
return httpClientHandler;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// <copyright file="Handler.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>

using System;
using System.Net.Http;

namespace OpenTelemetry.Contrib.Extensions.AWSXRay.Resources.Http
{
internal class Handler
{
public static HttpClientHandler Create(string certificateFile)
{
try
{
ServerCertificateValidationProvider serverCertificateValidationProvider =

Choose a reason for hiding this comment

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

should you check if serverCertificateValidationProvider is not ServerCertificateValidationProvider.InvalidProvider before adding a validation callback? If the cert file doesn't exist or cannot be parsed you cannot use it for validation

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 don't think it is required. If serverCertificateValidationProvider is ServerCertificateValidationProvider.InvalidProvider, ValidationCallback will be null and no call will be made for ServerCertificateCustomValidationCallback.

ServerCertificateValidationProvider.FromCertificateFile(certificateFile);

if (!serverCertificateValidationProvider.IsCertificateLoaded)
{
AWSXRayEventSource.Log.FailedToValidateCertificate(nameof(Handler), "Failed to Load the certificate file into trusted collection");
return null;
}

var clientHandler = new HttpClientHandler();
clientHandler.ServerCertificateCustomValidationCallback =

Choose a reason for hiding this comment

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

you can simplify this assignment to

clientHandler.ServerCertificateCustomValidationCallback = serverCertificateValidationProvider.ValidationCallback;

if you change the signature of the method in ServerCertificateValidationProvider class:

bool ValidationCallback(HttpRequestMessage requestMessage, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslErrors) { ... } 

(sender, x509Certificate2, x509Chain, sslPolicyErrors) =>
serverCertificateValidationProvider.ValidationCallback(null, x509Certificate2, x509Chain, sslPolicyErrors);
return clientHandler;
}
catch (Exception ex)
{
AWSXRayEventSource.Log.ResourceAttributesExtractException($"{nameof(Handler)} : Failed to create HttpClientHandler", ex);
}

return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// <copyright file="ServerCertificateValidationProvider.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>

using System;
using System.IO;
using System.Linq;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

namespace OpenTelemetry.Contrib.Extensions.AWSXRay.Resources.Http
{
internal class ServerCertificateValidationProvider
{
private static readonly ServerCertificateValidationProvider InvalidProvider =
new ServerCertificateValidationProvider(null);

private readonly X509Certificate2Collection trustedCertificates;

private ServerCertificateValidationProvider(X509Certificate2Collection trustedCertificates)
{
if (trustedCertificates == null)
{
this.trustedCertificates = null;
this.ValidationCallback = null;
this.IsCertificateLoaded = false;
return;
}

this.trustedCertificates = trustedCertificates;
this.ValidationCallback = (sender, cert, chain, errors) =>
this.ValidateCertificate(new X509Certificate2(cert), chain, errors);
this.IsCertificateLoaded = true;
}

public bool IsCertificateLoaded { get; }

public RemoteCertificateValidationCallback ValidationCallback { get; }

public static ServerCertificateValidationProvider FromCertificateFile(string certificateFile)
{
if (!File.Exists(certificateFile))
{
AWSXRayEventSource.Log.FailedToValidateCertificate(nameof(ServerCertificateValidationProvider), "Certificate File does not exist");
return InvalidProvider;
}

var trustedCertificates = new X509Certificate2Collection();
if (!LoadCertificateToTrustedCollection(trustedCertificates, certificateFile))
{
AWSXRayEventSource.Log.FailedToValidateCertificate(nameof(ServerCertificateValidationProvider), "Failed to load certificate in trusted collection");
return InvalidProvider;
}

return new ServerCertificateValidationProvider(trustedCertificates);
}

private static bool LoadCertificateToTrustedCollection(X509Certificate2Collection collection, string certFileName)
{
try
{
collection.Import(certFileName);
return true;
}
catch (Exception)
{
return false;
}
}

private bool ValidateCertificate(X509Certificate2 cert, X509Chain chain, SslPolicyErrors errors)
{
Copy link
Contributor

Choose a reason for hiding this comment

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

With this function we will try do full validation of certificate chain - it will still apply standard checks that certificate is not expired, is not revoked, created for proper domain.
It may be overkill if we would like just trust certificate provided in file on disk. So, we can discuss if we can validate just that server public key match public key on disk.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@srprash What's your thoughts on this?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, it may be a bit overkill, but since this validation will be done only 1 time on TraceProvider initialization, it should be okay. BTW, do you by any chance know what's the performance impact of this validation?

Copy link
Contributor

@iskiselev iskiselev Apr 20, 2022

Choose a reason for hiding this comment

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

We have not done performance measurement, but it should be neglectable as we are not doing much more than standard certificate validation will do - we try to build second chain (one chain is already built by normal check), validate results and scan certificate keys to match certificate on disk. I'd expect it may be up to 3 time slower then normal SSL certificate check, but it never will be on hot path - and in our case, it will be only at init time.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds good. Thanks!

var isSslPolicyPassed = errors == SslPolicyErrors.None ||
errors == SslPolicyErrors.RemoteCertificateChainErrors;
if (!isSslPolicyPassed)
{
if ((errors | SslPolicyErrors.RemoteCertificateNotAvailable) == errors)
{
AWSXRayEventSource.Log.FailedToValidateCertificate(nameof(ServerCertificateValidationProvider), "Failed to validate certificate due to RemoteCertificateNotAvailable");
}

if ((errors | SslPolicyErrors.RemoteCertificateNameMismatch) == errors)
{
AWSXRayEventSource.Log.FailedToValidateCertificate(nameof(ServerCertificateValidationProvider), "Failed to validate certificate due to RemoteCertificateNameMismatch");
}
}

chain.ChainPolicy.ExtraStore.AddRange(this.trustedCertificates);
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;

// building the chain to process basic validations e.g. signature, use, expiration, revocation
var isValidChain = chain.Build(cert);

if (!isValidChain)
{
var chainErrors = string.Empty;
foreach (var element in chain.ChainElements)
{
foreach (var status in element.ChainElementStatus)
{
chainErrors +=
$"\nCertificate [{element.Certificate.Subject}] Status [{status.Status}]: {status.StatusInformation}";
}
}

AWSXRayEventSource.Log.FailedToValidateCertificate(nameof(ServerCertificateValidationProvider), $"Failed to validate certificate due to {chainErrors}");
}

// check if at least one certificate in the chain is in our trust list
var isTrusted = this.HasCommonCertificate(chain, this.trustedCertificates);
if (!isTrusted)
{
var serverCertificates = string.Empty;
foreach (var element in chain.ChainElements)
{
serverCertificates += " " + element.Certificate.Subject;
}

var trustCertificates = string.Empty;
foreach (var trustCertificate in this.trustedCertificates)
{
trustCertificates += " " + trustCertificate.Subject;
}

AWSXRayEventSource.Log.FailedToValidateCertificate(
nameof(ServerCertificateValidationProvider),
$"Server Certificates Chain cannot be trusted. The chain doesn't match with the Trusted Certificates provided. Server Certificates:{serverCertificates}. Trusted Certificates:{trustCertificates}");
}

return isSslPolicyPassed && isValidChain && isTrusted;
}

private bool HasCommonCertificate(X509Chain chain, X509Certificate2Collection collection)
{
foreach (var chainElement in chain.ChainElements)
{
foreach (var certificate in collection)
{
if (Enumerable.SequenceEqual(chainElement.Certificate.GetPublicKey(), certificate.GetPublicKey()))
{
return true;
}
}
}

return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
<Compile Remove="Resources\TestAWSEKSResourceDetector.cs" />
<Compile Remove="Resources\TestAWSLambdaResourceDetector.cs" />
<Compile Remove="Resources\TestResourceBuilderExtensions.cs" />
<Compile Remove="Resources\Http\CertificateUploader.cs" />
<Compile Remove="Resources\Http\TestHandler.cs" />
<Compile Remove="Resources\Http\TestServerCertificateValidationProvider.cs" />

</ItemGroup>

<ItemGroup>
Expand Down
Loading