Skip to content

Commit

Permalink
feat(#370): Add protected Docker daemon socket support (TLS) (#548)
Browse files Browse the repository at this point in the history
Co-authored-by: Andre Hofmeister <[email protected]>
  • Loading branch information
vlaskal and HofmeisterAn authored Oct 20, 2022
1 parent aba9f75 commit 480fc82
Show file tree
Hide file tree
Showing 18 changed files with 512 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- 370 Add protected Docker daemon socket support (@vlaskal)
- 421 Add Azurite module (@vlaskal)
- 421 Add Cosmos DB Linux Emulator (@Yeseh, @ktjn)
- 504 Add Elasticsearch module (@chertby)
Expand Down
2 changes: 2 additions & 0 deletions Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" />
<PackageReference Update="JetBrains.Annotations" Version="2022.1.0" PrivateAssets="all" />
<PackageReference Update="Docker.DotNet" Version="3.125.12" />
<PackageReference Update="Docker.DotNet.X509" Version="3.125.12" />
<PackageReference Update="Microsoft.Bcl.AsyncInterfaces" Version="1.1.1" />
<PackageReference Update="Microsoft.Extensions.Logging.Abstractions" Version="3.1.26" />
<PackageReference Update="Portable.BouncyCastle" Version="1.9.0" />
<PackageReference Update="SharpZipLib" Version="1.4.0" />
<PackageReference Update="System.Text.Json" Version="4.7.2" />
<!-- Unit and integration test dependencies: -->
Expand Down
95 changes: 95 additions & 0 deletions src/Testcontainers/Builders/MTlsEndpointAuthenticationProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
namespace DotNet.Testcontainers.Builders
{
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using Docker.DotNet.X509;
using DotNet.Testcontainers.Configurations;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;

/// <inheritdoc cref="IDockerRegistryAuthenticationProvider" />
internal sealed class MTlsEndpointAuthenticationProvider : TlsEndpointAuthenticationProvider
{
private static readonly X509CertificateParser CertificateParser = new X509CertificateParser();

/// <summary>
/// Initializes a new instance of the <see cref="MTlsEndpointAuthenticationProvider" /> class.
/// </summary>
public MTlsEndpointAuthenticationProvider()
: this(PropertiesFileConfiguration.Instance, EnvironmentConfiguration.Instance)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="MTlsEndpointAuthenticationProvider" /> class.
/// </summary>
/// <param name="customConfigurations">A list of custom configurations.</param>
public MTlsEndpointAuthenticationProvider(params ICustomConfiguration[] customConfigurations)
: base(customConfigurations)
{
}

/// <inheritdoc />
public override bool IsApplicable()
{
var certificatesFiles = new[] { ClientCertificateFileName, ClientCertificateKeyFileName };
return this.TlsEnabled && this.TlsVerifyEnabled && certificatesFiles.Select(fileName => Path.Combine(this.CertificatesDirectoryPath, fileName)).All(File.Exists);
}

/// <inheritdoc />
public override IDockerEndpointAuthenticationConfiguration GetAuthConfig()
{
var credentials = new CertificateCredentials(this.GetClientCertificate());
credentials.ServerCertificateValidationCallback = this.ServerCertificateValidationCallback;
return new DockerEndpointAuthenticationConfiguration(this.DockerEngine, credentials);
}

/// <inheritdoc />
protected override X509Certificate2 GetClientCertificate()
{
var clientCertificateFilePath = Path.Combine(this.CertificatesDirectoryPath, ClientCertificateFileName);
var clientCertificateKeyFilePath = Path.Combine(this.CertificatesDirectoryPath, ClientCertificateKeyFileName);
return CreateFromPemFile(clientCertificateFilePath, clientCertificateKeyFilePath);
}

private static X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath)
{
if (!File.Exists(certPemFilePath))
{
throw new FileNotFoundException(certPemFilePath);
}

if (!File.Exists(keyPemFilePath))
{
throw new FileNotFoundException(keyPemFilePath);
}

using (var keyPairStream = new StreamReader(keyPemFilePath))
{
var store = new Pkcs12StoreBuilder().Build();

var certificate = CertificateParser.ReadCertificate(File.ReadAllBytes(certPemFilePath));

var password = Guid.NewGuid().ToString("D");

var keyPair = (AsymmetricCipherKeyPair)new PemReader(keyPairStream).ReadObject();

var certificateEntry = new X509CertificateEntry(certificate);

var keyEntry = new AsymmetricKeyEntry(keyPair.Private);
store.SetKeyEntry(certificate.SubjectDN + "_key", keyEntry, new[] { certificateEntry });

using (var certificateStream = new MemoryStream())
{
store.Save(certificateStream, password.ToCharArray(), new SecureRandom());
return new X509Certificate2(Pkcs12Utilities.ConvertToDefiniteLength(certificateStream.ToArray()), password);
}
}
}
}
}
115 changes: 115 additions & 0 deletions src/Testcontainers/Builders/TlsEndpointAuthenticationProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
namespace DotNet.Testcontainers.Builders
{
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using DotNet.Testcontainers.Configurations;

/// <inheritdoc cref="IDockerRegistryAuthenticationProvider" />
internal class TlsEndpointAuthenticationProvider : DockerEndpointAuthenticationProvider
{
protected const string CaCertificateFileName = "ca.pem";

protected const string ClientCertificateFileName = "cert.pem";

protected const string ClientCertificateKeyFileName = "key.pem";

/// <summary>
/// Initializes a new instance of the <see cref="TlsEndpointAuthenticationProvider" /> class.
/// </summary>
public TlsEndpointAuthenticationProvider()
: this(PropertiesFileConfiguration.Instance, EnvironmentConfiguration.Instance)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="TlsEndpointAuthenticationProvider" /> class.
/// </summary>
/// <param name="customConfigurations">A list of custom configurations.</param>
public TlsEndpointAuthenticationProvider(params ICustomConfiguration[] customConfigurations)
: this(customConfigurations
.OrderByDescending(item => item.GetDockerTlsVerify())
.ThenByDescending(item => item.GetDockerTls())
.DefaultIfEmpty(new PropertiesFileConfiguration(Array.Empty<string>()))
.First())
{
}

private TlsEndpointAuthenticationProvider(ICustomConfiguration customConfiguration)
{
this.TlsEnabled = customConfiguration.GetDockerTls() || customConfiguration.GetDockerTlsVerify();
this.TlsVerifyEnabled = customConfiguration.GetDockerTlsVerify();
this.CertificatesDirectoryPath = customConfiguration.GetDockerCertPath() ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".docker");
this.DockerEngine = customConfiguration.GetDockerHost() ?? new Uri("tcp://localhost:2376");
}

protected bool TlsEnabled { get; }

protected bool TlsVerifyEnabled { get; }

protected string CertificatesDirectoryPath { get; }

protected Uri DockerEngine { get; }

/// <inheritdoc />
public override bool IsApplicable()
{
return this.TlsEnabled;
}

/// <inheritdoc />
public override IDockerEndpointAuthenticationConfiguration GetAuthConfig()
{
var credentials = new TlsCredentials();
credentials.ServerCertificateValidationCallback = this.ServerCertificateValidationCallback;
return new DockerEndpointAuthenticationConfiguration(this.DockerEngine, credentials);
}

/// <summary>
/// Gets the root certificate authority (CA).
/// </summary>
/// <returns>The root certificate authority (CA).</returns>
protected virtual X509Certificate2 GetCaCertificate()
{
return new X509Certificate2(Path.Combine(this.CertificatesDirectoryPath, CaCertificateFileName));
}

/// <summary>
/// Gets the client certificate.
/// </summary>
/// <returns>The client certificate.</returns>
protected virtual X509Certificate2 GetClientCertificate()
{
return null;
}

/// <inheritdoc cref="ServicePointManager.ServerCertificateValidationCallback" />
protected virtual bool ServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
switch (sslPolicyErrors)
{
case SslPolicyErrors.None:
return true;
case SslPolicyErrors.RemoteCertificateNameMismatch:
case SslPolicyErrors.RemoteCertificateNotAvailable:
return false;
case SslPolicyErrors.RemoteCertificateChainErrors:
default:
using (var caCertificate = this.GetCaCertificate())
{
var validationChain = new X509Chain();
validationChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
validationChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
validationChain.ChainPolicy.ExtraStore.Add(caCertificate);
validationChain.ChainPolicy.ExtraStore.AddRange(chain.ChainElements.OfType<X509ChainElement>().Select(element => element.Certificate).ToArray());
var isVerified = validationChain.Build(new X509Certificate2(certificate));
var isSignedByExpectedRoot = validationChain.ChainElements[validationChain.ChainElements.Count - 1].Certificate.RawData.SequenceEqual(caCertificate.RawData);
return isVerified && isSignedByExpectedRoot;
}
}
}
}
}
6 changes: 6 additions & 0 deletions src/Testcontainers/Clients/DockerSystemOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace DotNet.Testcontainers.Clients
using System;
using System.Threading;
using System.Threading.Tasks;
using Docker.DotNet.Models;
using DotNet.Testcontainers.Configurations;
using Microsoft.Extensions.Logging;

Expand All @@ -19,5 +20,10 @@ public async Task<bool> GetIsWindowsEngineEnabled(CancellationToken ct = default
return (await this.Docker.System.GetSystemInfoAsync(ct)
.ConfigureAwait(false)).OperatingSystem.Contains("Windows");
}

public Task<VersionResponse> GetVersion(CancellationToken ct = default)
{
return this.Docker.System.GetVersionAsync(ct);
}
}
}
3 changes: 3 additions & 0 deletions src/Testcontainers/Clients/IDockerSystemOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ namespace DotNet.Testcontainers.Clients
{
using System.Threading;
using System.Threading.Tasks;
using Docker.DotNet.Models;

internal interface IDockerSystemOperations
{
Task<bool> GetIsWindowsEngineEnabled(CancellationToken ct = default);

Task<VersionResponse> GetVersion(CancellationToken ct = default);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,24 @@
/// Initializes a new instance of the <see cref="DockerEndpointAuthenticationConfiguration" /> struct.
/// </summary>
/// <param name="endpoint">The Docker API endpoint.</param>
public DockerEndpointAuthenticationConfiguration(Uri endpoint)
/// <param name="credentials">The Docker API authentication credentials.</param>
public DockerEndpointAuthenticationConfiguration(Uri endpoint, Credentials credentials = null)
{
this.Credentials = credentials;
this.Endpoint = endpoint;
}

/// <inheritdoc />
public Credentials Credentials { get; }

/// <inheritdoc />
public Uri Endpoint { get; }

/// <inheritdoc />
public DockerClientConfiguration GetDockerClientConfiguration(Guid sessionId = default)
{
var defaultHttpRequestHeaders = new ReadOnlyDictionary<string, string>(new Dictionary<string, string> { { "x-tc-sid", sessionId.ToString("D") } });
return new DockerClientConfiguration(this.Endpoint, defaultHttpRequestHeaders: defaultHttpRequestHeaders);
return new DockerClientConfiguration(this.Endpoint, this.Credentials, defaultHttpRequestHeaders: defaultHttpRequestHeaders);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ public interface IDockerEndpointAuthenticationConfiguration
[NotNull]
Uri Endpoint { get; }

/// <summary>
/// Gets the Docker API credentials.
/// </summary>
[CanBeNull]
Credentials Credentials { get; }

/// <summary>
/// Gets the Docker client configuration.
/// </summary>
Expand Down
9 changes: 8 additions & 1 deletion src/Testcontainers/Configurations/TestcontainersSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ public static class TestcontainersSettings
private static readonly IDockerImage RyukContainerImage = new DockerImage("testcontainers/ryuk:0.3.4");

private static readonly IDockerEndpointAuthenticationConfiguration DockerEndpointAuthConfig =
new IDockerEndpointAuthenticationProvider[] { new EnvironmentEndpointAuthenticationProvider(), new NpipeEndpointAuthenticationProvider(), new UnixEndpointAuthenticationProvider() }
new IDockerEndpointAuthenticationProvider[]
{
new MTlsEndpointAuthenticationProvider(),
new TlsEndpointAuthenticationProvider(),
new EnvironmentEndpointAuthenticationProvider(),
new NpipeEndpointAuthenticationProvider(),
new UnixEndpointAuthenticationProvider(),
}
.AsParallel()
.Where(authProvider => authProvider.IsApplicable())
.Where(authProvider => authProvider.IsAvailable())
Expand Down
27 changes: 27 additions & 0 deletions src/Testcontainers/Configurations/TlsCredentials.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace DotNet.Testcontainers.Configurations
{
using System.Net;
using System.Net.Http;
using Docker.DotNet.X509;
using Microsoft.Net.Http.Client;

internal sealed class TlsCredentials : CertificateCredentials
{
public TlsCredentials()
: base(null)
{
}

public override bool IsTlsCredentials()
{
return true;
}

public override HttpMessageHandler GetHandler(HttpMessageHandler innerHandler)
{
var handler = (ManagedHandler)innerHandler;
handler.ServerCertificateValidationCallback = this.ServerCertificateValidationCallback ?? ServicePointManager.ServerCertificateValidationCallback;
return handler;
}
}
}
4 changes: 3 additions & 1 deletion src/Testcontainers/Testcontainers.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Microsoft.Build.CentralPackageVersions" Version="2.1.3" />
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
Expand All @@ -9,8 +9,10 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" />
<PackageReference Include="Docker.DotNet" />
<PackageReference Include="Docker.DotNet.X509" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Portable.BouncyCastle" />
<PackageReference Include="SharpZipLib" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace DotNet.Testcontainers.Tests.Fixtures
{
using System.Collections.Generic;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;
using JetBrains.Annotations;

[UsedImplicitly]
public sealed class DockerMTlsFixture : ProtectDockerDaemonSocket
{
public DockerMTlsFixture()
: base(new TestcontainersBuilder<TestcontainersContainer>())
{
}

public override IList<string> CustomProperties
{
get
{
var customProperties = base.CustomProperties;
customProperties.Add("docker.tls=false");
customProperties.Add("docker.tls.verify=true");
return customProperties;
}
}
}
}
Loading

0 comments on commit 480fc82

Please sign in to comment.