diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Tls/SelfSignedCertificateProvider.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Tls/SelfSignedCertificateProvider.cs
new file mode 100644
index 0000000..c3961fd
--- /dev/null
+++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Tls/SelfSignedCertificateProvider.cs
@@ -0,0 +1,71 @@
+using System.Net;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.Extensions.Logging;
+using ZB.MOM.WW.MxGateway.Server.Configuration;
+
+namespace ZB.MOM.WW.MxGateway.Server.Security.Tls;
+
+///
+/// Generates and persists a long-lived self-signed certificate used as the
+/// Kestrel HTTPS default when no operator certificate is configured.
+///
+public sealed class SelfSignedCertificateProvider
+{
+ private const string ServerAuthOid = "1.3.6.1.5.5.7.3.1";
+
+ private readonly TlsOptions _options;
+ private readonly ILogger _logger;
+ private readonly TimeProvider _timeProvider;
+
+ public SelfSignedCertificateProvider(
+ TlsOptions options,
+ ILogger logger,
+ TimeProvider timeProvider)
+ {
+ _options = options;
+ _logger = logger;
+ _timeProvider = timeProvider;
+ }
+
+ /// Creates a fresh in-memory ECDSA P-256 self-signed certificate.
+ public X509Certificate2 GenerateCertificate()
+ {
+ using ECDsa key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
+ CertificateRequest request = new(
+ new X500DistinguishedName("CN=MxAccessGateway Self-Signed"),
+ key,
+ HashAlgorithmName.SHA256);
+
+ request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true));
+ request.CertificateExtensions.Add(new X509KeyUsageExtension(
+ X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment,
+ critical: true));
+ request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(
+ [new Oid(ServerAuthOid, "Server Authentication")],
+ critical: false));
+
+ SubjectAlternativeNameBuilder san = new();
+ san.AddDnsName("localhost");
+ string machine = Environment.MachineName;
+ if (!string.IsNullOrWhiteSpace(machine))
+ {
+ san.AddDnsName(machine);
+ }
+
+ foreach (string extra in _options.AdditionalDnsNames)
+ {
+ if (!string.IsNullOrWhiteSpace(extra))
+ {
+ san.AddDnsName(extra);
+ }
+ }
+
+ san.AddIpAddress(IPAddress.Loopback);
+ san.AddIpAddress(IPAddress.IPv6Loopback);
+ request.CertificateExtensions.Add(san.Build());
+
+ DateTimeOffset now = _timeProvider.GetUtcNow();
+ return request.CreateSelfSigned(now.AddDays(-1), now.AddYears(_options.ValidityYears));
+ }
+}
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Security/Tls/SelfSignedCertificateProviderTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Security/Tls/SelfSignedCertificateProviderTests.cs
new file mode 100644
index 0000000..f73cf36
--- /dev/null
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Security/Tls/SelfSignedCertificateProviderTests.cs
@@ -0,0 +1,40 @@
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Time.Testing;
+using ZB.MOM.WW.MxGateway.Server.Configuration;
+using ZB.MOM.WW.MxGateway.Server.Security.Tls;
+using Xunit;
+
+namespace ZB.MOM.WW.MxGateway.Tests.Security.Tls;
+
+public sealed class SelfSignedCertificateProviderTests
+{
+ private static SelfSignedCertificateProvider CreateProvider(TlsOptions options, FakeTimeProvider time)
+ => new(options, NullLogger.Instance, time);
+
+ [Fact]
+ public void GenerateCertificate_HasExpectedSansEkuAndValidity()
+ {
+ FakeTimeProvider time = new(new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
+ TlsOptions options = new() { ValidityYears = 7, AdditionalDnsNames = ["gw.internal"] };
+
+ using X509Certificate2 cert = CreateProvider(options, time).GenerateCertificate();
+
+ Assert.Equal(time.GetUtcNow().AddYears(7).UtcDateTime.Date, cert.NotAfter.ToUniversalTime().Date);
+ Assert.True(cert.NotBefore.ToUniversalTime() < time.GetUtcNow().UtcDateTime);
+ Assert.True(cert.HasPrivateKey);
+
+ string sans = ReadSubjectAltNames(cert);
+ Assert.Contains("localhost", sans);
+ Assert.Contains("gw.internal", sans);
+
+ X509EnhancedKeyUsageExtension eku = cert.Extensions.OfType().Single();
+ Assert.Contains(eku.EnhancedKeyUsages.Cast(),
+ o => o.Value == "1.3.6.1.5.5.7.3.1"); // serverAuth
+ }
+
+ private static string ReadSubjectAltNames(X509Certificate2 cert)
+ => cert.Extensions
+ .First(e => e.Oid?.Value == "2.5.29.17")
+ .Format(false);
+}
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj b/src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj
index 714c3cf..2026e35 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj
+++ b/src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj
@@ -7,6 +7,7 @@
+