Open drewnoakes opened 5 months ago
@drewnoakes please confirm if Bala has the steps for this.
I've coordinated with Bala and will share info and test steps here.
[!NOTE] These steps require https://github.com/dotnet/aspire/pull/5173 to have merged.
The dashboard supports use of client certificates for authorization when connecting to a resource service. This support exists for custom resource services.
The following instructions explain the rationale for these tests, as well as the required steps and outcomes along the way.
Some keys and certificates are required for testing. These can be created once and reused across test runs.
This script places these files in C:\certs\
. You can use a different path, but must update the path wherever it exists throughout the following example.
As you run these commands you'll be asked for passphrases. Use root
for the root CA, client
for the client cert and server
for the server cert. These values will also be provided in config for the dashboard, further down, so they have to match.
# create root CA details
openssl genrsa -aes256 -out rootCA.key 4096
openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 3650 -out rootCA.crt -subj "//CN=MyRootCA"
# server
openssl genrsa -aes256 -out server.key 4096
openssl req -new -key server.key -out server.csr -subj "//CN=localhost"
openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out server.crt -days 365 -sha256
openssl pkcs12 -export -out server.pfx -inkey server.key -in server.crt
# client
openssl genrsa -aes256 -out client.key 4096
openssl req -new -key client.key -out client.csr -subj "//CN=localhost"
openssl x509 -req -in client.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out client.crt -days 365 -sha256
openssl pkcs12 -export -out client.pfx -inkey client.key -in client.crt
Certificate testing requires a resource service that actually supports certificates, and none exist today (that we have access to). However we can modify the Aspire AppHost model and the TestShop
playground to demonstrate this working end-to-end.
dotnet/aspire
repo down.diff --git a/playground/TestShop/TestShop.AppHost/Properties/launchSettings.json b/playground/TestShop/TestShop.AppHost/Properties/launchSettings.json
index ea78f2933..8ec303999 100644
--- a/playground/TestShop/TestShop.AppHost/Properties/launchSettings.json
+++ b/playground/TestShop/TestShop.AppHost/Properties/launchSettings.json
@@ -11,7 +11,11 @@
//"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:16037",
"DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL": "https://localhost:16038",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:17037",
- "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true"
+ "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true",
+ "APPHOST__RESOURCESERVICE__AUTHMODE": "Certificate",
+ "APPHOST__RESOURCESERVICE__CLIENTCERTIFICATE__SOURCE": "File",
+ "APPHOST__RESOURCESERVICE__CLIENTCERTIFICATE__FILEPATH": "C:\\certs\\server.pfx",
+ "APPHOST__RESOURCESERVICE__CLIENTCERTIFICATE__PASSWORD": "server"
}
},
"http": {
diff --git a/src/Aspire.Dashboard/Properties/launchSettings.json b/src/Aspire.Dashboard/Properties/launchSettings.json
index 19fd120d7..113e16ff3 100644
--- a/src/Aspire.Dashboard/Properties/launchSettings.json
+++ b/src/Aspire.Dashboard/Properties/launchSettings.json
@@ -6,7 +6,11 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:15877",
- "DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL": "https://localhost:15876"
+ "DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL": "https://localhost:15876",
+ "DASHBOARD__RESOURCESERVICECLIENT__AUTHMODE": "Certificate",
+ "DASHBOARD__RESOURCESERVICECLIENT__CLIENTCERTIFICATE__SOURCE": "File",
+ "DASHBOARD__RESOURCESERVICECLIENT__CLIENTCERTIFICATE__FILEPATH": "C:\\certs\\client.pfx",
+ "DASHBOARD__RESOURCESERVICECLIENT__CLIENTCERTIFICATE__PASSWORD": "client"
},
"applicationUrl": "https://localhost:15889;http://localhost:15888"
}
diff --git a/src/Aspire.Hosting/Aspire.Hosting.csproj b/src/Aspire.Hosting/Aspire.Hosting.csproj
index 8208c241d..a979f46df 100644
--- a/src/Aspire.Hosting/Aspire.Hosting.csproj
+++ b/src/Aspire.Hosting/Aspire.Hosting.csproj
@@ -42,6 +42,7 @@
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" />
<PackageReference Include="KubernetesClient" />
+ <PackageReference Include="Microsoft.AspNetCore.Authentication.Certificate" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Polly.Core" />
</ItemGroup>
diff --git a/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs b/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs
index 09b55d5df..cd838da6a 100644
--- a/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs
+++ b/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs
@@ -180,7 +180,7 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource)
context.EnvironmentVariables[DashboardConfigNames.ResourceServiceClientAuthModeName.EnvVarName] = nameof(ResourceServiceAuthMode.ApiKey);
context.EnvironmentVariables[DashboardConfigNames.ResourceServiceClientApiKeyName.EnvVarName] = resourceServiceApiKey;
}
- else
+ else //if (string.IsNullOrEmpty(configuration["AppHost:ResourceService:AuthMode"]))
{
context.EnvironmentVariables[DashboardConfigNames.ResourceServiceClientAuthModeName.EnvVarName] = nameof(ResourceServiceAuthMode.Unsecured);
}
diff --git a/src/Aspire.Hosting/Dashboard/DashboardServiceHost.cs b/src/Aspire.Hosting/Dashboard/DashboardServiceHost.cs
index 36ba2b39d..bce9b9c40 100644
--- a/src/Aspire.Hosting/Dashboard/DashboardServiceHost.cs
+++ b/src/Aspire.Hosting/Dashboard/DashboardServiceHost.cs
@@ -3,14 +3,17 @@
using System.Diagnostics;
using System.Net;
+using System.Security.Cryptography.X509Certificates;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Dcp;
+using Microsoft.AspNetCore.Authentication.Certificate;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -73,6 +76,49 @@ public DashboardServiceHost(
// Turn on HTTPS
builder.WebHost.UseKestrelHttpsConfiguration();
+ #region TEMPORARY TEST CODE
+
+ // Auth
+ builder.Services
+ .AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
+ .AddCertificate(options =>
+ {
+ // Disallow self-signed cert\s.
+ options.AllowedCertificateTypes = CertificateTypes.Chained;
+
+ // Revocation checks require an online CA, which we don't have during testing.
+ options.RevocationMode = X509RevocationMode.NoCheck;
+
+ options.Events = new CertificateAuthenticationEvents()
+ {
+ OnAuthenticationFailed = context =>
+ {
+ _logger.LogError(context.Exception, "Failed authentication.");
+
+ return Task.CompletedTask;
+ },
+ OnCertificateValidated = context =>
+ {
+ _logger.LogInformation("Authentication complete.");
+
+ return Task.CompletedTask;
+ }
+ };
+ });
+
+ builder.Services.AddAuthorization();
+
+ builder.Services.Configure<KestrelServerOptions>(options =>
+ {
+ options.ConfigureHttpsDefaults(options =>
+ {
+ options.ServerCertificate = new X509Certificate2(@"C:\certs\server.pfx", "server", X509KeyStorageFlags.DefaultKeySet);
+ options.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
+ });
+ });
+
+ #endregion
+
// Configuration
builder.Services.AddSingleton(configuration);
@@ -107,7 +153,7 @@ public DashboardServiceHost(
builder.Services.AddSingleton(loggerOptions);
builder.Services.Add(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>)));
- builder.Services.AddGrpc();
+ builder.Services.AddGrpc(options => options.EnableDetailedErrors = true);
builder.Services.AddSingleton(applicationModel);
builder.Services.AddSingleton(kubernetesService);
builder.Services.AddSingleton<DashboardServiceData>();
diff --git a/src/Aspire.Hosting/Dashboard/ResourceServiceOptions.cs b/src/Aspire.Hosting/Dashboard/ResourceServiceOptions.cs
index fcf487c01..54c0d2379 100644
--- a/src/Aspire.Hosting/Dashboard/ResourceServiceOptions.cs
+++ b/src/Aspire.Hosting/Dashboard/ResourceServiceOptions.cs
@@ -13,7 +13,8 @@ internal enum ResourceServiceAuthMode
// certificate-based auth.
Unsecured,
- ApiKey
+ ApiKey,
+ Certificate
}
internal sealed class ResourceServiceOptions
diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs
index 679d7ea3f..9e236ac87 100644
--- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs
+++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs
@@ -197,7 +197,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
_innerBuilder.Configuration.AddInMemoryCollection(
new Dictionary<string, string?>
{
- ["AppHost:ResourceService:AuthMode"] = nameof(ResourceServiceAuthMode.ApiKey),
+ //["AppHost:ResourceService:AuthMode"] = nameof(ResourceServiceAuthMode.ApiKey),
["AppHost:ResourceService:ApiKey"] = apiKey
}
);
TestShop.AppHost
the startup project.The dashboard should appear, populated with TestShop's resources.
@balachir please confirm if you have reviewed this and the validation has been added to the suite of tests.
I spoke with Bala earlier today and the team had some challenges with this testing. I'm waiting on further info to help unblock them.
(Split out from #3739)
Either set up automated testing for this scenario or add steps to manual test runs to validate the use of certificates over this channel.