Skip to content

Commit

Permalink
Fix #197 - AWSEKSResourceDetector fails to detect resources due to ex…
Browse files Browse the repository at this point in the history
…ception "The SSL connection could not be established" (#208)

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

Issue Description:
With the current code, it requires an additional step of installing kubernetes self-signed certificate to the trusted store as .NET core expects them to the trsuted store to make a secure connection.

Fix Provided:
ServerCertificationValidationProvider - Safely loads the certificate to the trusted collection along with ServerSideValidation. This would avoid the unnecessary step of installing the certificate manually.
Handler - Creates HttpClientHandler with client certificate.

(cherry picked from commit 41e68d3)

* PR Review Comments

(cherry picked from commit 0b5d9a9)

* Address PR comments

(cherry picked from commit 4aa1c62)

* Remove unused import

* Added Unit Tests

* Removed unused directive

* Added Trait for codecov build

* Use temporary files to store test certificate

* Fixed File Access Issue in Unit Test

* Use TempFileName for Unit Tests

* Added Debug logging for build test
Fixed For loop tries

* Revert "Added Debug logging for build test"

This reverts commit b982325.

* Fixed tries logic in for loop

Co-authored-by: Prashant Srivastava <[email protected]>
Co-authored-by: Igor Kiselev <[email protected]>
  • Loading branch information
3 people authored May 12, 2022
1 parent 055c156 commit 5608616
Show file tree
Hide file tree
Showing 9 changed files with 426 additions and 17 deletions.
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" />
<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 =
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 =
(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)
{
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

0 comments on commit 5608616

Please sign in to comment.