diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 372133fc8..be0d9538e 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -63,7 +63,7 @@ jobs: env: # Lowest API version that GitHub runners support. - DOCKER_API_VERSION: 1.52 + DOCKER_API_VERSION: 1.44 steps: - name: Checkout Repository diff --git a/docs/modules/postgres.md b/docs/modules/postgres.md index cd9c3879c..8cb7879e9 100644 --- a/docs/modules/postgres.md +++ b/docs/modules/postgres.md @@ -23,6 +23,29 @@ The following example utilizes the [xUnit.net](/test_frameworks/xunit_net/) modu --8<-- "tests/Testcontainers.PostgreSql.Tests/PostgreSqlContainerTest.cs:UsePostgreSqlContainer" ``` +## SSL + +Use `WithSsl` to enable TLS and map the server certificates. Configure the client connection string with `SslMode` and (for validation) the CA certificate. + +!!! note + When SSL is enabled, Testcontainers doesn't set the SSL mode for the connection string. You'll need to choose the `SslMode` and configure it yourself. + +```csharp +--8<-- "tests/Testcontainers.PostgreSql.Tests/PostgreSqlContainerTest.cs:PostgreSqlSslBuilder" +``` + +```csharp +--8<-- "tests/Testcontainers.PostgreSql.Tests/PostgreSqlContainerTest.cs:PostgreSqlSslConnectionString" +``` + +### VerifyFull and DNS SANs + +`SslMode=VerifyFull` validates DNS SANs. Use a DNS host like `localhost` if you need full verification. + +```csharp +--8<-- "tests/Testcontainers.PostgreSql.Tests/PostgreSqlContainerTest.cs:PostgreSqlSslVerifyFull" +``` + The test example uses the following NuGet dependencies: === "Package References" diff --git a/src/Testcontainers.PostgreSql/PostgreSqlBuilder.cs b/src/Testcontainers.PostgreSql/PostgreSqlBuilder.cs index 6d9897fd0..7729a8b7c 100644 --- a/src/Testcontainers.PostgreSql/PostgreSqlBuilder.cs +++ b/src/Testcontainers.PostgreSql/PostgreSqlBuilder.cs @@ -15,6 +15,16 @@ public sealed class PostgreSqlBuilder : ContainerBuilder /// Initializes a new instance of the class. /// @@ -102,6 +112,35 @@ public PostgreSqlBuilder WithPassword(string password) .WithEnvironment("POSTGRES_PASSWORD", password); } + /// + /// Enables SSL for PostgreSql. + /// + /// The SSL certificate file. + /// The SSL certificate private key file. + /// A configured instance of . + public PostgreSqlBuilder WithSsl(string certificateFilePath, string certificateKeyFilePath) + { + return WithResourceMapping(new FileInfo(certificateFilePath), new FileInfo(CertificateFilePath), PostgresUid, PostgresGid, Unix.FileMode600) + .WithResourceMapping(new FileInfo(certificateKeyFilePath), new FileInfo(CertificateKeyFilePath), PostgresUid, PostgresGid, Unix.FileMode600) + .WithCommand("-c", "ssl=on") + .WithCommand("-c", "ssl_cert_file=" + CertificateFilePath) + .WithCommand("-c", "ssl_key_file=" + CertificateKeyFilePath); + } + + /// + /// Enables SSL for PostgreSql. + /// + /// The SSL certificate file. + /// The SSL certificate private key file. + /// The CA certificate file. + /// A configured instance of . + public PostgreSqlBuilder WithSsl(string certificateFilePath, string certificateKeyFilePath, string caCertificateFilePath) + { + return WithSsl(certificateFilePath, certificateKeyFilePath) + .WithResourceMapping(new FileInfo(caCertificateFilePath), new FileInfo(CaCertificateFilePath), PostgresUid, PostgresGid, Unix.FileMode600) + .WithCommand("-c", "ssl_ca_file=" + CaCertificateFilePath); + } + /// public override PostgreSqlContainer Build() { diff --git a/src/Testcontainers/Configurations/Unix.cs b/src/Testcontainers/Configurations/Unix.cs index d6e8da664..b85ac1ce5 100644 --- a/src/Testcontainers/Configurations/Unix.cs +++ b/src/Testcontainers/Configurations/Unix.cs @@ -10,6 +10,13 @@ namespace DotNet.Testcontainers.Configurations [PublicAPI] public sealed class Unix : IOperatingSystem { + /// + /// Represents the Unix file mode 600, which grants read and write permissions to the user and no permissions to the group and others. + /// + public const UnixFileModes FileMode600 = + UnixFileModes.UserRead | + UnixFileModes.UserWrite; + /// /// Represents the Unix file mode 644, which grants read and write permissions to the user and read permissions to the group and others. /// diff --git a/tests/Testcontainers.PostgreSql.Tests/PostgreSqlContainerTest.cs b/tests/Testcontainers.PostgreSql.Tests/PostgreSqlContainerTest.cs index 39028958c..5a363c839 100644 --- a/tests/Testcontainers.PostgreSql.Tests/PostgreSqlContainerTest.cs +++ b/tests/Testcontainers.PostgreSql.Tests/PostgreSqlContainerTest.cs @@ -2,6 +2,12 @@ namespace Testcontainers.PostgreSql; public abstract class PostgreSqlContainerTest(PostgreSqlContainerTest.PostgreSqlDefaultFixture fixture) { + private static readonly string ServerCertificateFilePath = Certificates.Instance.GetFilePath("server", "server.crt"); + + private static readonly string ServerCertificateKeyFilePath = Certificates.Instance.GetFilePath("server", "server.key"); + + private static readonly string CaCertificateFilePath = Certificates.Instance.GetFilePath("ca", "ca.crt"); + // # --8<-- [start:UsePostgreSqlContainer] [Fact] [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] @@ -85,6 +91,73 @@ protected override PostgreSqlBuilder Configure() => base.Configure().WithWaitStrategy(Wait.ForUnixContainer().UntilDatabaseIsAvailable(DbProviderFactory)); } + [UsedImplicitly] + public class PostgreSqlSslRequireFixture(IMessageSink messageSink) + : PostgreSqlDefaultFixture(messageSink) + { + protected override PostgreSqlBuilder Configure() + => base.Configure().WithSsl(ServerCertificateFilePath, ServerCertificateKeyFilePath); + + public override string ConnectionString + { + get + { + var connectionStringBuilder = new NpgsqlConnectionStringBuilder(base.ConnectionString); + connectionStringBuilder.TrustServerCertificate = true; + connectionStringBuilder.SslMode = SslMode.Require; + return connectionStringBuilder.ConnectionString; + } + } + } + + [UsedImplicitly] + public class PostgreSqlSslVerifyCaFixture(IMessageSink messageSink) + : PostgreSqlDefaultFixture(messageSink) + { + // # --8<-- [start:PostgreSqlSslBuilder] + protected override PostgreSqlBuilder Configure() + => base.Configure().WithSsl(ServerCertificateFilePath, ServerCertificateKeyFilePath, CaCertificateFilePath); + // # --8<-- [end:PostgreSqlSslBuilder] + + public override string ConnectionString + { + get + { + // # --8<-- [start:PostgreSqlSslConnectionString] + var connectionStringBuilder = new NpgsqlConnectionStringBuilder(base.ConnectionString); + connectionStringBuilder.SslMode = SslMode.VerifyCA; + connectionStringBuilder.RootCertificate = CaCertificateFilePath; + return connectionStringBuilder.ConnectionString; + // # --8<-- [end:PostgreSqlSslConnectionString] + } + } + } + + [UsedImplicitly] + public class PostgreSqlSslVerifyFullFixture(IMessageSink messageSink) + : PostgreSqlDefaultFixture(messageSink) + { + protected override PostgreSqlBuilder Configure() + => base.Configure().WithSsl(ServerCertificateFilePath, ServerCertificateKeyFilePath, CaCertificateFilePath); + + public override string ConnectionString + { + get + { + var connectionStringBuilder = new NpgsqlConnectionStringBuilder(base.ConnectionString); + // # --8<-- [start:PostgreSqlSslVerifyFull] + // Npgsql checks VerifyFull against DNS SANs, it's necessary to use "localhost" instead of + // the IP address. Testcontainers defaults to using the IP because of an old Docker bug + // with IPv4/IPv6 port mapping, where "localhost" might resolve to a different public port. + connectionStringBuilder.Host = "localhost"; + connectionStringBuilder.SslMode = SslMode.VerifyFull; + // # --8<-- [end:PostgreSqlSslVerifyFull] + connectionStringBuilder.RootCertificate = CaCertificateFilePath; + return connectionStringBuilder.ConnectionString; + } + } + } + [UsedImplicitly] public sealed class PostgreSqlDefaultConfiguration(PostgreSqlDefaultFixture fixture) : PostgreSqlContainerTest(fixture), IClassFixture; @@ -92,4 +165,16 @@ public sealed class PostgreSqlDefaultConfiguration(PostgreSqlDefaultFixture fixt [UsedImplicitly] public sealed class PostgreSqlWaitForDatabaseConfiguration(PostgreSqlWaitForDatabaseFixture fixture) : PostgreSqlContainerTest(fixture), IClassFixture; + + [UsedImplicitly] + public sealed class PostgreSqlSslRequireConfiguration(PostgreSqlSslRequireFixture fixture) + : PostgreSqlContainerTest(fixture), IClassFixture; + + [UsedImplicitly] + public sealed class PostgreSqlSslVerifyCaConfiguration(PostgreSqlSslVerifyCaFixture fixture) + : PostgreSqlContainerTest(fixture), IClassFixture; + + [UsedImplicitly] + public sealed class PostgreSqlSslVerifyFullConfiguration(PostgreSqlSslVerifyFullFixture fixture) + : PostgreSqlContainerTest(fixture), IClassFixture; } \ No newline at end of file