diff --git a/examples/PostgreSqlSslConfigDemo/PostgreSqlSSLConfigExample.cs b/examples/PostgreSqlSslConfigDemo/PostgreSqlSSLConfigExample.cs
new file mode 100644
index 000000000..27cf69624
--- /dev/null
+++ b/examples/PostgreSqlSslConfigDemo/PostgreSqlSSLConfigExample.cs
@@ -0,0 +1,330 @@
+namespace Testcontainers.PostgreSql.Examples;
+
+using System;
+using System.IO;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading.Tasks;
+using Npgsql;
+
+///
+/// Example demonstrating how to use PostgreSQL with SSL configuration using client certificate authentication.
+/// This example shows how to set up a PostgreSQL container with server-side SSL certificates
+/// and connect to it using client certificates for mutual TLS authentication.
+///
+public static class PostgreSqlSSLConfigExample
+{
+ ///
+ /// Demonstrates creating a PostgreSQL container with SSL configuration and client certificate authentication.
+ ///
+ public static async Task RunExample()
+ {
+ // Create temporary directory for SSL certificates
+ var tempDir = Path.Combine(Path.GetTempPath(), "testcontainers-ssl-example");
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ // Generate SSL certificates for the example
+ var (caCertPath, serverCertPath, serverKeyPath, clientCertPath, clientKeyPath) =
+ await GenerateSSLCertificates(tempDir);
+
+ Console.WriteLine("Generated SSL certificates for PostgreSQL server configuration:");
+ Console.WriteLine($" CA Certificate: {caCertPath}");
+ Console.WriteLine($" Server Certificate: {serverCertPath}");
+ Console.WriteLine($" Server Private Key: {serverKeyPath}");
+ Console.WriteLine();
+
+ // Create PostgreSQL container with SSL configuration
+ await using var container = new PostgreSqlBuilder()
+ .WithImage("postgres:16-alpine")
+ .WithDatabase("example_db")
+ .WithUsername("ssl_user")
+ .WithPassword("secure_password123")
+ .WithSSLSettings(caCertPath, serverCertPath, serverKeyPath)
+ .Build();
+
+ Console.WriteLine("Starting PostgreSQL container with SSL configuration...");
+ await container.StartAsync();
+ Console.WriteLine("PostgreSQL container started successfully with SSL enabled!");
+ Console.WriteLine();
+
+ // Example 1: Connect with SSL required but without client certificate
+ await ConnectWithSSLOnly(container);
+
+ // Example 2: Connect with client certificate authentication
+ await ConnectWithClientCertificate(container, caCertPath, clientCertPath, clientKeyPath);
+
+ // Example 3: Demonstrate SSL connection properties
+ await DemonstrateSSLProperties(container);
+
+ Console.WriteLine("SSL configuration example completed successfully!");
+ }
+ finally
+ {
+ // Clean up temporary certificates
+ if (Directory.Exists(tempDir))
+ {
+ Directory.Delete(tempDir, true);
+ Console.WriteLine($"Cleaned up temporary directory: {tempDir}");
+ }
+ }
+ }
+
+ ///
+ /// Connects to PostgreSQL with SSL required but without client certificate.
+ ///
+ private static async Task ConnectWithSSLOnly(PostgreSqlContainer container)
+ {
+ Console.WriteLine("Example 1: Connecting with SSL required (no client certificate)");
+
+ var connectionStringBuilder = new NpgsqlConnectionStringBuilder(container.GetConnectionString())
+ {
+ SslMode = SslMode.Require,
+ TrustServerCertificate = true // For demo only - use proper certificate validation in production
+ };
+
+ await using var connection = new NpgsqlConnection(connectionStringBuilder.ConnectionString);
+ await connection.OpenAsync();
+
+ Console.WriteLine($" ✓ Connected successfully with SSL");
+
+ // Ensure sslinfo extension is available for SSL inspection functions
+ await using (var enableExt = new NpgsqlCommand("CREATE EXTENSION IF NOT EXISTS sslinfo;", connection))
+ {
+ await enableExt.ExecuteNonQueryAsync();
+ }
+
+ // Verify SSL is active
+ await using var command = new NpgsqlCommand("SELECT ssl_is_used();", connection);
+ var sslIsUsed = await command.ExecuteScalarAsync();
+ Console.WriteLine($" ✓ SSL is active: {sslIsUsed}");
+ Console.WriteLine();
+ }
+
+ ///
+ /// Connects to PostgreSQL using client certificate authentication.
+ ///
+ private static async Task ConnectWithClientCertificate(PostgreSqlContainer container,
+ string caCertPath, string clientCertPath, string clientKeyPath)
+ {
+ Console.WriteLine("Example 2: Connecting with client certificate authentication");
+
+ var connectionStringBuilder = new NpgsqlConnectionStringBuilder(container.GetConnectionString())
+ {
+ // Validate server certificate against our generated CA (no hostname verification to avoid IP/hostname mismatch)
+ SslMode = SslMode.VerifyCA,
+ RootCertificate = caCertPath,
+ ClientCertificate = clientCertPath,
+ ClientCertificateKey = clientKeyPath
+ };
+
+ await using var connection = new NpgsqlConnection(connectionStringBuilder.ConnectionString);
+ await connection.OpenAsync();
+
+ Console.WriteLine($" ✓ Connected successfully with client certificate");
+
+ // Create a test table and insert data
+ await using var createCommand = new NpgsqlCommand(
+ "CREATE TABLE IF NOT EXISTS ssl_test (id SERIAL PRIMARY KEY, message TEXT, created_at TIMESTAMP DEFAULT NOW());",
+ connection);
+ await createCommand.ExecuteNonQueryAsync();
+
+ await using var insertCommand = new NpgsqlCommand(
+ "INSERT INTO ssl_test (message) VALUES (@message) RETURNING id;",
+ connection);
+ insertCommand.Parameters.AddWithValue("@message", "SSL connection with client certificate successful!");
+
+ var insertedId = await insertCommand.ExecuteScalarAsync();
+ Console.WriteLine($" ✓ Inserted record with ID: {insertedId}");
+ Console.WriteLine();
+ }
+
+ ///
+ /// Demonstrates various SSL connection properties and queries.
+ ///
+ private static async Task DemonstrateSSLProperties(PostgreSqlContainer container)
+ {
+ Console.WriteLine("Example 3: Demonstrating SSL connection properties");
+
+ var connectionStringBuilder = new NpgsqlConnectionStringBuilder(container.GetConnectionString())
+ {
+ SslMode = SslMode.Require,
+ TrustServerCertificate = true
+ };
+
+ await using var connection = new NpgsqlConnection(connectionStringBuilder.ConnectionString);
+ await connection.OpenAsync();
+
+ // Ensure sslinfo extension is available for SSL inspection functions
+ await using (var enableExt = new NpgsqlCommand("CREATE EXTENSION IF NOT EXISTS sslinfo;", connection))
+ {
+ await enableExt.ExecuteNonQueryAsync();
+ }
+
+ // Query SSL-related information
+ var sslQueries = new[]
+ {
+ ("SSL Version", "SELECT ssl_version();"),
+ ("SSL Cipher", "SELECT ssl_cipher();"),
+ ("SSL Client Certificate Present", "SELECT CASE WHEN ssl_client_cert_present() THEN 'Yes' ELSE 'No' END;"),
+ ("SSL Client Serial Number", "SELECT COALESCE(ssl_client_serial(), 'Not Available');")
+ };
+
+ foreach (var (description, query) in sslQueries)
+ {
+ try
+ {
+ await using var command = new NpgsqlCommand(query, connection);
+ var result = await command.ExecuteScalarAsync();
+ Console.WriteLine($" {description}: {result}");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($" {description}: Error - {ex.Message}");
+ }
+ }
+ Console.WriteLine();
+ }
+
+ ///
+ /// Generates SSL certificates for testing purposes.
+ /// In production, use proper certificates from a trusted CA.
+ ///
+ private static async Task<(string CaCert, string ServerCert, string ServerKey, string ClientCert, string ClientKey)>
+ GenerateSSLCertificates(string outputDir)
+ {
+ // Create CA certificate
+ using var caRsa = RSA.Create(2048);
+ var caCertRequest = new CertificateRequest(
+ "CN=Test CA, O=Testcontainers Example",
+ caRsa,
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1);
+
+ caCertRequest.CertificateExtensions.Add(
+ new X509BasicConstraintsExtension(true, false, 0, true));
+
+ caCertRequest.CertificateExtensions.Add(
+ new X509KeyUsageExtension(
+ X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign,
+ true));
+
+ using var caCert = caCertRequest.CreateSelfSigned(
+ DateTimeOffset.UtcNow.AddDays(-1),
+ DateTimeOffset.UtcNow.AddDays(365));
+
+ var caCertPath = Path.Combine(outputDir, "ca_cert.pem");
+ await File.WriteAllTextAsync(caCertPath,
+ "-----BEGIN CERTIFICATE-----\n" +
+ Convert.ToBase64String(caCert.RawData, Base64FormattingOptions.InsertLineBreaks) +
+ "\n-----END CERTIFICATE-----\n");
+
+ // Create server certificate
+ using var serverRsa = RSA.Create(2048);
+ var serverCertRequest = new CertificateRequest(
+ "CN=localhost, O=Testcontainers Example",
+ serverRsa,
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1);
+
+ serverCertRequest.CertificateExtensions.Add(
+ new X509KeyUsageExtension(
+ X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment,
+ true));
+
+ serverCertRequest.CertificateExtensions.Add(
+ new X509EnhancedKeyUsageExtension(
+ new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, // Server Authentication
+ true));
+
+ var sanBuilder = new SubjectAlternativeNameBuilder();
+ sanBuilder.AddDnsName("localhost");
+ sanBuilder.AddIpAddress(System.Net.IPAddress.Loopback);
+ serverCertRequest.CertificateExtensions.Add(sanBuilder.Build());
+
+ using var serverCert = serverCertRequest.Create(
+ caCert,
+ caCert.NotBefore,
+ caCert.NotAfter.AddSeconds(-5),
+ new ReadOnlySpan(RandomNumberGenerator.GetBytes(16)));
+
+ var serverCertPath = Path.Combine(outputDir, "server.crt");
+ await File.WriteAllTextAsync(serverCertPath,
+ "-----BEGIN CERTIFICATE-----\n" +
+ Convert.ToBase64String(serverCert.RawData, Base64FormattingOptions.InsertLineBreaks) +
+ "\n-----END CERTIFICATE-----\n");
+
+ var serverKeyPath = Path.Combine(outputDir, "server.key");
+ await File.WriteAllTextAsync(serverKeyPath,
+ "-----BEGIN PRIVATE KEY-----\n" +
+ Convert.ToBase64String(serverRsa.ExportPkcs8PrivateKey(), Base64FormattingOptions.InsertLineBreaks) +
+ "\n-----END PRIVATE KEY-----\n");
+
+ // Create client certificate
+ using var clientRsa = RSA.Create(2048);
+ var clientCertRequest = new CertificateRequest(
+ "CN=testcontainers-client, O=Testcontainers Example",
+ clientRsa,
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1);
+
+ clientCertRequest.CertificateExtensions.Add(
+ new X509KeyUsageExtension(
+ X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment,
+ true));
+
+ clientCertRequest.CertificateExtensions.Add(
+ new X509EnhancedKeyUsageExtension(
+ new OidCollection { new Oid("1.3.6.1.5.5.7.3.2") }, // Client Authentication
+ true));
+
+ using var clientCert = clientCertRequest.Create(
+ caCert,
+ caCert.NotBefore,
+ caCert.NotAfter.AddSeconds(-5),
+ new ReadOnlySpan(RandomNumberGenerator.GetBytes(16)));
+
+ var clientCertPath = Path.Combine(outputDir, "client.crt");
+ await File.WriteAllTextAsync(clientCertPath,
+ "-----BEGIN CERTIFICATE-----\n" +
+ Convert.ToBase64String(clientCert.RawData, Base64FormattingOptions.InsertLineBreaks) +
+ "\n-----END CERTIFICATE-----\n");
+
+ var clientKeyPath = Path.Combine(outputDir, "client.key");
+ await File.WriteAllTextAsync(clientKeyPath,
+ "-----BEGIN PRIVATE KEY-----\n" +
+ Convert.ToBase64String(clientRsa.ExportPkcs8PrivateKey(), Base64FormattingOptions.InsertLineBreaks) +
+ "\n-----END PRIVATE KEY-----\n");
+
+ // Set appropriate file permissions for private keys
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ {
+ File.SetUnixFileMode(serverKeyPath, UnixFileMode.UserRead | UnixFileMode.UserWrite);
+ File.SetUnixFileMode(clientKeyPath, UnixFileMode.UserRead | UnixFileMode.UserWrite);
+ }
+
+ return (caCertPath, serverCertPath, serverKeyPath, clientCertPath, clientKeyPath);
+ }
+
+ ///
+ /// Entry point for the example.
+ ///
+ public static async Task Main(string[] args)
+ {
+ Console.WriteLine("PostgreSQL SSL Configuration Example");
+ Console.WriteLine("=====================================");
+ Console.WriteLine();
+
+ try
+ {
+ await RunExample();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Example failed with error: {ex.Message}");
+ Console.WriteLine($"Stack trace: {ex.StackTrace}");
+ Environment.Exit(1);
+ }
+ }
+}
diff --git a/examples/PostgreSqlSslConfigDemo/PostgreSqlSslConfigDemo.csproj b/examples/PostgreSqlSslConfigDemo/PostgreSqlSslConfigDemo.csproj
new file mode 100644
index 000000000..1d913f913
--- /dev/null
+++ b/examples/PostgreSqlSslConfigDemo/PostgreSqlSslConfigDemo.csproj
@@ -0,0 +1,17 @@
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ Program
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/PostgreSqlSslConfigDemo/Program.cs b/examples/PostgreSqlSslConfigDemo/Program.cs
new file mode 100644
index 000000000..915fbec2a
--- /dev/null
+++ b/examples/PostgreSqlSslConfigDemo/Program.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Threading.Tasks;
+using Testcontainers.PostgreSql.Examples;
+
+internal class Program
+{
+ public static async Task Main(string[] args)
+ {
+ Console.WriteLine("=== Testcontainers PostgreSQL WithSSLSettings demo ===\n");
+
+ try
+ {
+ await PostgreSqlSSLConfigExample.RunExample();
+ Console.WriteLine("\nDemo completed successfully.");
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine("\nDemo failed: " + ex);
+ Environment.ExitCode = -1;
+ }
+ }
+}
diff --git a/examples/PostgreSqlSslConfigDemo/README.md b/examples/PostgreSqlSslConfigDemo/README.md
new file mode 100644
index 000000000..7403ba791
--- /dev/null
+++ b/examples/PostgreSqlSslConfigDemo/README.md
@@ -0,0 +1,27 @@
+# PostgreSQL WithSSLSettings demo (Testcontainers for .NET)
+
+This small console project demonstrates how to run a PostgreSQL container configured for SSL/TLS using Testcontainers' `WithSSLSettings` API and how to connect to it using Npgsql.
+
+What it does:
+- Generates a temporary CA, server, and client certificates on-the-fly (for demo purposes only).
+- Starts a `postgres:16-alpine` container with server-side SSL enabled via `WithSSLSettings(ca, serverCert, serverKey)`.
+- Shows two connection scenarios:
+ 1) SSL required (server-auth only; trusting the server cert for demo).
+ 2) Mutual TLS (client certificate authentication) using the generated client cert/key.
+- Runs a simple SQL command and prints SSL-related metadata.
+
+How to run
+1. Ensure Docker is installed and running.
+2. From the repository root, run:
+
+ ```bash
+ cd examples/PostgreSqlSslConfigDemo
+ dotnet run -c Release
+ ```
+
+You should see output indicating the container starts with SSL and that both SSL-only and client-certificate connections succeed. Temporary certificates will be created in a temp folder and deleted automatically after the run.
+
+Notes
+- The example links to the shared implementation file `examples/PostgreSqlSSLConfigExample.cs` to avoid duplication.
+- This demo targets .NET 8.0 and uses the local `Testcontainers.PostgreSql` project reference from `src/` so you can test changes to `WithSSLSettings` live.
+- Do not use `TrustServerCertificate = true` in production; it is included here only for demonstration.
diff --git a/src/Testcontainers.PostgreSql/PostgreSqlBuilder.cs b/src/Testcontainers.PostgreSql/PostgreSqlBuilder.cs
index 73ec3a317..00f2f1456 100644
--- a/src/Testcontainers.PostgreSql/PostgreSqlBuilder.cs
+++ b/src/Testcontainers.PostgreSql/PostgreSqlBuilder.cs
@@ -2,7 +2,8 @@ namespace Testcontainers.PostgreSql;
///
[PublicAPI]
-public sealed class PostgreSqlBuilder : ContainerBuilder
+public sealed class
+ PostgreSqlBuilder : ContainerBuilder
{
public const string PostgreSqlImage = "postgres:15.1";
@@ -14,6 +15,8 @@ public sealed class PostgreSqlBuilder : ContainerBuilder
/// Initializes a new instance of the class.
///
@@ -69,14 +72,122 @@ public PostgreSqlBuilder WithPassword(string password)
.WithEnvironment("POSTGRES_PASSWORD", password);
}
+ ///
+ /// Sets the PostgreSql SSL mode.
+ ///
+ /// The PostgreSql SSL mode.
+ /// A configured instance of .
+ public PostgreSqlBuilder WithSslMode(SslMode sslMode)
+ {
+ return Merge(DockerResourceConfiguration, new PostgreSqlConfiguration(sslMode: sslMode))
+ .WithEnvironment("PGSSLMODE", sslMode.ToString().ToLowerInvariant());
+ }
+
+ ///
+ /// Sets the PostgreSql root certificate file.
+ ///
+ /// The path to the root certificate file.
+ /// A configured instance of .
+ public PostgreSqlBuilder WithRootCertificate(string rootCertFile)
+ {
+ return Merge(DockerResourceConfiguration, new PostgreSqlConfiguration(rootCertFile: rootCertFile))
+ .WithBindMount(rootCertFile, Path.Combine(DefaultCertificatesDirectory, "root.crt"), AccessMode.ReadOnly)
+ .WithEnvironment("PGSSLROOTCERT", Path.Combine(DefaultCertificatesDirectory, "root.crt"));
+ }
+
+ ///
+ /// Sets the PostgreSql client certificate and key files.
+ ///
+ /// The path to the client certificate file.
+ /// The path to the client key file.
+ /// A configured instance of .
+ public PostgreSqlBuilder WithClientCertificate(string clientCertFile, string clientKeyFile)
+ {
+ return Merge(DockerResourceConfiguration,
+ new PostgreSqlConfiguration(clientCertFile: clientCertFile, clientKeyFile: clientKeyFile))
+ .WithBindMount(clientCertFile, Path.Combine(DefaultCertificatesDirectory, "postgresql.crt"),
+ AccessMode.ReadOnly)
+ .WithBindMount(clientKeyFile, Path.Combine(DefaultCertificatesDirectory, "postgresql.key"),
+ AccessMode.ReadOnly)
+ .WithEnvironment("PGSSLCERT", Path.Combine(DefaultCertificatesDirectory, "postgresql.crt"))
+ .WithEnvironment("PGSSLKEY", Path.Combine(DefaultCertificatesDirectory, "postgresql.key"));
+ }
+
+ ///
+ /// Configures the PostgreSQL server to run with SSL using the provided CA certificate, server certificate and private key.
+ /// This enables server-side SSL configuration with client certificate authentication.
+ ///
+ /// The path to the CA certificate file.
+ /// The path to the server certificate file.
+ /// The path to the server private key file.
+ /// A configured instance of .
+ ///
+ /// This method configures PostgreSQL for server-side SSL with client certificate authentication.
+ /// It requires a custom PostgreSQL configuration file that enables SSL and sets the appropriate
+ /// certificate paths. The certificates are mounted into the container and PostgreSQL is configured
+ /// to use them for SSL connections.
+ ///
+ public PostgreSqlBuilder WithSSLSettings(string caCertFile, string serverCertFile, string serverKeyFile)
+ {
+ if (string.IsNullOrWhiteSpace(caCertFile))
+ {
+ throw new ArgumentException("CA certificate file path cannot be null or empty.", nameof(caCertFile));
+ }
+
+ if (string.IsNullOrWhiteSpace(serverCertFile))
+ {
+ throw new ArgumentException("Server certificate file path cannot be null or empty.",
+ nameof(serverCertFile));
+ }
+
+ if (string.IsNullOrWhiteSpace(serverKeyFile))
+ {
+ throw new ArgumentException("Server key file path cannot be null or empty.", nameof(serverKeyFile));
+ }
+
+ const string sslConfigDir = "/tmp/testcontainers-dotnet/postgres";
+
+ var wrapperEntrypoint = @"#!/bin/sh
+set -e
+SSL_DIR=/tmp/testcontainers-dotnet/postgres
+# Fix ownership and permissions for SSL key/cert before Postgres init runs
+if [ -f ""$SSL_DIR/server.key"" ]; then
+ chown postgres:postgres ""$SSL_DIR/server.key"" || true
+ chmod 600 ""$SSL_DIR/server.key"" || true
+fi
+if [ -f ""$SSL_DIR/server.crt"" ]; then
+ chown postgres:postgres ""$SSL_DIR/server.crt"" || true
+fi
+if [ -f ""$SSL_DIR/ca_cert.pem"" ]; then
+ chown postgres:postgres ""$SSL_DIR/ca_cert.pem"" || true
+fi
+exec /usr/local/bin/docker-entrypoint.sh ""$@""
+";
+
+ return Merge(DockerResourceConfiguration, new PostgreSqlConfiguration(
+ serverCertFile: serverCertFile,
+ serverKeyFile: serverKeyFile,
+ caCertFile: caCertFile))
+ .WithResourceMapping(File.ReadAllBytes(caCertFile), $"{sslConfigDir}/ca_cert.pem", fileMode: Unix.FileMode644)
+ .WithResourceMapping(File.ReadAllBytes(serverCertFile), $"{sslConfigDir}/server.crt", fileMode: Unix.FileMode644)
+ .WithResourceMapping(File.ReadAllBytes(serverKeyFile), $"{sslConfigDir}/server.key", fileMode: Unix.FileMode700)
+ .WithResourceMapping(Encoding.UTF8.GetBytes(wrapperEntrypoint), "/usr/local/bin/docker-entrypoint-ssl.sh", fileMode: Unix.FileMode755)
+ .WithEntrypoint("/usr/local/bin/docker-entrypoint-ssl.sh")
+ .WithCommand("-c", "ssl=on")
+ .WithCommand("-c", $"ssl_ca_file={sslConfigDir}/ca_cert.pem")
+ .WithCommand("-c", $"ssl_cert_file={sslConfigDir}/server.crt")
+ .WithCommand("-c", $"ssl_key_file={sslConfigDir}/server.key");
+ }
+
///
public override PostgreSqlContainer Build()
{
Validate();
- // By default, the base builder waits until the container is running. However, for PostgreSql, a more advanced waiting strategy is necessary that requires access to the configured database and username.
- // If the user does not provide a custom waiting strategy, append the default PostgreSql waiting strategy.
- var postgreSqlBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil(DockerResourceConfiguration)));
+ // Ensure PostgreSQL is actually ready to accept connections over TCP, not just that the container is running.
+ // Always append the pg_isready-based wait strategy by default so tests using the default fixture are stable.
+ var postgreSqlBuilder =
+ WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil(DockerResourceConfiguration)));
return new PostgreSqlContainer(postgreSqlBuilder.DockerResourceConfiguration);
}
@@ -135,7 +246,11 @@ private sealed class WaitUntil : IWaitUntil
public WaitUntil(PostgreSqlConfiguration configuration)
{
// Explicitly specify the host to ensure readiness only after the initdb scripts have executed, and the server is listening on TCP/IP.
- _command = new List { "pg_isready", "--host", "localhost", "--dbname", configuration.Database, "--username", configuration.Username };
+ _command = new List
+ {
+ "pg_isready", "--host", "localhost", "--dbname", configuration.Database, "--username",
+ configuration.Username
+ };
}
///
@@ -154,7 +269,8 @@ public async Task UntilAsync(IContainer container)
if (execResult.Stderr.Contains("pg_isready was not found"))
{
- throw new NotSupportedException($"The '{container.Image.FullName}' image does not contain: pg_isready. Please use 'postgres:9.3' onwards.");
+ throw new NotSupportedException(
+ $"The '{container.Image.FullName}' image does not contain: pg_isready. Please use 'postgres:9.3' onwards.");
}
return 0L.Equals(execResult.ExitCode);
diff --git a/src/Testcontainers.PostgreSql/PostgreSqlConfiguration.cs b/src/Testcontainers.PostgreSql/PostgreSqlConfiguration.cs
index 05873fe8a..bac9d9ec1 100644
--- a/src/Testcontainers.PostgreSql/PostgreSqlConfiguration.cs
+++ b/src/Testcontainers.PostgreSql/PostgreSqlConfiguration.cs
@@ -10,14 +10,35 @@ public sealed class PostgreSqlConfiguration : ContainerConfiguration
/// The PostgreSql database.
/// The PostgreSql username.
/// The PostgreSql password.
+ /// The PostgreSql SSL mode.
+ /// The path to the PostgreSql root certificate file.
+ /// The path to the PostgreSql client certificate file.
+ /// The path to the PostgreSql client key file.
+ /// The path to the PostgreSql server certificate file.
+ /// The path to the PostgreSql server key file.
+ /// The path to the PostgreSql CA certificate file.
public PostgreSqlConfiguration(
string database = null,
string username = null,
- string password = null)
+ string password = null,
+ SslMode? sslMode = null,
+ string rootCertFile = null,
+ string clientCertFile = null,
+ string clientKeyFile = null,
+ string serverCertFile = null,
+ string serverKeyFile = null,
+ string caCertFile = null)
{
Database = database;
Username = username;
Password = password;
+ SslMode = sslMode;
+ RootCertFile = rootCertFile;
+ ClientCertFile = clientCertFile;
+ ClientKeyFile = clientKeyFile;
+ ServerCertFile = serverCertFile;
+ ServerKeyFile = serverKeyFile;
+ CaCertFile = caCertFile;
}
///
@@ -61,6 +82,13 @@ public PostgreSqlConfiguration(PostgreSqlConfiguration oldValue, PostgreSqlConfi
Database = BuildConfiguration.Combine(oldValue.Database, newValue.Database);
Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username);
Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password);
+ SslMode = BuildConfiguration.Combine(oldValue.SslMode, newValue.SslMode);
+ RootCertFile = BuildConfiguration.Combine(oldValue.RootCertFile, newValue.RootCertFile);
+ ClientCertFile = BuildConfiguration.Combine(oldValue.ClientCertFile, newValue.ClientCertFile);
+ ClientKeyFile = BuildConfiguration.Combine(oldValue.ClientKeyFile, newValue.ClientKeyFile);
+ ServerCertFile = BuildConfiguration.Combine(oldValue.ServerCertFile, newValue.ServerCertFile);
+ ServerKeyFile = BuildConfiguration.Combine(oldValue.ServerKeyFile, newValue.ServerKeyFile);
+ CaCertFile = BuildConfiguration.Combine(oldValue.CaCertFile, newValue.CaCertFile);
}
///
@@ -77,4 +105,39 @@ public PostgreSqlConfiguration(PostgreSqlConfiguration oldValue, PostgreSqlConfi
/// Gets the PostgreSql password.
///
public string Password { get; }
+
+ ///
+ /// Gets the PostgreSql SSL mode.
+ ///
+ public SslMode? SslMode { get; }
+
+ ///
+ /// Gets the path to the PostgreSql root certificate file.
+ ///
+ public string RootCertFile { get; }
+
+ ///
+ /// Gets the path to the PostgreSql client certificate file.
+ ///
+ public string ClientCertFile { get; }
+
+ ///
+ /// Gets the path to the PostgreSql client key file.
+ ///
+ public string ClientKeyFile { get; }
+
+ ///
+ /// Gets the path to the PostgreSql server certificate file.
+ ///
+ public string ServerCertFile { get; }
+
+ ///
+ /// Gets the path to the PostgreSql server key file.
+ ///
+ public string ServerKeyFile { get; }
+
+ ///
+ /// Gets the path to the PostgreSql CA certificate file.
+ ///
+ public string CaCertFile { get; }
}
\ No newline at end of file
diff --git a/src/Testcontainers.PostgreSql/PostgreSqlContainer.cs b/src/Testcontainers.PostgreSql/PostgreSqlContainer.cs
index 1981730dd..812617ccc 100644
--- a/src/Testcontainers.PostgreSql/PostgreSqlContainer.cs
+++ b/src/Testcontainers.PostgreSql/PostgreSqlContainer.cs
@@ -44,7 +44,7 @@ public async Task ExecScriptAsync(string scriptContent, Cancellation
await CopyAsync(Encoding.Default.GetBytes(scriptContent), scriptFilePath, fileMode: Unix.FileMode644, ct: ct)
.ConfigureAwait(false);
- return await ExecAsync(new[] { "psql", "--username", _configuration.Username, "--dbname", _configuration.Database, "--file", scriptFilePath }, ct)
+ return await ExecAsync(new[] { "psql", "--host", "localhost", "--username", _configuration.Username, "--dbname", _configuration.Database, "--file", scriptFilePath }, ct)
.ConfigureAwait(false);
}
}
\ No newline at end of file
diff --git a/src/Testcontainers.PostgreSql/SslMode.cs b/src/Testcontainers.PostgreSql/SslMode.cs
new file mode 100644
index 000000000..8abcc4947
--- /dev/null
+++ b/src/Testcontainers.PostgreSql/SslMode.cs
@@ -0,0 +1,27 @@
+namespace Testcontainers.PostgreSql;
+
+///
+/// Represents the SSL mode for PostgreSQL connections.
+///
+public enum SslMode
+{
+ ///
+ /// SSL is disabled.
+ ///
+ Disable,
+
+ ///
+ /// SSL is required.
+ ///
+ Require,
+
+ ///
+ /// SSL is required, and the server certificate is verified against the root certificate.
+ ///
+ VerifyCa,
+
+ ///
+ /// SSL is required, and the server certificate is verified against the root certificate and the common name.
+ ///
+ VerifyFull
+}
\ No newline at end of file
diff --git a/tests/Testcontainers.PostgreSql.Tests/PostgreSqlSSLConfigTest.cs b/tests/Testcontainers.PostgreSql.Tests/PostgreSqlSSLConfigTest.cs
new file mode 100644
index 000000000..bcc3828eb
--- /dev/null
+++ b/tests/Testcontainers.PostgreSql.Tests/PostgreSqlSSLConfigTest.cs
@@ -0,0 +1,269 @@
+#nullable enable
+using System.IO;
+using System.Net;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Testcontainers.PostgreSql.Tests;
+
+[UsedImplicitly]
+public sealed class PostgreSqlSslConfigTest
+{
+ private const string CaCertFileName = "ca_cert.pem";
+ private const string ServerCertFileName = "server.crt";
+ private const string ServerKeyFileName = "server.key";
+ private const string ClientCertFileName = "client.crt";
+ private const string ClientKeyFileName = "client.key";
+
+ private readonly string _tempDir;
+ private readonly string _caCertPath;
+ private readonly string _serverCertPath;
+ private readonly string _serverKeyPath;
+
+ private PostgreSqlContainer? _postgreSqlContainer;
+
+ public PostgreSqlSslConfigTest()
+ {
+ _tempDir = Path.Combine(Path.GetTempPath(), "testcontainers-ssl-" + Guid.NewGuid().ToString("N")[..8]);
+ Directory.CreateDirectory(_tempDir);
+
+ _caCertPath = Path.Combine(_tempDir, CaCertFileName);
+ _serverCertPath = Path.Combine(_tempDir, ServerCertFileName);
+ _serverKeyPath = Path.Combine(_tempDir, ServerKeyFileName);
+ Path.Combine(_tempDir, ClientCertFileName);
+ Path.Combine(_tempDir, ClientKeyFileName);
+ }
+
+ private async Task EnsureContainerStartedAsync()
+ {
+ if (_postgreSqlContainer != null)
+ {
+ return;
+ }
+
+ // Generate SSL certificates for testing
+ await GenerateSSLCertificates();
+
+ // Create and start the PostgreSQL container with SSL configuration
+ _postgreSqlContainer = new PostgreSqlBuilder()
+ .WithImage("postgres:16-alpine")
+ .WithDatabase("testdb")
+ .WithUsername("testuser")
+ .WithPassword("testpass123")
+ .WithSSLSettings(_caCertPath, _serverCertPath, _serverKeyPath)
+ .WithWaitStrategy(Wait.ForUnixContainer()
+ .UntilInternalTcpPortIsAvailable(PostgreSqlBuilder.PostgreSqlPort)
+ .UntilMessageIsLogged("database system is ready to accept connections"))
+ .Build();
+
+ await _postgreSqlContainer.StartAsync();
+ }
+
+ private void Cleanup()
+ {
+ try
+ {
+ _postgreSqlContainer?.DisposeAsync().AsTask().GetAwaiter().GetResult();
+ }
+ catch
+ {
+ // ignore
+ }
+
+ if (Directory.Exists(_tempDir))
+ {
+ Directory.Delete(_tempDir, true);
+ }
+ }
+
+ [Fact]
+ public async Task PostgreSqlContainerCanStartWithSSLSettings()
+ {
+ // Given
+ await EnsureContainerStartedAsync();
+ Assert.NotNull(_postgreSqlContainer);
+
+ // When
+ var connectionString = _postgreSqlContainer!.GetConnectionString();
+
+ // Then
+ Assert.NotEmpty(connectionString);
+ Assert.Contains("testdb", connectionString);
+ Assert.Contains("testuser", connectionString);
+ }
+
+ [Fact]
+ public async Task PostgreSqlContainerCanConnectWithSSL()
+ {
+ // Given
+ await EnsureContainerStartedAsync();
+ Assert.NotNull(_postgreSqlContainer);
+
+ var connectionStringBuilder = new NpgsqlConnectionStringBuilder(_postgreSqlContainer!.GetConnectionString())
+ {
+ SslMode = Npgsql.SslMode.Require,
+ TrustServerCertificate = true // For testing only - in production use proper certificate validation
+ };
+
+ // When
+ await using var connection = new NpgsqlConnection(connectionStringBuilder.ConnectionString);
+ await connection.OpenAsync(TestContext.Current.CancellationToken);
+
+ // Then
+ Assert.Equal(ConnectionState.Open, connection.State);
+
+ // Verify SSL is being used
+ await using var command =
+ new NpgsqlCommand("SELECT ssl FROM pg_stat_ssl WHERE pid = pg_backend_pid();", connection);
+ var sslIsUsed = await command.ExecuteScalarAsync(TestContext.Current.CancellationToken);
+ Assert.True(sslIsUsed is bool b && b, "SSL should be enabled for the connection");
+ }
+
+ [Fact]
+ public async Task PostgreSqlContainerWithSSLCanExecuteQueries()
+ {
+ // Given
+ await EnsureContainerStartedAsync();
+ Assert.NotNull(_postgreSqlContainer);
+
+ var connectionStringBuilder = new NpgsqlConnectionStringBuilder(_postgreSqlContainer!.GetConnectionString())
+ {
+ SslMode = Npgsql.SslMode.Require,
+ TrustServerCertificate = true
+ };
+
+ // When
+ await using var connection = new NpgsqlConnection(connectionStringBuilder.ConnectionString);
+ await connection.OpenAsync(TestContext.Current.CancellationToken);
+
+ await using var command =
+ new NpgsqlCommand("CREATE TABLE test_table (id SERIAL PRIMARY KEY, name VARCHAR(100));", connection);
+ await command.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
+
+ await using var insertCommand =
+ new NpgsqlCommand("INSERT INTO test_table (name) VALUES ('Test SSL Connection');", connection);
+ await insertCommand.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
+
+ await using var selectCommand = new NpgsqlCommand("SELECT COUNT(*) FROM test_table;", connection);
+ var count = await selectCommand.ExecuteScalarAsync(TestContext.Current.CancellationToken);
+
+ // Then
+ Assert.Equal(1L, count);
+ }
+
+ [Fact]
+ public void WithSSLCSettingsThrowsArgumentExceptionForEmptyCaCert()
+ {
+ // Given, When, Then
+ var exception = Assert.Throws(() =>
+ new PostgreSqlBuilder().WithSSLSettings("", _serverCertPath, _serverKeyPath));
+
+ Assert.Equal("caCertFile", exception.ParamName);
+ Assert.Contains("CA certificate file path cannot be null or empty", exception.Message);
+ }
+
+ [Fact]
+ public void WithSSLSettingsThrowsArgumentExceptionForEmptyServerCert()
+ {
+ // Given, When, Then
+ var exception = Assert.Throws(() =>
+ new PostgreSqlBuilder().WithSSLSettings(_caCertPath, "", _serverKeyPath));
+
+ Assert.Equal("serverCertFile", exception.ParamName);
+ Assert.Contains("Server certificate file path cannot be null or empty", exception.Message);
+ }
+
+ [Fact]
+ public void WithSSLSettingsThrowsArgumentExceptionForEmptyServerKey()
+ {
+ // Given, When, Then
+ var exception = Assert.Throws(() =>
+ new PostgreSqlBuilder().WithSSLSettings(_caCertPath, _serverCertPath, ""));
+
+ Assert.Equal("serverKeyFile", exception.ParamName);
+ Assert.Contains("Server key file path cannot be null or empty", exception.Message);
+ }
+
+ private async Task GenerateSSLCertificates()
+ {
+ // Create a simple RSA key pair for testing
+ using var rsa = RSA.Create(2048);
+
+ // Create CA certificate
+ var caCertRequest = new CertificateRequest(
+ "CN=Test CA, O=Testcontainers",
+ rsa,
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1);
+
+ caCertRequest.CertificateExtensions.Add(
+ new X509BasicConstraintsExtension(true, false, 0, true));
+
+ caCertRequest.CertificateExtensions.Add(
+ new X509KeyUsageExtension(
+ X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign,
+ true));
+
+ var caNotBefore = DateTimeOffset.UtcNow.AddDays(-1);
+ var caNotAfter = DateTimeOffset.UtcNow.AddDays(365);
+ using var caCert = caCertRequest.CreateSelfSigned(caNotBefore, caNotAfter);
+
+ // Save CA certificate
+ await File.WriteAllTextAsync(_caCertPath,
+ "-----BEGIN CERTIFICATE-----\n" +
+ Convert.ToBase64String(caCert.RawData, Base64FormattingOptions.InsertLineBreaks) +
+ "\n-----END CERTIFICATE-----\n");
+
+ // Create server certificate
+ using var serverRsa = RSA.Create(2048);
+ var serverCertRequest = new CertificateRequest(
+ "CN=localhost, O=Testcontainers",
+ serverRsa,
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1);
+
+ serverCertRequest.CertificateExtensions.Add(
+ new X509KeyUsageExtension(
+ X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment,
+ true));
+
+ serverCertRequest.CertificateExtensions.Add(
+ new X509EnhancedKeyUsageExtension(
+ new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, // Server Authentication
+ true));
+
+ // Add Subject Alternative Names
+ var sanBuilder = new SubjectAlternativeNameBuilder();
+ sanBuilder.AddDnsName("localhost");
+ sanBuilder.AddIpAddress(IPAddress.Loopback);
+ serverCertRequest.CertificateExtensions.Add(sanBuilder.Build());
+
+ var serverNotBefore = caNotBefore.AddMinutes(1) > DateTimeOffset.UtcNow.AddDays(-1)
+ ? caNotBefore.AddMinutes(1)
+ : DateTimeOffset.UtcNow.AddDays(-1);
+ var serverNotAfter = caNotAfter.AddMinutes(-1);
+ using var serverCert = serverCertRequest.Create(
+ caCert,
+ serverNotBefore,
+ serverNotAfter,
+ new ReadOnlySpan(RandomNumberGenerator.GetBytes(16)));
+
+ // Save server certificate
+ await File.WriteAllTextAsync(_serverCertPath,
+ "-----BEGIN CERTIFICATE-----\n" +
+ Convert.ToBase64String(serverCert.RawData, Base64FormattingOptions.InsertLineBreaks) +
+ "\n-----END CERTIFICATE-----\n");
+
+ // Save server private key
+ await File.WriteAllTextAsync(_serverKeyPath,
+ "-----BEGIN PRIVATE KEY-----\n" +
+ Convert.ToBase64String(serverRsa.ExportPkcs8PrivateKey(), Base64FormattingOptions.InsertLineBreaks) +
+ "\n-----END PRIVATE KEY-----\n");
+
+ // Set appropriate permissions for private key
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ {
+ File.SetUnixFileMode(_serverKeyPath, UnixFileMode.UserRead | UnixFileMode.UserWrite);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Testcontainers.PostgreSql.Tests/PostgreSqlSslTest.cs b/tests/Testcontainers.PostgreSql.Tests/PostgreSqlSslTest.cs
new file mode 100644
index 000000000..223bd7a00
--- /dev/null
+++ b/tests/Testcontainers.PostgreSql.Tests/PostgreSqlSslTest.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Data;
+using System.IO;
+using System.Threading.Tasks;
+using DotNet.Testcontainers.Configurations;
+using Npgsql;
+using Xunit;
+using Xunit.Internal;
+
+namespace Testcontainers.PostgreSql.Tests;
+
+[UsedImplicitly]
+public sealed class PostgreSqlSslTest : IAsyncLifetime
+{
+ private readonly string _tempDir;
+ private readonly string _caCertPath;
+ private readonly string _serverCertPath;
+ private readonly string _serverKeyPath;
+
+ private readonly PostgreSqlContainer _postgreSqlContainer;
+
+ public PostgreSqlSslTest()
+ {
+ _tempDir = Path.Combine(Path.GetTempPath(), "testcontainers-ssl-" + Guid.NewGuid().ToString("N").Substring(0, 8));
+ Directory.CreateDirectory(_tempDir);
+
+ _caCertPath = Path.Combine(_tempDir, "ca_cert.pem");
+ _serverCertPath = Path.Combine(_tempDir, "server.crt");
+ _serverKeyPath = Path.Combine(_tempDir, "server.key");
+
+ // Generate simple CA and server certificates for the test
+ GenerateCertificatesAsync().GetAwaiter().GetResult();
+
+ _postgreSqlContainer = new PostgreSqlBuilder()
+ .WithImage("postgres:16-alpine")
+ .WithDatabase("testdb")
+ .WithUsername("testuser")
+ .WithPassword("testpass123")
+ .WithSSLSettings(_caCertPath, _serverCertPath, _serverKeyPath)
+ .WithWaitStrategy(Wait.ForUnixContainer()
+ .UntilInternalTcpPortIsAvailable(PostgreSqlBuilder.PostgreSqlPort)
+ .UntilMessageIsLogged("database system is ready to accept connections"))
+ .Build();
+ }
+
+ [Fact]
+ public async Task PostgreSqlContainerCanConnectWithSsl()
+ {
+ // Given
+ await _postgreSqlContainer.StartAsync(TestContext.Current.CancellationToken);
+
+ var connectionStringBuilder = new NpgsqlConnectionStringBuilder(_postgreSqlContainer.GetConnectionString())
+ {
+ SslMode = Npgsql.SslMode.Require,
+ TrustServerCertificate = true
+ };
+
+ // When
+ await using var connection = new NpgsqlConnection(connectionStringBuilder.ConnectionString);
+ await connection.OpenAsync(TestContext.Current.CancellationToken);
+
+ // Then
+ Assert.Equal(ConnectionState.Open, connection.State);
+ }
+
+ public ValueTask InitializeAsync()
+ {
+ // no-op, container started within test
+ return ValueTask.CompletedTask;
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ try
+ {
+ if (_postgreSqlContainer != null)
+ {
+ await _postgreSqlContainer.DisposeAsync();
+ }
+ }
+ finally
+ {
+ if (Directory.Exists(_tempDir))
+ {
+ Directory.Delete(_tempDir, recursive: true);
+ }
+ }
+ }
+
+ private static async Task WritePemAsync(string path, byte[] derBytes, string begin, string end)
+ {
+ await File.WriteAllTextAsync(path, $"{begin}\n{Convert.ToBase64String(derBytes, Base64FormattingOptions.InsertLineBreaks)}\n{end}\n");
+ }
+
+ private async Task GenerateCertificatesAsync()
+ {
+ using var caRsa = System.Security.Cryptography.RSA.Create(2048);
+ var caReq = new System.Security.Cryptography.X509Certificates.CertificateRequest(
+ "CN=Test CA, O=Testcontainers",
+ caRsa,
+ System.Security.Cryptography.HashAlgorithmName.SHA256,
+ System.Security.Cryptography.RSASignaturePadding.Pkcs1);
+ caReq.CertificateExtensions.Add(new System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension(true, false, 0, true));
+ caReq.CertificateExtensions.Add(new System.Security.Cryptography.X509Certificates.X509KeyUsageExtension(
+ System.Security.Cryptography.X509Certificates.X509KeyUsageFlags.KeyCertSign |
+ System.Security.Cryptography.X509Certificates.X509KeyUsageFlags.CrlSign, true));
+ var caNotBefore = DateTimeOffset.UtcNow.AddDays(-1);
+ var caNotAfter = DateTimeOffset.UtcNow.AddDays(365);
+ using var caCert = caReq.CreateSelfSigned(caNotBefore, caNotAfter);
+ await WritePemAsync(_caCertPath, caCert.RawData, "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----");
+
+ using var serverRsa = System.Security.Cryptography.RSA.Create(2048);
+ var serverReq = new System.Security.Cryptography.X509Certificates.CertificateRequest(
+ "CN=localhost, O=Testcontainers",
+ serverRsa,
+ System.Security.Cryptography.HashAlgorithmName.SHA256,
+ System.Security.Cryptography.RSASignaturePadding.Pkcs1);
+ serverReq.CertificateExtensions.Add(new System.Security.Cryptography.X509Certificates.X509KeyUsageExtension(
+ System.Security.Cryptography.X509Certificates.X509KeyUsageFlags.DigitalSignature |
+ System.Security.Cryptography.X509Certificates.X509KeyUsageFlags.KeyEncipherment, true));
+ var san = new System.Security.Cryptography.X509Certificates.SubjectAlternativeNameBuilder();
+ san.AddDnsName("localhost");
+ san.AddIpAddress(System.Net.IPAddress.Loopback);
+ serverReq.CertificateExtensions.Add(san.Build());
+ var serverNotBefore = caNotBefore.AddMinutes(1);
+ var serverNotAfter = caNotAfter.AddMinutes(-1);
+ using var serverCert = serverReq.Create(
+ caCert,
+ serverNotBefore,
+ serverNotAfter,
+ new ReadOnlySpan(System.Security.Cryptography.RandomNumberGenerator.GetBytes(16)));
+ await WritePemAsync(_serverCertPath, serverCert.RawData, "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----");
+ await File.WriteAllTextAsync(_serverKeyPath, "-----BEGIN PRIVATE KEY-----\n" +
+ Convert.ToBase64String(serverRsa.ExportPkcs8PrivateKey(), Base64FormattingOptions.InsertLineBreaks) +
+ "\n-----END PRIVATE KEY-----\n");
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ {
+ File.SetUnixFileMode(_serverKeyPath, UnixFileMode.UserRead | UnixFileMode.UserWrite);
+ }
+ }
+}
\ No newline at end of file