diff --git a/src/ServiceControl.AcceptanceTesting/Https/HttpsTestConfiguration.cs b/src/ServiceControl.AcceptanceTesting/Https/HttpsTestConfiguration.cs
index 7c98392949..9e385618bf 100644
--- a/src/ServiceControl.AcceptanceTesting/Https/HttpsTestConfiguration.cs
+++ b/src/ServiceControl.AcceptanceTesting/Https/HttpsTestConfiguration.cs
@@ -15,6 +15,18 @@ public class HttpsTestConfiguration(ServiceControlInstanceType instanceType) : I
readonly string envVarPrefix = EnvironmentVariablePrefixes.GetPrefix(instanceType);
bool disposed;
+ ///
+ /// Configures Kestrel to serve HTTPS with the PKCS#12 certificate at the given path.
+ /// The file must exist when the settings are constructed, because they load it.
+ ///
+ /// Path to a PFX file holding the certificate and its private key, with no password
+ public HttpsTestConfiguration WithKestrelHttps(string certificatePath)
+ {
+ SetEnvironmentVariable("HTTPS_ENABLED", "true");
+ SetEnvironmentVariable("HTTPS_CERTIFICATEPATH", certificatePath);
+ return this;
+ }
+
///
/// Configures HTTPS redirect to be enabled.
/// When enabled, HTTP requests will be redirected to HTTPS.
diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/When_hosting_error_ingestion_only.cs b/src/ServiceControl.AcceptanceTests/Recoverability/When_hosting_error_ingestion_only.cs
index c9472b6b94..51add4610e 100644
--- a/src/ServiceControl.AcceptanceTests/Recoverability/When_hosting_error_ingestion_only.cs
+++ b/src/ServiceControl.AcceptanceTests/Recoverability/When_hosting_error_ingestion_only.cs
@@ -5,8 +5,12 @@ namespace ServiceControl.AcceptanceTests.Recoverability
using System.Diagnostics;
using System.IO;
using System.Linq;
+ using System.Net.Http;
using System.Runtime.Loader;
+ using System.Security.Cryptography;
+ using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
+ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.TestHost;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
@@ -18,6 +22,8 @@ namespace ServiceControl.AcceptanceTests.Recoverability
using NUnit.Framework;
using Particular.ServiceControl.Hosting;
using ServiceBus.Management.Infrastructure.Settings;
+ using ServiceControl.AcceptanceTesting;
+ using ServiceControl.AcceptanceTesting.Https;
using ServiceControl.ExternalIntegrations;
using ServiceControl.Hosting.Commands;
using ServiceControl.Infrastructure;
@@ -163,6 +169,70 @@ await WaitFor(host, async dbContext => await dbContext.EventLogItems.AsNoTrackin
}
}
+ [Test]
+ [NonParallelizable]
+ public async Task Should_serve_health_over_https_with_the_configured_certificate()
+ {
+ var certificatePath = Path.Combine(Path.GetTempPath(), $"sc-test-{Guid.NewGuid():n}.pfx");
+ using var https = new HttpsTestConfiguration(ServiceControlInstanceType.Primary).WithKestrelHttps(certificatePath);
+ WebApplication host = null;
+
+ try
+ {
+ var configuredThumbprint = WriteSelfSignedPfx(certificatePath);
+
+ var settings = await CreateSettings();
+ await new SetupCommand().Execute(new HostArguments([]), settings);
+
+ host = ErrorIngestionOnlyCommand.BuildHost(settings);
+ host.Urls.Add("https://127.0.0.1:0");
+
+ await host.StartAsync();
+
+ var url = host.Urls.Single();
+ string servedThumbprint = null;
+ using var handler = new HttpClientHandler
+ {
+ ServerCertificateCustomValidationCallback = (_, certificate, _, _) =>
+ {
+ servedThumbprint = certificate?.Thumbprint;
+ return true;
+ }
+ };
+ using var client = new HttpClient(handler);
+
+ using var response = await client.GetAsync($"{url}/health");
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(response.IsSuccessStatusCode, Is.True);
+ // A machine with an ASP.NET Core development certificate binds without the configured one, so only the thumbprint tells them apart.
+ Assert.That(servedThumbprint, Is.EqualTo(configuredThumbprint));
+ }
+ }
+ finally
+ {
+ File.Delete(certificatePath);
+
+ if (host != null)
+ {
+ await host.StopAsync();
+ await host.DisposeAsync();
+ }
+ }
+ }
+
+ static string WriteSelfSignedPfx(string path)
+ {
+ using var key = RSA.Create(2048);
+ var request = new CertificateRequest("CN=localhost", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+ using var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(1));
+
+ File.WriteAllBytes(path, certificate.Export(X509ContentType.Pkcs12));
+
+ return certificate.Thumbprint;
+ }
+
static async Task DispatchFailedMessage(Settings settings, string messageId)
{
var dispatchSettings = new TransportSettings
diff --git a/src/ServiceControl/Hosting/Commands/ErrorIngestionOnlyCommand.cs b/src/ServiceControl/Hosting/Commands/ErrorIngestionOnlyCommand.cs
index 1fc1c68e4e..00d38bb0b3 100644
--- a/src/ServiceControl/Hosting/Commands/ErrorIngestionOnlyCommand.cs
+++ b/src/ServiceControl/Hosting/Commands/ErrorIngestionOnlyCommand.cs
@@ -11,6 +11,7 @@ namespace ServiceControl.Hosting.Commands
using ServiceBus.Management.Infrastructure.Settings;
using ServiceControl.EventLog;
using ServiceControl.ExternalIntegrations;
+ using ServiceControl.Hosting.Https;
using ServiceControl.Infrastructure.Health;
using ServiceControl.Monitoring;
using ServiceControl.Persistence;
@@ -43,6 +44,7 @@ internal static WebApplication BuildHost(Settings settings, Action