Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3c42e64
Add logging if we detect the app host is running with an untrusted de…
danegsta Jan 15, 2026
77d3818
Allow overriding the dev cert used by the dashboard
danegsta Jan 15, 2026
be562bd
Also notify the dashboard if the certificate isn't trusted
danegsta Jan 16, 2026
33f0d1e
Update src/Aspire.Hosting/DeveloperCertificateService.cs
danegsta Jan 15, 2026
7d95f07
Move forward unlocking the macos keychain and trust the cert
danegsta Jan 16, 2026
b79f3cd
Update trust check to handle MacOS
danegsta Jan 16, 2026
60fe2d2
Add link to dev-certs code
danegsta Jan 16, 2026
9bf3372
Cleanup some of the exceptions
danegsta Jan 16, 2026
1fb6076
Trusting on Mac isn't going to be an option; switch to a warning in logs
danegsta Jan 17, 2026
1783692
Use resource strings, move check and logging to DcpHost setup
danegsta Jan 24, 2026
0eb5f0c
Update how HTTPS check is determined
danegsta Jan 24, 2026
85e6e21
Use the file system service to get a temp folder
danegsta Jan 24, 2026
30f474b
Refactor DeveloperCertificateService to use ProcessSpec and ProcessUt…
Copilot Jan 24, 2026
1009421
Update test service mocks
danegsta Jan 26, 2026
ca2f7e9
Guard against the interaction service not being available
danegsta Jan 27, 2026
d2c166a
Fix merge issue in DashboardEventHandlers
danegsta Feb 25, 2026
797e82e
Fix developer cert service timeout on Mac CI
danegsta Feb 25, 2026
4021dbc
Only skip interaction service prompt if service not available
danegsta Feb 25, 2026
b972a48
Add additional test
danegsta Feb 25, 2026
2f1f17c
Fix failing test
danegsta Feb 25, 2026
c9c57f0
Disable dev cert https in mac ci tests
danegsta Feb 25, 2026
c45514a
Revert unrequired macos timeout change
danegsta Feb 25, 2026
6624ff3
Add comments explaining the dashboard certificate config
danegsta Feb 25, 2026
a2d7ec2
Only warn if there's an HTTPS/TLS endpoint
danegsta Feb 25, 2026
80cd613
Merge branch 'release/13.2' into danegsta/trustLog
danegsta Mar 4, 2026
98d854b
Cleanup how we determine certificate trust to a cross-platform approach
danegsta Mar 4, 2026
e145d44
Expose interface property such that it can be used in tests
danegsta Mar 4, 2026
41e6906
Add chain build that got removed
danegsta Mar 4, 2026
8988e3f
Check if cert is in root store on Windows
danegsta Mar 4, 2026
0415f8a
Fix check that wasn't properly detecting the lack of trusted certific…
danegsta Mar 5, 2026
6a951b3
Update src/Aspire.Hosting/Dcp/DcpHost.cs
danegsta Mar 7, 2026
12754bc
Add logging if we detect the app host is running with an untrusted de…
Copilot Mar 7, 2026
db607d3
Fix failing test after log message updated to reference aka.ms/aspire…
Copilot Mar 7, 2026
1f08c4e
Replace "ASP.NET Core Development Certificate" with "development cert…
Copilot Mar 8, 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);
}
}
}
36 changes: 36 additions & 0 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 @@ -424,6 +425,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 @@ -433,6 +435,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 @@ -442,12 +445,45 @@ 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>())
Comment thread
danegsta marked this conversation as resolved.
{
// If the dashboard has an HTTPS endpoint and we haven't already applied an HTTPS certificate configuration (no HttpsCertificateConfigurationCallbackAnnotation),
// apply a default configuration with a valid trusted dev cert instance.
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 =>
{
// Ensure we use a trusted developer certificate (Kestrel selects the latest certificate, which may not be trusted after an SDK update).
// There can be issues referencing an exported PEM key pair on MacOS, so we the PFX version of the certificate here.
ctx.EnvironmentVariables["Kestrel__Certificates__Default__Path"] = ctx.PfxPath;
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;
Expand Down
104 changes: 103 additions & 1 deletion src/Aspire.Hosting/Dcp/DcpHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@
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 +32,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 +54,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 +67,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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why in the DCP host? Doesn't seem DCP related to me.

What about doing it in an app host event? For example, Seb added detection for whether secrets are disable and you have persistent container in an app host event.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was purely because it's the place we're doing existing startup runtime dependency checks (the container health check also lives here). We may be able to decide on a better location, but for now this is a location of convenience.

EnsureDcpHostRunning();
}

Expand Down Expand Up @@ -122,6 +135,63 @@ internal async Task EnsureDcpContainerRuntimeAsync(CancellationToken cancellatio
}
}

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

try
{
// If no resources use HTTPS/TLS, there's no need to warn about untrusted dev certificates.
if (!_applicationModel.Resources.Any(ResourceUsesTls))
{
return;
}

// Check and warn if no trusted dev certs exist, or if a newer untrusted cert was detected
var hasNewerUntrustedCert = _developerCertificateService.LatestCertificateIsUntrusted;
var hasNoTrustedCerts = _developerCertificateService.Certificates.Count == 0;

if (hasNoTrustedCerts || hasNewerUntrustedCert)
{
Comment thread
danegsta marked this conversation as resolved.
string title;
string message;

if (hasNoTrustedCerts)
{
title = InteractionStrings.NoDeveloperCertificateTrustedTitle;
message = InteractionStrings.NoDeveloperCertificateTrustedMessage;
_logger.LogWarning("No trusted Aspire development certificate was found. See https://aka.ms/aspire/devcerts for more information.");
}
else
{
title = InteractionStrings.DeveloperCertificateNotFullyTrustedTitle;
message = InteractionStrings.DeveloperCertificateNotFullyTrustedMessage;
_logger.LogWarning("The most recent development certificate isn't fully trusted. See https://aka.ms/aspire/devcerts for more information.");
}

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

// 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 @@ -474,6 +544,38 @@ private static bool IsContainerRuntimeHealthy(DcpInfo dcpInfo)
var running = dcpInfo.Containers?.Running ?? false;
return installed && running;
}

/// <summary>
/// Determines whether a resource uses HTTPS/TLS by checking for HTTPS endpoint annotations
/// or active HTTPS certificate configuration callbacks that haven't been disabled.
/// </summary>
private static bool ResourceUsesTls(IResource resource)
{
// Check if the resource has any HTTPS endpoints
if (resource.Annotations.OfType<EndpointAnnotation>().Any(e => e.UriScheme is "https"))
{
return true;
}

// Check if the resource has an HTTPS certificate configuration callback that hasn't been
// disabled via WithoutHttpsCertificate(). HttpsCertificateAnnotation has no effect without
// HttpsCertificateConfigurationCallbackAnnotation, so it's only checked as a filter here.
if (resource.Annotations.OfType<HttpsCertificateConfigurationCallbackAnnotation>().Any())
{
// The callback is present. Check if it's been disabled by WithoutHttpsCertificate()
// which sets UseDeveloperCertificate = false and Certificate = null.
if (resource.TryGetLastAnnotation<HttpsCertificateAnnotation>(out var certAnnotation)
&& certAnnotation.UseDeveloperCertificate is false or null
&& certAnnotation.Certificate is null)
{
return false;
}

return true;
}

return false;
}
}

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