Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
364ac99
Add logging if we detect the app host is running with an untrusted de…
danegsta Jan 15, 2026
2191b7d
Update src/Aspire.Hosting/DeveloperCertificateService.cs
danegsta Jan 15, 2026
22ff768
Allow overriding the dev cert used by the dashboard
danegsta Jan 15, 2026
27c4ee4
Also notify the dashboard if the certificate isn't trusted
danegsta Jan 16, 2026
9ee16ca
Merge branch 'danegsta/trust' of github.com:dotnet/aspire into danegs…
danegsta Jan 16, 2026
effd859
Merge remote-tracking branch 'upstream/main' into danegsta/trust
danegsta Jan 16, 2026
0d30edf
Update trust check to handle MacOS
danegsta Jan 16, 2026
19cb551
Add link to dev-certs code
danegsta Jan 16, 2026
45ea6a3
Cleanup some of the exceptions
danegsta Jan 16, 2026
f7b3ade
Move forward unlocking the macos keychain and trust the cert
danegsta Jan 16, 2026
e255e12
Merge branch 'danegsta/trust' of github.com:dotnet/aspire into danegs…
danegsta Jan 16, 2026
d6f780d
Trusting on Mac isn't going to be an option; switch to a warning in logs
danegsta Jan 17, 2026
8c90f3e
Merge branch 'main' into danegsta/trust
danegsta Jan 24, 2026
360c705
Use resource strings, move check and logging to DcpHost setup
danegsta Jan 24, 2026
9165cfd
Update how HTTPS check is determined
danegsta Jan 24, 2026
1a45c81
Use the file system service to get a temp folder
danegsta Jan 24, 2026
c08965a
Refactor DeveloperCertificateService to use ProcessSpec and ProcessUt…
Copilot Jan 24, 2026
b0816e0
Update test service mocks
danegsta Jan 26, 2026
157a72c
Guard against the interaction service not being available
danegsta Jan 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/Aspire.Hosting/AspireEventSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -370,4 +370,22 @@ public void StartResourceStop(string kind, string resourceName)
WriteEvent(42, kind, resourceName);
}
}

[Event(43, Level = EventLevel.Informational, Message = "Development certificate trust check is starting...")]
public void DevelopmentCertificateTrustCheckStart()
{
if (IsEnabled())
{
WriteEvent(43);
}
}

[Event(44, Level = EventLevel.Informational, Message = "Development certificate trust check completed")]
public void DevelopmentCertificateTrustCheckStop()
{
if (IsEnabled())
{
WriteEvent(44);
}
}
}
37 changes: 35 additions & 2 deletions src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

#pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only
#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only

using System.Collections.Concurrent;
using System.Diagnostics;
Expand Down Expand Up @@ -412,6 +413,7 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource)
if (otlpGrpcEndpointUrl != null)
{
var address = BindingAddress.Parse(otlpGrpcEndpointUrl);

dashboardResource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, name: OtlpGrpcEndpointName, uriScheme: address.Scheme, port: address.Port, isProxied: true, transport: "http2")
{
TargetHost = address.Host
Expand All @@ -421,6 +423,7 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource)
if (otlpHttpEndpointUrl != null)
{
var address = BindingAddress.Parse(otlpHttpEndpointUrl);

dashboardResource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, name: OtlpHttpEndpointName, uriScheme: address.Scheme, port: address.Port, isProxied: true)
{
TargetHost = address.Host
Expand All @@ -430,16 +433,46 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource)
if (mcpEndpointUrl != null)
{
var address = BindingAddress.Parse(mcpEndpointUrl);

dashboardResource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, name: McpEndpointName, uriScheme: address.Scheme, port: address.Port, isProxied: true)
{
TargetHost = address.Host
});
}

// Determine whether any HTTPS endpoints are configured
var hasHttpsEndpoint = dashboardResource.TryGetAnnotationsOfType<EndpointAnnotation>(out var endpoints) && endpoints.Any(e => e.UriScheme is "https");

if (hasHttpsEndpoint &&
!dashboardResource.HasAnnotationOfType<HttpsCertificateConfigurationCallbackAnnotation>())
{
var developerCertificateService = executionContext.ServiceProvider.GetRequiredService<IDeveloperCertificateService>();
var trustDeveloperCertificate = developerCertificateService.TrustCertificate;
if (dashboardResource.TryGetLastAnnotation<CertificateAuthorityCollectionAnnotation>(out var certificateAuthorityAnnotation))
{
trustDeveloperCertificate = certificateAuthorityAnnotation.TrustDeveloperCertificates.GetValueOrDefault(trustDeveloperCertificate);
}

if (trustDeveloperCertificate)
{
dashboardResource.Annotations.Add(new HttpsCertificateConfigurationCallbackAnnotation(ctx =>
{
ctx.EnvironmentVariables["Kestrel__Certificates__Default__Path"] = ctx.CertificatePath;
ctx.EnvironmentVariables["Kestrel__Certificates__Default__KeyPath"] = ctx.KeyPath;
if (ctx.Password is not null)
{
ctx.EnvironmentVariables["Kestrel__Certificates__Default__Password"] = ctx.Password;
}

return Task.CompletedTask;
}));
}
}

dashboardResource.Annotations.Add(new ResourceUrlsCallbackAnnotation(c =>
{
var browserToken = options.DashboardToken;

foreach (var url in c.Urls)
{
if (url.Endpoint is { } endpoint)
Expand All @@ -450,7 +483,7 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource)
// Order these before non-browser usable endpoints.
url.DisplayText = $"Dashboard ({endpoint.EndpointName})";
url.DisplayOrder = 1;

// Append the browser token to the URL as a query string parameter if token is configured
if (!string.IsNullOrEmpty(browserToken))
{
Expand Down
70 changes: 64 additions & 6 deletions src/Aspire.Hosting/Dcp/DcpHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,23 @@

using System.Buffers;
using System.Collections;
using System.Globalization;
using System.IO.Pipelines;
using System.Net.Sockets;
using System.Text;
using Aspire.Dashboard.Utils;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Dcp.Process;
using Aspire.Hosting.Resources;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Aspire.Hosting.Dcp;

#pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

internal sealed class DcpHost
{
Expand All @@ -29,6 +33,9 @@ internal sealed class DcpHost
private readonly IInteractionService _interactionService;
private readonly Locations _locations;
private readonly TimeProvider _timeProvider;
private readonly IDeveloperCertificateService _developerCertificateService;
private readonly IFileSystemService _fileSystemService;
private readonly IConfiguration _configuration;
private readonly CancellationTokenSource _shutdownCts = new();
private Task? _logProcessorTask;

Expand All @@ -48,7 +55,10 @@ public DcpHost(
IInteractionService interactionService,
Locations locations,
DistributedApplicationModel applicationModel,
TimeProvider timeProvider)
TimeProvider timeProvider,
IDeveloperCertificateService developerCertificateService,
IFileSystemService fileSystemService,
IConfiguration configuration)
{
_loggerFactory = loggerFactory;
_logger = loggerFactory.CreateLogger<DcpHost>();
Expand All @@ -58,11 +68,15 @@ public DcpHost(
_locations = locations;
_applicationModel = applicationModel;
_timeProvider = timeProvider;
_developerCertificateService = developerCertificateService;
_fileSystemService = fileSystemService;
_configuration = configuration;
}

public async Task StartAsync(CancellationToken cancellationToken)
{
await EnsureDcpContainerRuntimeAsync(cancellationToken).ConfigureAwait(false);
await EnsureDevelopmentCertificateTrustAsync(cancellationToken).ConfigureAwait(false);
EnsureDcpHostRunning();
}

Expand Down Expand Up @@ -111,7 +125,7 @@ internal async Task EnsureDcpContainerRuntimeAsync(CancellationToken cancellatio
if (dcpInfo is not null)
{
DcpDependencyCheck.CheckDcpInfoAndLogErrors(_logger, _dcpOptions, dcpInfo, throwIfUnhealthy: requireContainerRuntimeInitialization);

// Show UI notification if container runtime is unhealthy
TryShowContainerRuntimeNotification(dcpInfo, cancellationToken);
}
Expand All @@ -122,6 +136,50 @@ internal async Task EnsureDcpContainerRuntimeAsync(CancellationToken cancellatio
}
}

internal async Task EnsureDevelopmentCertificateTrustAsync(CancellationToken cancellationToken)
{
AspireEventSource.Instance.DevelopmentCertificateTrustCheckStart();

try
{
// Check if the interaction service is available (dashboard enabled)
if (!_interactionService.IsAvailable)
{
return;
}

// Check and warn if the developer certificate is not trusted
if (_developerCertificateService.TrustCertificate && _developerCertificateService.Certificates.Count > 0 && !await DeveloperCertificateService.IsCertificateTrustedAsync(_fileSystemService, _developerCertificateService.Certificates.First(), cancellationToken).ConfigureAwait(false))
{
var trustLocation = "your project folder";
var appHostDirectory = _configuration["AppHost:Directory"];
if (!string.IsNullOrWhiteSpace(appHostDirectory))
{
trustLocation = $"'{appHostDirectory}'";
}

var title = InteractionStrings.DeveloperCertificateNotFullyTrustedTitle;
var message = string.Format(CultureInfo.CurrentCulture, InteractionStrings.DeveloperCertificateNotFullyTrustedMessage, trustLocation);

_logger.LogWarning("{Message}", message);

// Send notification to the dashboard
_ = _interactionService.PromptNotificationAsync(
title: title,
message: message,
options: new NotificationInteractionOptions
{
Intent = MessageIntent.Error,
},
cancellationToken: cancellationToken);
}
}
finally
{
AspireEventSource.Instance.DevelopmentCertificateTrustCheckStop();
}
}

public async Task StopAsync()
{
_shutdownCts.Cancel();
Expand Down Expand Up @@ -408,7 +466,7 @@ private void TryShowContainerRuntimeNotification(DcpInfo dcpInfo, CancellationTo

// Create a cancellation token source that can be cancelled when runtime becomes healthy
var notificationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _shutdownCts.Token);

// Single background task to show notification and poll for health updates
_ = Task.Run(async () =>
{
Expand All @@ -424,7 +482,7 @@ private void TryShowContainerRuntimeNotification(DcpInfo dcpInfo, CancellationTo
try
{
var dcpInfo = await _dependencyCheckService.GetDcpInfoAsync(force: true, cancellationToken: notificationCts.Token).ConfigureAwait(false);

if (dcpInfo is not null && IsContainerRuntimeHealthy(dcpInfo))
{
// Container runtime is now healthy, exit the polling loop
Expand All @@ -442,10 +500,10 @@ private void TryShowContainerRuntimeNotification(DcpInfo dcpInfo, CancellationTo
_logger.LogDebug(ex, "Error while polling container runtime health for notification");
}
}

// Cancel the notification at the end of the loop
notificationCts.Cancel();

// Wait for notification task to complete
try
{
Expand Down
Loading
Loading