Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion src/Aspire.Hosting.NodeJs/NodeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,14 @@ private static IResourceBuilder<NodeAppResource> WithNodeDefaults(this IResource
.WithEnvironment("NODE_ENV", builder.ApplicationBuilder.Environment.IsDevelopment() ? "development" : "production")
.WithExecutableCertificateTrustCallback((ctx) =>
{
if (ctx.Scope == CustomCertificateAuthoritiesScope.Append)
if (ctx.Scope == CertificateTrustScope.Append)
{
ctx.CertificateBundleEnvironment.Add("NODE_EXTRA_CA_CERTS");
}
else
{
ctx.CertificateTrustArguments.Add("--use-openssl-ca");
ctx.CertificateBundleEnvironment.Add("SSL_CERT_FILE");
}

return Task.CompletedTask;
Expand Down
33 changes: 22 additions & 11 deletions src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.ComponentModel;
using System.Runtime.CompilerServices;

#pragma warning disable ASPIREEXTENSION001
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Python;
Expand Down Expand Up @@ -281,20 +282,30 @@ private static IResourceBuilder<PythonAppResource> AddPythonAppCore(
});

// Configure required environment variables for custom certificate trust when running as an executable
resourceBuilder.WithExecutableCertificateTrustCallback(ctx =>
{
if (ctx.Scope == CustomCertificateAuthoritiesScope.Override)
// Python defaults to using System scope to allow combining custom CAs with system CAs as there's no clean
// way to simply append additional certificates to default Python trust stores such as certifi.
resourceBuilder
.WithCertificateTrustScope(CertificateTrustScope.System)
.WithExecutableCertificateTrustCallback(ctx =>
{
// See: https://docs.python-requests.org/en/latest/user/advanced/#ssl-cert-verification
ctx.CertificateBundleEnvironment.Add("REQUESTS_CA_BUNDLE");
}
if (ctx.Scope != CertificateTrustScope.Append)
{
// Override default certificates path for the requests module.
// See: https://docs.python-requests.org/en/latest/user/advanced/#ssl-cert-verification
ctx.CertificateBundleEnvironment.Add("REQUESTS_CA_BUNDLE");

// Override default certificates path for Python modules that honor OpenSSL style paths.
// This has been tested with urllib, urllib3, httpx, and aiohttp.
// See: https://docs.openssl.org/3.0/man3/SSL_CTX_load_verify_locations/#description
ctx.CertificateBundleEnvironment.Add("SSL_CERT_FILE");
}

// 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_CERTIFICATE");
// 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_CERTIFICATE");

return Task.CompletedTask;
});
return Task.CompletedTask;
});

// VS Code debug support - only applicable for Script and Module types
if (entrypointType is EntrypointType.Script or EntrypointType.Module)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,32 @@
namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Defines the scope of custom certificate authorities for a resource. The default is <see cref="Append"/>.
/// Defines the scope of custom certificate authorities for a resource. The default scope for most resources
/// is <see cref="Append"/>, but some resources may choose to override this default behavior.
/// </summary>
public enum CustomCertificateAuthoritiesScope
public enum CertificateTrustScope
{
/// <summary>
/// Append the specified certificate authorities to the default set of trusted CAs for a resource.
/// Append the specified certificate authorities to the default set of trusted CAs for a resource. Not all
/// resources support this mode, in which case custom certificate authorities may not be applied. In that case,
/// consider using <see cref="Override"/> or <see cref="System"/> instead.
/// </summary>
Append,
/// <summary>
/// Replace the default set of trusted CAs for a resource with the specified certificate authorities.
/// </summary>
Override,
/// <summary>
/// Attempt to configure the resource to trust the default system certificate authorities in addition to
/// any configured custom certificate trust. This mode is useful for resources that don't otherwise
/// allow appending to their default trusted certificate authorities but do allow overriding the set
/// of trusted certificates (e.g. Python, Rust, etc.).
/// </summary>
System,
/// <summary>
/// Disable all custom certificate authority configuration for a resource.
/// </summary>
None,
}

/// <summary>
Expand All @@ -37,5 +51,5 @@ public sealed class CertificateAuthorityCollectionAnnotation : IResourceAnnotati
/// 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; }
public CertificateTrustScope? Scope { get; internal set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ public sealed class ContainerCertificateTrustCallbackAnnotationContext
public required IResource Resource { get; init; }

/// <summary>
/// Gets the <see cref="CustomCertificateAuthoritiesScope"/> of trust for the resource.
/// Gets the <see cref="CertificateTrustScope"/> of trust for the resource.
/// </summary>
public required CustomCertificateAuthoritiesScope Scope { get; init; }
public required CertificateTrustScope Scope { get; init; }

/// <summary>
/// Gets the <see cref="X509Certificate2Collection"/> of certificates for this resource.
Expand Down Expand Up @@ -72,7 +72,7 @@ public sealed class ContainerCertificateTrustCallbackAnnotationContext

/// <summary>
/// List of default certificate bundle files in the container that will be replaced if the resource scope of trust is
/// set to <see cref="CustomCertificateAuthoritiesScope.Override"/>. Defaults to common Linux paths for CA certificates
/// set to <see cref="CertificateTrustScope.Override"/>. Defaults to common Linux paths for CA certificates
/// to maximize compatibility, but can be overriden with specific paths for a given resource if needed.
/// See: https://go.dev/src/crypto/x509/root_linux.go
/// </summary>
Expand All @@ -94,7 +94,7 @@ public sealed class ContainerCertificateTrustCallbackAnnotationContext

/// <summary>
/// List of default certificate directories in a container that should be appended to the custom certificate directories in
/// <see cref="CustomCertificateAuthoritiesScope.Append"/> mode. Defaults to common Linux paths for CA certificates.
/// <see cref="CertificateTrustScope.Append"/> mode. Defaults to common Linux paths for CA certificates.
/// See: https://go.dev/src/crypto/x509/root_linux.go
/// </summary>
public List<string> DefaultContainerCertificatesDirectoryPaths { get; } = new()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ public sealed class ExecutableCertificateTrustCallbackAnnotationContext
public required IResource Resource { get; init; }

/// <summary>
/// Gets the <see cref="CustomCertificateAuthoritiesScope"/> of trust for the resource.
/// Gets the <see cref="CertificateTrustScope"/> of trust for the resource.
/// </summary>
public required CustomCertificateAuthoritiesScope Scope { get; init; }
public required CertificateTrustScope Scope { get; init; }

/// <summary>
/// Gets the <see cref="X509Certificate2Collection"/> of certificates for this resource.
Expand Down
40 changes: 32 additions & 8 deletions src/Aspire.Hosting/Dcp/DcpExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2055,7 +2055,7 @@ await modelResource.ProcessContainerRuntimeArgValues(
bool trustDevCert = _distributedApplicationOptions.TrustDeveloperCertificate;

var certificates = new X509Certificate2Collection();
var scope = CustomCertificateAuthoritiesScope.Append;
var scope = CertificateTrustScope.Append;
if (modelResource.TryGetLastAnnotation<CertificateAuthorityCollectionAnnotation>(out var caAnnotation))
{
foreach (var certCollection in caAnnotation.CertificateAuthorityCollections)
Expand All @@ -2067,6 +2067,17 @@ await modelResource.ProcessContainerRuntimeArgValues(
scope = caAnnotation.Scope.GetValueOrDefault(scope);
}

if (scope == CertificateTrustScope.None)
{
return (new List<string>(), new List<EnvVar>(), false);
}

if (scope == CertificateTrustScope.System)
{
// Read the system root certificates and add them to the collection
certificates.AddRootCertificates();
}

if (trustDevCert)
{
foreach (var cert in _developerCertificateService.Certificates)
Expand Down Expand Up @@ -2164,7 +2175,7 @@ await modelResource.ProcessContainerRuntimeArgValues(
bool trustDevCert = _distributedApplicationOptions.TrustDeveloperCertificate;

var certificates = new X509Certificate2Collection();
var scope = CustomCertificateAuthoritiesScope.Append;
var scope = CertificateTrustScope.Append;
if (modelResource.TryGetLastAnnotation<CertificateAuthorityCollectionAnnotation>(out var caAnnotation))
{
foreach (var certCollection in caAnnotation.CertificateAuthorityCollections)
Expand All @@ -2176,6 +2187,18 @@ await modelResource.ProcessContainerRuntimeArgValues(
scope = caAnnotation.Scope.GetValueOrDefault(scope);
}

if (scope == CertificateTrustScope.None)
{
// Resource has disabled custom certificate authorities
return (new List<string>(), new List<EnvVar>(), new List<ContainerCreateFileSystem>(), false);
}

if (scope == CertificateTrustScope.System)
{
// Read the system root certificates and add them to the collection
certificates.AddRootCertificates();
}

if (trustDevCert)
{
foreach (var cert in _developerCertificateService.Certificates)
Expand All @@ -2191,10 +2214,11 @@ await modelResource.ProcessContainerRuntimeArgValues(
Certificates = certificates,
CancellationToken = cancellationToken
};
if (scope == CustomCertificateAuthoritiesScope.Override)

if (scope != CertificateTrustScope.Append)
{
// Override default OpenSSL certificate bundle path resolution
// SSL_CERT_FILE is always added to the defaults when the scope is Override
// When Override or System scope is set (not Append), override the default OpenSSL certificate bundle path
// resolution by setting the SSL_CERT_FILE environment variable.
// See: https://docs.openssl.org/3.0/man3/SSL_CTX_load_verify_locations/#description
context.CertificateBundleEnvironment.Add("SSL_CERT_FILE");
}
Expand Down Expand Up @@ -2254,7 +2278,7 @@ await modelResource.ProcessContainerRuntimeArgValues(
}

var caDirEnvValue = caFilesPath;
if (scope == CustomCertificateAuthoritiesScope.Append)
if (scope == CertificateTrustScope.Append)
{
foreach (var defaultCaDir in context.DefaultContainerCertificatesDirectoryPaths)
{
Expand Down Expand Up @@ -2313,9 +2337,9 @@ await modelResource.ProcessContainerRuntimeArgValues(
],
});

if (scope == CustomCertificateAuthoritiesScope.Override)
if (scope != CertificateTrustScope.Append)
{
// If overriding the system CA bundle, then we want to copy our bundle to the well-known locations
// If overriding the default resource CA bundle, then we want to copy our bundle to the well-known locations
// used by common Linux distributions to make it easier to ensure applications pick it up.
foreach (var bundlePath in context.DefaultContainerCertificateAuthorityBundlePaths)
{
Expand Down
12 changes: 6 additions & 6 deletions src/Aspire.Hosting/ResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2156,16 +2156,16 @@ public static IResourceBuilder<TResource> WithDeveloperCertificateTrust<TResourc
}

/// <summary>
/// Sets the <see cref="CustomCertificateAuthoritiesScope"/> for custom certificate authorities associated with the resource.
/// Sets the <see cref="CertificateTrustScope"/> for custom certificate authorities associated with the resource.
/// </summary>
/// <typeparam name="TResource">The type of the resource.</typeparam>
/// <param name="builder">The resource builder.</param>
/// <param name="scope">The scope to apply to custom certificate authorities associated with the resource.</param>
/// <returns>The <see cref="IResourceBuilder{TResource}"/>.</returns>
/// <remarks>
/// The default scope is <see cref="CustomCertificateAuthoritiesScope.Append"/> which means that custom certificate authorities
/// should be appended to the default trusted certificate authorities for the resource. Setting the scope to
/// <see cref="CustomCertificateAuthoritiesScope.Override"/> indicates the set of certificates in referenced
/// The default scope if not overridden is <see cref="CertificateTrustScope.Append"/> which means that custom certificate
/// authorities should be appended to the default trusted certificate authorities for the resource. Setting the scope to
/// <see cref="CertificateTrustScope.Override"/> indicates the set of certificates in referenced
/// <see cref="CertificateAuthorityCollection"/> (and optionally Aspire developer certificiates) should be used as the
/// exclusive source of trust for a resource.
/// In all cases, this is a best effort implementation as not all resources support full customization of certificate
Expand All @@ -2178,11 +2178,11 @@ public static IResourceBuilder<TResource> WithDeveloperCertificateTrust<TResourc
///
/// var container = builder.AddContainer("my-service", "my-service:latest")
/// .WithCertificateAuthorityCollection(caCollection)
/// .WithCustomCertificateAuthoritiesScope(CustomCertificateAuthoritiesScope.Override);
/// .WithCertificateTrustScope(CertificateTrustScope.Override);
/// </code>
/// </example>
/// </remarks>
public static IResourceBuilder<TResource> WithCustomCertificateAuthoritiesScope<TResource>(this IResourceBuilder<TResource> builder, CustomCertificateAuthoritiesScope scope)
public static IResourceBuilder<TResource> WithCertificateTrustScope<TResource>(this IResourceBuilder<TResource> builder, CertificateTrustScope scope)
where TResource : IResourceWithEnvironment, IResourceWithArgs
{
ArgumentNullException.ThrowIfNull(builder);
Expand Down
22 changes: 22 additions & 0 deletions src/Aspire.Hosting/Utils/X509Certificate2Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,26 @@ public static bool SupportsContainerTrust(this X509Certificate2 certificate)

return true;
}

/// <summary>
/// Adds certificates from the system root stores to the specified collection.
/// </summary>
/// <param name="collection">The <see cref="X509Certificate2Collection"/> to add the certificates to.</param>
public static void AddRootCertificates(this X509Certificate2Collection collection)
{
ArgumentNullException.ThrowIfNull(collection);

var locations = new[]
{
(StoreName.Root, StoreLocation.CurrentUser),
(StoreName.Root, StoreLocation.LocalMachine),
};

foreach (var (storeName, storeLocation) in locations)
{
using var store = new X509Store(storeName, storeLocation);
store.Open(OpenFlags.ReadOnly);
collection.AddRange(store.Certificates);
}
}
}