Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resource service client certificate testing #4626

Open
drewnoakes opened this issue Jun 23, 2024 · 5 comments
Open

Resource service client certificate testing #4626

drewnoakes opened this issue Jun 23, 2024 · 5 comments

Comments

@drewnoakes
Copy link
Member

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

@drewnoakes drewnoakes added this to the 8.2 milestone Jun 23, 2024
@drewnoakes drewnoakes self-assigned this Jun 23, 2024
@kvenkatrajan
Copy link
Member

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

@drewnoakes
Copy link
Member Author

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

@drewnoakes
Copy link
Member Author

drewnoakes commented Aug 5, 2024

Manual Certificate Testing

Note

These steps require #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 ceb5d5a):
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
Copy link
Member

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

@drewnoakes
Copy link
Member Author

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.

@drewnoakes drewnoakes modified the milestones: 8.2, 9.0 Aug 23, 2024
@drewnoakes drewnoakes modified the milestones: 9.0, Backlog Oct 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants