dotnet / aspire

Tools, templates, and packages to accelerate building observable, production-ready apps
https://learn.microsoft.com/dotnet/aspire
MIT License
3.94k stars 482 forks source link

Resource service client certificate testing #4626

Open drewnoakes opened 5 months ago

drewnoakes commented 5 months ago

(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.

kvenkatrajan commented 4 months ago

@drewnoakes please confirm if Bala has the steps for this.

drewnoakes commented 3 months ago

I've coordinated with Bala and will share info and test steps here.

drewnoakes commented 3 months ago

Manual Certificate Testing

[!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.

Generate keys

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

Modify the app host

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.

  1. Clone the dotnet/aspire repo down.
  2. Apply the following changes (correct as of ceb5d5a0af5b350198da4468401b59d7c7bca3aa):
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
                         }
                     );

Run test

  1. Make TestShop.AppHost the startup project.
  2. Make sure Docker desktop is running.
  3. F5

The dashboard should appear, populated with TestShop's resources.

kvenkatrajan commented 3 months ago

@balachir please confirm if you have reviewed this and the validation has been added to the suite of tests.

drewnoakes commented 3 months ago

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.