Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4b73479
Add initial support for custom certificate trust
danegsta Oct 7, 2025
efe6a96
Update src/Aspire.Hosting/Dcp/DcpExecutor.cs
danegsta Oct 7, 2025
a42e923
Update src/Aspire.Hosting/Dcp/DcpExecutor.cs
danegsta Oct 7, 2025
0886e9e
Update src/Aspire.Hosting/Utils/X509Certificate2Extensions.cs
danegsta Oct 7, 2025
88c2eed
Fix failing DCP tests
danegsta Oct 7, 2025
92467b2
Merge remote-tracking branch 'upstream/main' into danegsta/trustCerts
danegsta Oct 8, 2025
c6a94a2
Fix tests that don't expect custom certificates in output
danegsta Oct 8, 2025
4ab71a5
Set as string
danegsta Oct 8, 2025
987b977
Skip automatic config on conflict with manual config
danegsta Oct 9, 2025
aa61232
Update yarp tests
danegsta Oct 9, 2025
91c924e
Fix another failing test after change
danegsta Oct 9, 2025
5a07be9
Add test cases for default container cert behavior
danegsta Oct 9, 2025
757c69a
Use a list
danegsta Oct 10, 2025
51f2648
Fix last failing test
danegsta Oct 10, 2025
8a1651e
Add examples to new API and additional helper method
danegsta Oct 10, 2025
c7c9e7b
Respond to PR feedback and ensure nodejs config works
danegsta Oct 13, 2025
54ef7fb
Fix DcpExecutor test
danegsta Oct 13, 2025
bc1c117
Enable python https instrumentation
danegsta Oct 13, 2025
d42ed49
Remove extra using
danegsta Oct 13, 2025
dd1ba27
Add container certificate trust config
danegsta Oct 13, 2025
4d34591
Merge remote-tracking branch 'upstream/main' into danegsta/trustCerts
danegsta Oct 13, 2025
0434472
Fix failing tests due to options change
danegsta Oct 14, 2025
16d7867
Respond to PR comments
danegsta Oct 14, 2025
c2b7a58
Add helper for loading certificates from an X509Store
danegsta Oct 14, 2025
87c3baa
Add some additional examples
danegsta Oct 14, 2025
3501ac2
Merge branch 'main' into danegsta/trustCerts
danegsta Oct 14, 2025
8e9e267
Exclude dev certs from test
danegsta Oct 14, 2025
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"launchBrowser": true,
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:15887;http://localhost:15888",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:16037",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:17037"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
Expand Down
15 changes: 14 additions & 1 deletion src/Aspire.Hosting.NodeJs/NodeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,18 @@ public static IResourceBuilder<NodeAppResource> AddNpmApp(this IDistributedAppli

private static IResourceBuilder<NodeAppResource> WithNodeDefaults(this IResourceBuilder<NodeAppResource> builder) =>
builder.WithOtlpExporter()
.WithEnvironment("NODE_ENV", builder.ApplicationBuilder.Environment.IsDevelopment() ? "development" : "production");
.WithEnvironment("NODE_ENV", builder.ApplicationBuilder.Environment.IsDevelopment() ? "development" : "production")
.WithExecutableCertificateTrustCallback((ctx) =>
{
if (ctx.Scope == CustomCertificateAuthoritiesScope.Append)
{
ctx.CertificateBundleEnvironment.Add("NODE_EXTRA_CA_CERTS");
}
else
{
ctx.CertificateTrustArguments.Add("--use-openssl-ca");
}

return Task.CompletedTask;
});
}
32 changes: 24 additions & 8 deletions src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static class PythonAppResourceBuilderExtensions
/// Use <c>WithArgs</c> to pass arguments to the script.
/// </para>
/// <para>
/// The virtual environment must be initialized before running the app. To setup a virtual environment use the
/// The virtual environment must be initialized before running the app. To setup a virtual environment use the
/// <c>python -m venv .venv</c> command in the app directory.
/// </para>
/// <para>
Expand Down Expand Up @@ -161,6 +161,22 @@ private static IResourceBuilder<PythonAppResource> AddPythonAppCore(
context.EnvironmentVariables["OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED"] = "true";
});

// Configure required environment variables for custom certificate trust when running as an executable
resourceBuilder.WithExecutableCertificateTrustCallback(ctx =>
{
if (ctx.Scope == CustomCertificateAuthoritiesScope.Override)
{
// See: https://docs.python-requests.org/en/latest/user/advanced/#ssl-cert-verification
ctx.CertificateBundleEnvironment.Add("REQUESTS_CA_BUNDLE");
}

// Override default opentelemetry-python certificate bundle path
// See: https://opentelemetry-python.readthedocs.io/en/latest/exporter/otlp/otlp.html#module-opentelemetry.exporter.otlp
ctx.CertificateBundleEnvironment.Add("OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE");

return Task.CompletedTask;
});

resourceBuilder.WithVSCodeDebugSupport(mode => new PythonLaunchConfiguration { ProgramPath = Path.Join(appDirectory, scriptPath), Mode = mode }, "ms-python.python", ctx =>
{
ctx.Args.RemoveAt(0); // The first argument when running from command line is the entrypoint file.
Expand Down Expand Up @@ -243,7 +259,7 @@ public static IResourceBuilder<PythonAppResource> WithVirtualEnvironment(
/// </para>
/// <para>
/// UV (https://github.com/astral-sh/uv) is a modern Python package manager written in Rust that can manage virtual environments
/// and dependencies with significantly faster performance than traditional tools. The <c>uv sync</c> command ensures that the virtual
/// and dependencies with significantly faster performance than traditional tools. The <c>uv sync</c> command ensures that the virtual
/// environment exists and all dependencies specified in pyproject.toml are installed and synchronized.
/// </para>
/// <para>
Expand All @@ -255,11 +271,11 @@ public static IResourceBuilder<PythonAppResource> WithVirtualEnvironment(
/// Add a Python app with automatic UV environment setup:
/// <code lang="csharp">
/// var builder = DistributedApplication.CreateBuilder(args);
///
///
/// var python = builder.AddPythonApp("api", "../python-api", "main.py")
/// .WithUvEnvironment() // Automatically runs 'uv sync' before starting the app
/// .WithHttpEndpoint(port: 5000);
///
///
/// builder.Build().Run();
/// </code>
/// </example>
Expand All @@ -273,21 +289,21 @@ public static IResourceBuilder<T> WithUvEnvironment<T>(this IResourceBuilder<T>
ArgumentNullException.ThrowIfNull(builder);

var uvEnvironmentName = $"{builder.Resource.Name}-uv-environment";

// Check if the UV environment resource already exists
var existingResource = builder.ApplicationBuilder.Resources
.FirstOrDefault(r => string.Equals(r.Name, uvEnvironmentName, StringComparison.OrdinalIgnoreCase));

IResourceBuilder<PythonUvEnvironmentResource> uvBuilder;

if (existingResource is not null)
{
// Resource already exists, return a builder for it
if (existingResource is not PythonUvEnvironmentResource uvEnvironmentResource)
{
throw new DistributedApplicationException($"Cannot add UV environment resource with name '{uvEnvironmentName}' because a resource of type '{existingResource.GetType()}' with that name already exists.");
}

uvBuilder = builder.ApplicationBuilder.CreateResourceBuilder(uvEnvironmentResource);
}
else
Expand Down
18 changes: 14 additions & 4 deletions src/Aspire.Hosting.Yarp/YarpResourceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Yarp;
using Microsoft.Extensions.DependencyInjection;

namespace Aspire.Hosting;

Expand Down Expand Up @@ -37,10 +38,19 @@ public static IResourceBuilder<YarpResource> AddYarp(

if (builder.ExecutionContext.IsRunMode)
{
// YARP will not trust the cert used by Aspire otlp endpoint when running locally
// The Aspire otlp endpoint uses the dev cert, only valid for localhost, but from the container
// perspective, the url will be something like https://docker.host.internal, so it will NOT be valid.
yarpBuilder.WithEnvironment("YARP_UNSAFE_OLTP_CERT_ACCEPT_ANY_SERVER_CERTIFICATE", "true");
yarpBuilder.WithEnvironment(ctx =>
{
var developerCertificateService = ctx.ExecutionContext.ServiceProvider.GetRequiredService<IDeveloperCertificateService>();
if (!developerCertificateService.SupportsContainerTrust)
{
// On systems without the ASP.NET DevCert updates introduced in .NET 10, YARP will not trust the cert used
// by Aspire otlp endpoint when running locally. The Aspire otlp endpoint uses the dev cert, and prior to
// .NET 10, it was only valid for localhost, but from the container perspective, the url will be something
// like https://docker.host.internal, so it will NOT be valid. This is not necessary when using the latest
// dev cert.
ctx.EnvironmentVariables["YARP_UNSAFE_OLTP_CERT_ACCEPT_ANY_SERVER_CERTIFICATE"] = "true";
}
});
}

yarpBuilder.WithEnvironment(ctx =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Defines the scope of custom certificate authorities for a resource. The default is <see cref="Append"/>.
/// </summary>
public enum CustomCertificateAuthoritiesScope
{
/// <summary>
/// Append the specified certificate authorities to the default set of trusted CAs for a resource.
/// </summary>
Append,
/// <summary>
/// Replace the default set of trusted CAs for a resource with the specified certificate authorities.
/// </summary>
Override,
}

/// <summary>
/// An annotation that indicates a resource is referencing a certificate authority collection.
/// </summary>
public sealed class CertificateAuthorityCollectionAnnotation : IResourceAnnotation
{
/// <summary>
/// Gets the <see cref="global::CertificateAuthorityCollection"/> that is being referenced.
/// </summary>
public List<CertificateAuthorityCollection> CertificateAuthorityCollections { get; internal set; } = new List<CertificateAuthorityCollection>();

/// <summary>
/// Gets a value indicating whether platform developer certificates should be considered trusted.
/// </summary>
public bool? TrustDeveloperCertificates { get; internal set; }

/// <summary>
/// Gets a value indicating whether the resource should attempt to override its default CA trust behavior in
/// favor of the provided certificates (not all resources will support this).
/// </summary>
public CustomCertificateAuthoritiesScope? Scope { get; internal set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Security.Cryptography.X509Certificates;
using Aspire.Hosting.ApplicationModel;

/// <summary>
/// Represents a collection of certificate authorities within the application model.
/// </summary>
/// <remarks>
/// This class implements <see cref="IResourceWithoutLifetime"/> and provides access to
/// the name and annotations associated with the certificate authority collection.
/// </remarks>
public class CertificateAuthorityCollection : Resource
{
/// <summary>
/// Initializes a new instance of the <see cref="CertificateAuthorityCollection"/> class with the specified name.
/// </summary>
/// <param name="name">The name of the certificate authority collection resource.</param>
public CertificateAuthorityCollection(string name) : base(name)
{
ArgumentNullException.ThrowIfNull(name);
}

/// <summary>
/// Gets the <see cref="X509Certificate2Collection"/> of certificates for this resource.
/// </summary>
public X509Certificate2Collection Certificates { get; } = new();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Security.Cryptography.X509Certificates;

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Provides extension methods for <see cref="CertificateAuthorityCollection"/>.
/// </summary>
public static class CertificateAuthorityCollectionResourceExtensions
{
/// <summary>
/// Adds a new <see cref="CertificateAuthorityCollection"/> to the application model.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name of the certificate authority collection resource.</param>
/// <returns>An <see cref="IResourceBuilder{CertificateAuthorityCollectionResource}"/> instance.</returns>
public static IResourceBuilder<CertificateAuthorityCollection> AddCertificateAuthorityCollection(this IDistributedApplicationBuilder builder, [ResourceName] string name)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(name);

var resource = new CertificateAuthorityCollection(name);
return builder.AddResource(resource);
}

/// <summary>
/// Adds a certificate to the <see cref="CertificateAuthorityCollection.Certificates"/> collection.
/// </summary>
/// <param name="builder">The <see cref="IResourceBuilder{CertificateAuthorityCollectionResource}"/>.</param>
/// <param name="certificate">The certificate to add.</param>
/// <returns>The updated <see cref="IResourceBuilder{CertificateAuthorityCollectionResource}"/>.</returns>
public static IResourceBuilder<CertificateAuthorityCollection> WithCertificate(this IResourceBuilder<CertificateAuthorityCollection> builder, X509Certificate2 certificate)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(certificate);

builder.Resource.Certificates.Add(certificate);
return builder;
}

/// <summary>
/// Adds a collection of certificates to the <see cref="CertificateAuthorityCollection.Certificates"/> collection.
/// </summary>
/// <param name="builder">The <see cref="IResourceBuilder{CertificateAuthorityCollectionResource}"/>.</param>
/// <param name="certificates">The collection of certificates to add.</param>
/// <returns>The updated <see cref="IResourceBuilder{CertificateAuthorityCollectionResource}"/>.</returns>
public static IResourceBuilder<CertificateAuthorityCollection> WithCertificates(this IResourceBuilder<CertificateAuthorityCollection> builder, X509Certificate2Collection certificates)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(certificates);

builder.Resource.Certificates.AddRange(certificates);
return builder;
}

/// <summary>
/// Adds a collection of certificates to the <see cref="CertificateAuthorityCollection.Certificates"/> collection.
/// </summary>
/// <param name="builder">The <see cref="IResourceBuilder{CertificateAuthorityCollectionResource}"/>.</param>
/// <param name="certificates">The collection of certificates to add.</param>
/// <returns>The updated <see cref="IResourceBuilder{CertificateAuthorityCollectionResource}"/>.</returns>
public static IResourceBuilder<CertificateAuthorityCollection> WithCertificates(this IResourceBuilder<CertificateAuthorityCollection> builder, IEnumerable<X509Certificate2> certificates)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(certificates);

builder.Resource.Certificates.AddRange(certificates.ToArray());
return builder;
}

/// <summary>
/// Adds certificates from a certificate store to the <see cref="CertificateAuthorityCollection.Certificates"/> collection.
/// </summary>
/// <param name="builder">The <see cref="IResourceBuilder{CertificateAuthorityCollectionResource}"/>.</param>
/// <param name="storeName">The name of the certificate store.</param>
/// <param name="storeLocation">The location of the certificate store.</param>
/// <param name="filter">An optional filter to apply to the certificates.</param>
/// <returns>The updated <see cref="IResourceBuilder{CertificateAuthorityCollectionResource}"/>.</returns>
/// <remarks>
/// <example>
/// This example adds all certificates from the "Root" store in the "LocalMachine" location.
/// <code language="csharp">
/// builder.AddCertificateAuthorityCollection("my-ca")
/// .WithCertificatesFromStore(StoreName.Root, StoreLocation.LocalMachine);
/// </code>
/// </example>
/// <example>
/// This example adds only certificates that are not expired from the "My" store in the "CurrentUser" location.
/// <code language="csharp">
/// builder.AddCertificateAuthorityCollection("my-ca")
/// .WithCertificatesFromStore(StoreName.My, StoreLocation.CurrentUser, c => c.NotAfter &gt; DateTime.UtcNow);
/// </code>
/// </example>
/// </remarks>
public static IResourceBuilder<CertificateAuthorityCollection> WithCertificatesFromStore(this IResourceBuilder<CertificateAuthorityCollection> builder, StoreName storeName, StoreLocation storeLocation, Func<X509Certificate2, bool>? filter = null)
{
ArgumentNullException.ThrowIfNull(builder);

using var store = new X509Store(storeName, storeLocation);
store.Open(OpenFlags.ReadOnly);
var certificates = store.Certificates as IEnumerable<X509Certificate2>;
if (filter != null)
{
certificates = certificates.Where(filter);
}
builder.Resource.Certificates.AddRange(certificates.ToArray());
return builder;
}

/// <summary>
/// Adds certificates from a PEM file to the <see cref="CertificateAuthorityCollection.Certificates"/> collection.
/// </summary>
/// <param name="builder">The <see cref="IResourceBuilder{CertificateAuthorityCollection}"/>.</param>
/// <param name="pemFilePath">The path to the PEM file.</param>
/// <param name="filter">An optional filter to apply to the loaded certificates before they are added to the collection.</param>
/// <returns>The updated <see cref="IResourceBuilder{CertificateAuthorityCollection}"/>.</returns>
/// <remarks>
/// <example>
/// This example adds certificates from a PEM file located at "../path/to/certificates.pem".
/// <code language="csharp">
/// builder.AddCertificateAuthorityCollection("my-ca")
/// .WithCertificatesFromFile("../path/to/certificates.pem");
/// </code>
/// </example>
/// <example>
/// This example adds only certificates that are not expired from a PEM file located at "../path/to/certificates.pem".
/// <code language="csharp">
/// builder.AddCertificateAuthorityCollection("my-ca")
/// .WithCertificatesFromFile("../path/to/certificates.pem", c => c.NotAfter &gt; DateTime.UtcNow);
/// </code>
/// </example>
/// </remarks>
public static IResourceBuilder<CertificateAuthorityCollection> WithCertificatesFromFile(this IResourceBuilder<CertificateAuthorityCollection> builder, string pemFilePath, Func<X509Certificate2, bool>? filter = null)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(pemFilePath);

var certificates = new X509Certificate2Collection();
certificates.ImportFromPemFile(pemFilePath);
if (filter != null)
{
builder.WithCertificates(certificates.Where(filter).ToArray());
}
else
{
builder.WithCertificates(certificates);
}

return builder;
}
}
Loading