save changes

This commit is contained in:
master
2026-02-17 00:51:35 +02:00
parent 70fdbfcf25
commit fb46a927ad
324 changed files with 4976 additions and 1499 deletions

View File

@@ -39,13 +39,20 @@ public sealed class IdentityHeaderPolicyMiddleware
"X-Stella-Project",
"X-Stella-Actor",
"X-Stella-Scopes",
// Headers used by downstream services in header-based auth mode
"X-Scopes",
"X-Tenant-Id",
// Raw claim headers (internal/legacy pass-through)
"sub",
"tid",
"scope",
"scp",
"cnf",
"cnf.jkt"
"cnf.jkt",
// Auth headers consumed by the gateway — strip before proxying
// so backends trust identity headers instead of re-validating JWT.
"Authorization",
"DPoP"
];
public IdentityHeaderPolicyMiddleware(
@@ -91,8 +98,18 @@ public sealed class IdentityHeaderPolicyMiddleware
private void StripReservedHeaders(HttpContext context)
{
var preserveAuthHeaders = _options.JwtPassthroughPrefixes.Count > 0
&& _options.JwtPassthroughPrefixes.Any(prefix =>
context.Request.Path.StartsWithSegments(prefix, StringComparison.OrdinalIgnoreCase));
foreach (var header in ReservedHeaders)
{
// Preserve Authorization/DPoP for routes that need JWT pass-through
if (preserveAuthHeaders && (header == "Authorization" || header == "DPoP"))
{
continue;
}
if (context.Request.Headers.ContainsKey(header))
{
_logger.LogDebug(
@@ -114,7 +131,7 @@ public sealed class IdentityHeaderPolicyMiddleware
// In AllowAnonymous mode the Gateway cannot validate identity claims.
// Pass through the client-provided tenant so the upstream service
// can validate it against the JWT's own tenant claim.
var passThruTenant = !string.IsNullOrWhiteSpace(clientTenant) ? clientTenant.Trim() : null;
var passThruTenant = !string.IsNullOrWhiteSpace(clientTenant) ? clientTenant.Trim() : "default";
return new IdentityContext
{
@@ -192,9 +209,37 @@ public sealed class IdentityHeaderPolicyMiddleware
}
}
// Expand coarse OIDC scopes to fine-grained service scopes.
// This bridges the gap between Authority-registered scopes (e.g. "scheduler:read")
// and the fine-grained scopes that downstream services expect (e.g. "scheduler.runs.read").
ExpandCoarseScopes(scopes);
return scopes;
}
/// <summary>
/// Expands coarse OIDC scopes into fine-grained service scopes.
/// Pattern: "{service}:{action}" expands to "{service}.{resource}.{action}" for known resources.
/// </summary>
private static void ExpandCoarseScopes(HashSet<string> scopes)
{
// scheduler:read -> scheduler.schedules.read, scheduler.runs.read
// scheduler:operate -> scheduler.schedules.write, scheduler.runs.write, scheduler.runs.preview, scheduler.runs.manage
if (scopes.Contains("scheduler:read"))
{
scopes.Add("scheduler.schedules.read");
scopes.Add("scheduler.runs.read");
}
if (scopes.Contains("scheduler:operate"))
{
scopes.Add("scheduler.schedules.write");
scopes.Add("scheduler.runs.write");
scopes.Add("scheduler.runs.preview");
scopes.Add("scheduler.runs.manage");
}
}
private void StoreIdentityContext(HttpContext context, IdentityContext identity)
{
context.Items[GatewayContextKeys.IsAnonymous] = identity.IsAnonymous;
@@ -248,6 +293,7 @@ public sealed class IdentityHeaderPolicyMiddleware
if (!string.IsNullOrEmpty(identity.Tenant))
{
headers["X-StellaOps-Tenant"] = identity.Tenant;
headers["X-Tenant-Id"] = identity.Tenant;
if (_options.EnableLegacyHeaders)
{
headers["X-Stella-Tenant"] = identity.Tenant;
@@ -270,6 +316,7 @@ public sealed class IdentityHeaderPolicyMiddleware
var sortedScopes = identity.Scopes.OrderBy(s => s, StringComparer.Ordinal);
var scopesValue = string.Join(" ", sortedScopes);
headers["X-StellaOps-Scopes"] = scopesValue;
headers["X-Scopes"] = scopesValue;
if (_options.EnableLegacyHeaders)
{
headers["X-Stella-Scopes"] = scopesValue;
@@ -279,6 +326,7 @@ public sealed class IdentityHeaderPolicyMiddleware
{
// Explicit empty scopes for anonymous to prevent ambiguity
headers["X-StellaOps-Scopes"] = string.Empty;
headers["X-Scopes"] = string.Empty;
if (_options.EnableLegacyHeaders)
{
headers["X-Stella-Scopes"] = string.Empty;
@@ -347,4 +395,13 @@ public sealed class IdentityHeaderPolicyOptions
/// Default: false (forbidden for security).
/// </summary>
public bool AllowScopeHeaderOverride { get; set; } = false;
/// <summary>
/// Route prefixes where Authorization and DPoP headers should be preserved
/// (passed through to the upstream service) instead of stripped.
/// Use this for upstream services that require JWT validation themselves
/// (e.g., Authority admin API at /console).
/// Default: empty (strip auth headers for all routes).
/// </summary>
public List<string> JwtPassthroughPrefixes { get; set; } = [];
}

View File

@@ -124,7 +124,11 @@ builder.Services.AddSingleton<IDpopProofValidator, DpopProofValidator>();
builder.Services.AddSingleton(new IdentityHeaderPolicyOptions
{
EnableLegacyHeaders = bootstrapOptions.Auth.EnableLegacyHeaders,
AllowScopeHeaderOverride = bootstrapOptions.Auth.AllowScopeHeader
AllowScopeHeaderOverride = bootstrapOptions.Auth.AllowScopeHeader,
JwtPassthroughPrefixes = bootstrapOptions.Routes
.Where(r => r.PreserveAuthHeaders)
.Select(r => r.Path)
.ToList()
});
// Route table: resolver + error routes + HTTP client for reverse proxy
@@ -222,6 +226,20 @@ static void ConfigureAuthentication(WebApplicationBuilder builder, GatewayOption
}
});
// Configure the OIDC metadata HTTP client to accept self-signed certificates
// (Authority uses a dev cert in Docker)
if (!authOptions.Authority.RequireHttpsMetadata)
{
builder.Services.ConfigureHttpClientDefaults(clientBuilder =>
{
clientBuilder.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
});
});
}
if (authOptions.Authority.RequiredScopes.Count > 0)
{
builder.Services.AddAuthorization(config =>

View File

@@ -43,9 +43,9 @@
"MtlsEnabled": false,
"AllowAnonymous": true,
"Authority": {
"Issuer": "",
"RequireHttpsMetadata": true,
"MetadataAddress": "",
"Issuer": "https://authority.stella-ops.local",
"RequireHttpsMetadata": false,
"MetadataAddress": "https://authority.stella-ops.local/.well-known/openid-configuration",
"Audiences": [],
"RequiredScopes": []
}
@@ -66,7 +66,7 @@
},
"Routes": [
{ "Type": "ReverseProxy", "Path": "/api/v1/release-orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local/api/v1/release-orchestrator" },
{ "Type": "ReverseProxy", "Path": "/api/v1/vex", "TranslatesTo": "http://vexhub.stella-ops.local/api/v1/vex" },
{ "Type": "ReverseProxy", "Path": "/api/v1/vex", "TranslatesTo": "https://vexhub.stella-ops.local/api/v1/vex" },
{ "Type": "ReverseProxy", "Path": "/api/v1/vexlens", "TranslatesTo": "http://vexlens.stella-ops.local/api/v1/vexlens" },
{ "Type": "ReverseProxy", "Path": "/api/v1/notify", "TranslatesTo": "http://notify.stella-ops.local/api/v1/notify" },
{ "Type": "ReverseProxy", "Path": "/api/v1/notifier", "TranslatesTo": "http://notifier.stella-ops.local/api/v1/notifier" },
@@ -78,7 +78,7 @@
{ "Type": "ReverseProxy", "Path": "/v1/audit-bundles", "TranslatesTo": "http://evidencelocker.stella-ops.local/v1/audit-bundles" },
{ "Type": "ReverseProxy", "Path": "/policy", "TranslatesTo": "http://policy-gateway.stella-ops.local" },
{ "Type": "ReverseProxy", "Path": "/api/policy", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/policy" },
{ "Type": "ReverseProxy", "Path": "/api/risk", "TranslatesTo": "http://policy-engine.stella-ops.local/api/risk" },
{ "Type": "ReverseProxy", "Path": "/api/risk", "TranslatesTo": "http://policy-engine.stella-ops.local/api/risk", "PreserveAuthHeaders": true },
{ "Type": "ReverseProxy", "Path": "/api/analytics", "TranslatesTo": "http://platform.stella-ops.local/api/analytics" },
{ "Type": "ReverseProxy", "Path": "/api/release-orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local/api/release-orchestrator" },
{ "Type": "ReverseProxy", "Path": "/api/releases", "TranslatesTo": "http://orchestrator.stella-ops.local/api/releases" },
@@ -87,15 +87,16 @@
{ "Type": "ReverseProxy", "Path": "/api/v1/scanner", "TranslatesTo": "http://scanner.stella-ops.local/api/v1/scanner" },
{ "Type": "ReverseProxy", "Path": "/api/v1/findings", "TranslatesTo": "http://findings.stella-ops.local/api/v1/findings" },
{ "Type": "ReverseProxy", "Path": "/api/v1/policy", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/v1/policy" },
{ "Type": "ReverseProxy", "Path": "/api/policy", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/policy" },
{ "Type": "ReverseProxy", "Path": "/api/v1/reachability", "TranslatesTo": "http://reachgraph.stella-ops.local/api/v1/reachability" },
{ "Type": "ReverseProxy", "Path": "/api/v1/attestor", "TranslatesTo": "http://attestor.stella-ops.local/api/v1/attestor" },
{ "Type": "ReverseProxy", "Path": "/api/v1/attestations", "TranslatesTo": "http://attestor.stella-ops.local/api/v1/attestations" },
{ "Type": "ReverseProxy", "Path": "/api/v1/sbom", "TranslatesTo": "http://sbomservice.stella-ops.local/api/v1/sbom" },
{ "Type": "ReverseProxy", "Path": "/api/v1/signals", "TranslatesTo": "http://signals.stella-ops.local/api/v1/signals" },
{ "Type": "ReverseProxy", "Path": "/api/v1/vex", "TranslatesTo": "http://vexhub.stella-ops.local/api/v1/vex" },
{ "Type": "ReverseProxy", "Path": "/api/v1/vex", "TranslatesTo": "https://vexhub.stella-ops.local/api/v1/vex" },
{ "Type": "ReverseProxy", "Path": "/api/v1/orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local/api/v1/orchestrator" },
{ "Type": "ReverseProxy", "Path": "/api/v1/authority", "TranslatesTo": "https://authority.stella-ops.local/api/v1/authority" },
{ "Type": "ReverseProxy", "Path": "/api/v1/trust", "TranslatesTo": "https://authority.stella-ops.local/api/v1/trust" },
{ "Type": "ReverseProxy", "Path": "/api/v1/authority", "TranslatesTo": "https://authority.stella-ops.local/api/v1/authority", "PreserveAuthHeaders": true },
{ "Type": "ReverseProxy", "Path": "/api/v1/trust", "TranslatesTo": "https://authority.stella-ops.local/api/v1/trust", "PreserveAuthHeaders": true },
{ "Type": "ReverseProxy", "Path": "/api/v1/evidence", "TranslatesTo": "http://evidencelocker.stella-ops.local/api/v1/evidence" },
{ "Type": "ReverseProxy", "Path": "/api/v1/proofs", "TranslatesTo": "http://evidencelocker.stella-ops.local/api/v1/proofs" },
{ "Type": "ReverseProxy", "Path": "/api/v1/timeline", "TranslatesTo": "http://timelineindexer.stella-ops.local/api/v1/timeline" },
@@ -127,15 +128,15 @@
{ "Type": "ReverseProxy", "Path": "/api/orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local/api/orchestrator" },
{ "Type": "ReverseProxy", "Path": "/api/sbomservice", "TranslatesTo": "http://sbomservice.stella-ops.local/api/sbomservice" },
{ "Type": "ReverseProxy", "Path": "/api/vuln-explorer", "TranslatesTo": "http://vulnexplorer.stella-ops.local/api/vuln-explorer" },
{ "Type": "ReverseProxy", "Path": "/api/vex", "TranslatesTo": "http://vexhub.stella-ops.local/api/vex" },
{ "Type": "ReverseProxy", "Path": "/api/vex", "TranslatesTo": "https://vexhub.stella-ops.local/api/vex" },
{ "Type": "ReverseProxy", "Path": "/api/admin", "TranslatesTo": "http://platform.stella-ops.local/api/admin" },
{ "Type": "ReverseProxy", "Path": "/api", "TranslatesTo": "http://platform.stella-ops.local/api" },
{ "Type": "ReverseProxy", "Path": "/platform", "TranslatesTo": "http://platform.stella-ops.local/platform" },
{ "Type": "ReverseProxy", "Path": "/connect", "TranslatesTo": "https://authority.stella-ops.local" },
{ "Type": "ReverseProxy", "Path": "/.well-known", "TranslatesTo": "https://authority.stella-ops.local/.well-known" },
{ "Type": "ReverseProxy", "Path": "/jwks", "TranslatesTo": "https://authority.stella-ops.local/jwks" },
{ "Type": "ReverseProxy", "Path": "/authority", "TranslatesTo": "https://authority.stella-ops.local/authority" },
{ "Type": "ReverseProxy", "Path": "/console", "TranslatesTo": "https://authority.stella-ops.local/console" },
{ "Type": "ReverseProxy", "Path": "/connect", "TranslatesTo": "https://authority.stella-ops.local", "PreserveAuthHeaders": true },
{ "Type": "ReverseProxy", "Path": "/.well-known", "TranslatesTo": "https://authority.stella-ops.local/.well-known", "PreserveAuthHeaders": true },
{ "Type": "ReverseProxy", "Path": "/jwks", "TranslatesTo": "https://authority.stella-ops.local/jwks", "PreserveAuthHeaders": true },
{ "Type": "ReverseProxy", "Path": "/authority", "TranslatesTo": "https://authority.stella-ops.local/authority", "PreserveAuthHeaders": true },
{ "Type": "ReverseProxy", "Path": "/console", "TranslatesTo": "https://authority.stella-ops.local/console", "PreserveAuthHeaders": true },
{ "Type": "ReverseProxy", "Path": "/gateway", "TranslatesTo": "http://gateway.stella-ops.local" },
{ "Type": "ReverseProxy", "Path": "/scanner", "TranslatesTo": "http://scanner.stella-ops.local" },
{ "Type": "ReverseProxy", "Path": "/policyGateway", "TranslatesTo": "http://policy-gateway.stella-ops.local" },
@@ -148,7 +149,7 @@
{ "Type": "ReverseProxy", "Path": "/signals", "TranslatesTo": "http://signals.stella-ops.local" },
{ "Type": "ReverseProxy", "Path": "/excititor", "TranslatesTo": "http://excititor.stella-ops.local" },
{ "Type": "ReverseProxy", "Path": "/findingsLedger", "TranslatesTo": "http://findings.stella-ops.local" },
{ "Type": "ReverseProxy", "Path": "/vexhub", "TranslatesTo": "http://vexhub.stella-ops.local" },
{ "Type": "ReverseProxy", "Path": "/vexhub", "TranslatesTo": "https://vexhub.stella-ops.local" },
{ "Type": "ReverseProxy", "Path": "/vexlens", "TranslatesTo": "http://vexlens.stella-ops.local" },
{ "Type": "ReverseProxy", "Path": "/orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local" },
{ "Type": "ReverseProxy", "Path": "/taskrunner", "TranslatesTo": "http://taskrunner.stella-ops.local" },
@@ -175,7 +176,6 @@
{ "Type": "ReverseProxy", "Path": "/airgapController", "TranslatesTo": "http://airgap-controller.stella-ops.local" },
{ "Type": "ReverseProxy", "Path": "/airgapTime", "TranslatesTo": "http://airgap-time.stella-ops.local" },
{ "Type": "ReverseProxy", "Path": "/smremote", "TranslatesTo": "http://smremote.stella-ops.local" },
{ "Type": "ReverseProxy", "Path": "/envsettings.json", "TranslatesTo": "http://platform.stella-ops.local/platform/envsettings.json" },
{ "Type": "StaticFiles", "Path": "/", "TranslatesTo": "/app/wwwroot", "Headers": { "x-spa-fallback": "true" } },
{ "Type": "NotFoundPage", "Path": "/_error/404", "TranslatesTo": "/app/wwwroot/index.html" },
{ "Type": "ServerErrorPage", "Path": "/_error/500", "TranslatesTo": "/app/wwwroot/index.html" }

View File

@@ -22,4 +22,11 @@ public sealed class StellaOpsRoute
public string? TranslatesTo { get; set; }
public Dictionary<string, string> Headers { get; set; } = new();
/// <summary>
/// When true, the gateway preserves Authorization and DPoP headers instead
/// of stripping them. Use for upstream services that perform their own JWT
/// validation (e.g., Authority admin API).
/// </summary>
public bool PreserveAuthHeaders { get; set; }
}