diff --git a/docs/specs/dashboard-http-api.md b/docs/specs/dashboard-http-api.md
index e1daf534b38..4b6e62c8882 100644
--- a/docs/specs/dashboard-http-api.md
+++ b/docs/specs/dashboard-http-api.md
@@ -49,7 +49,7 @@ The API can be enabled/disabled and configured via `Dashboard:Api` settings:
| Setting | Values | Default | Description |
|---------|--------|---------|-------------|
-| `Enabled` | `true`, `false` | `true` | Whether the Telemetry HTTP API is enabled |
+| `Enabled` | `true`, `false` | `true` when frontend is unsecured or an API key is configured; `false` when the frontend requires authentication and no API key is set | Whether the Telemetry HTTP API is enabled |
| `AuthMode` | `ApiKey`, `Unsecured` | `Unsecured` | Authentication mode for the API |
| `PrimaryApiKey` | string | - | API key for authentication (required when `AuthMode=ApiKey`) |
| `SecondaryApiKey` | string | - | Optional secondary API key for key rotation |
@@ -58,6 +58,7 @@ The API can be enabled/disabled and configured via `Dashboard:Api` settings:
- The API shares the same port as the Dashboard frontend (default: 18888).
- Hosters may set `Enabled: false` to disable the API for security.
+- When the frontend requires authentication (e.g., OpenID Connect) and no API key is configured, the API is disabled by default. Set `Enabled: true` explicitly to override.
- API keys are shared with MCP configuration—setting either configures both.
- When running in unsecured mode, a warning is logged on first API request.
diff --git a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs
index 7a14a80af5e..d76daf200a8 100644
--- a/src/Aspire.Dashboard/Configuration/DashboardOptions.cs
+++ b/src/Aspire.Dashboard/Configuration/DashboardOptions.cs
@@ -149,8 +149,10 @@ public sealed class ApiOptions
///
/// Gets or sets whether the Telemetry HTTP API is enabled.
- /// When false, the /api/telemetry/* endpoints are not registered.
- /// Defaults to true.
+ /// When , the /api/telemetry/* endpoints are not registered.
+ /// Defaults to when the dashboard frontend is unsecured or an API key is configured.
+ /// Defaults to when the frontend requires authentication (e.g., OpenID Connect
+ /// or BrowserToken) and no API key is configured, to prevent unauthenticated access to telemetry data.
///
public bool? Enabled { get; set; }
diff --git a/src/Aspire.Dashboard/Configuration/PostConfigureDashboardOptions.cs b/src/Aspire.Dashboard/Configuration/PostConfigureDashboardOptions.cs
index b166dfda5b4..717a400c119 100644
--- a/src/Aspire.Dashboard/Configuration/PostConfigureDashboardOptions.cs
+++ b/src/Aspire.Dashboard/Configuration/PostConfigureDashboardOptions.cs
@@ -73,6 +73,16 @@ public void PostConfigure(string? name, DashboardOptions options)
// If an API key is configured, default to ApiKey auth mode instead of Unsecured.
options.Mcp.AuthMode ??= string.IsNullOrEmpty(options.Mcp.PrimaryApiKey) ? McpAuthMode.Unsecured : McpAuthMode.ApiKey;
options.Api.AuthMode ??= string.IsNullOrEmpty(options.Api.PrimaryApiKey) ? ApiAuthMode.Unsecured : ApiAuthMode.ApiKey;
+
+ // When the frontend requires authentication (e.g. OpenID Connect or BrowserToken) but the API
+ // has no key configured (and would therefore default to Unsecured), disable the API by default
+ // to prevent unauthenticated access to telemetry data on the same port.
+ if (options.Api.Enabled is null &&
+ options.Frontend.AuthMode is not FrontendAuthMode.Unsecured &&
+ options.Api.AuthMode is ApiAuthMode.Unsecured)
+ {
+ options.Api.Enabled = false;
+ }
}
if (options.Frontend.AuthMode == FrontendAuthMode.BrowserToken && string.IsNullOrEmpty(options.Frontend.BrowserToken))
diff --git a/tests/Aspire.Dashboard.Tests/Integration/TelemetryApiTests.cs b/tests/Aspire.Dashboard.Tests/Integration/TelemetryApiTests.cs
index 822559e11ff..05f67c3801c 100644
--- a/tests/Aspire.Dashboard.Tests/Integration/TelemetryApiTests.cs
+++ b/tests/Aspire.Dashboard.Tests/Integration/TelemetryApiTests.cs
@@ -82,6 +82,86 @@ public async Task Configuration_ApiKeyExplicit_OverridesMcp()
Assert.Equal(apiKey.Length, options.Api.GetPrimaryApiKeyBytesOrNull()!.Length);
}
+ [Fact]
+ public async Task Configuration_ApiDefaultsToDisabled_WhenFrontendIsBrowserToken()
+ {
+ // Arrange - BrowserToken frontend with no API key configured
+ await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
+ {
+ config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.BrowserToken.ToString();
+ // Don't set any Api config — no API key
+ });
+ await app.StartAsync().DefaultTimeout();
+
+ // Assert - API should default to disabled when frontend has auth but no API key
+ var options = app.Services.GetRequiredService>().CurrentValue;
+ Assert.False(options.Api.Enabled);
+ }
+
+ [Fact]
+ public async Task Configuration_ApiStaysEnabled_WhenExplicitlyEnabled()
+ {
+ // Arrange - BrowserToken frontend, explicitly enable API with API key auth
+ var apiKey = "TestKey123!";
+ await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
+ {
+ config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.BrowserToken.ToString();
+ config[DashboardConfigNames.DashboardApiEnabledName.ConfigKey] = "true";
+ config[DashboardConfigNames.DashboardApiAuthModeName.ConfigKey] = ApiAuthMode.ApiKey.ToString();
+ config[DashboardConfigNames.DashboardApiPrimaryApiKeyName.ConfigKey] = apiKey;
+ });
+ await app.StartAsync().DefaultTimeout();
+
+ // Assert - API should be enabled because explicitly configured
+ var options = app.Services.GetRequiredService>().CurrentValue;
+ Assert.True(options.Api.Enabled);
+
+ using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
+ httpClient.DefaultRequestHeaders.TryAddWithoutValidation(ApiAuthenticationHandler.ApiKeyHeaderName, apiKey);
+ var response = await httpClient.GetAsync("/api/telemetry/spans").DefaultTimeout();
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task Configuration_ApiStaysEnabled_WhenApiKeyConfigured()
+ {
+ // Arrange - BrowserToken frontend with API key configured (should auto-enable ApiKey auth mode)
+ var apiKey = "TestKey123!";
+ await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
+ {
+ config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.BrowserToken.ToString();
+ config[DashboardConfigNames.DashboardApiPrimaryApiKeyName.ConfigKey] = apiKey;
+ });
+ await app.StartAsync().DefaultTimeout();
+
+ // Assert - API should be enabled because an API key is configured (AuthMode defaults to ApiKey)
+ var options = app.Services.GetRequiredService>().CurrentValue;
+ Assert.Equal(ApiAuthMode.ApiKey, options.Api.AuthMode);
+ Assert.NotEqual(false, options.Api.Enabled);
+ }
+
+ [Fact]
+ public async Task Configuration_ApiStaysEnabled_WhenFrontendUnsecured()
+ {
+ // Arrange - Unsecured frontend with no API key configured
+ await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(_testOutputHelper, config =>
+ {
+ config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = FrontendAuthMode.Unsecured.ToString();
+ // Don't set any Api config
+ });
+ await app.StartAsync().DefaultTimeout();
+
+ // Assert - API should remain enabled when frontend is also unsecured
+ var options = app.Services.GetRequiredService>().CurrentValue;
+ Assert.NotEqual(false, options.Api.Enabled);
+ Assert.Equal(ApiAuthMode.Unsecured, options.Api.AuthMode);
+
+ // Verify endpoints work
+ using var httpClient = IntegrationTestHelpers.CreateHttpClient($"http://{app.FrontendSingleEndPointAccessor().EndPoint}");
+ var response = await httpClient.GetAsync("/api/telemetry/spans").DefaultTimeout();
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
#endregion
[Fact]