fix(router): ship audit bundle frontdoor cutover
This commit is contained in:
@@ -45,6 +45,8 @@ public static class GatewayOptionsValidator
|
||||
|
||||
private static void ValidateRoutes(List<StellaOpsRoute> routes)
|
||||
{
|
||||
var exactPathIndices = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
for (var i = 0; i < routes.Count; i++)
|
||||
{
|
||||
var route = routes[i];
|
||||
@@ -66,6 +68,17 @@ public static class GatewayOptionsValidator
|
||||
throw new InvalidOperationException($"{prefix}: Path is not a valid regex pattern: {ex.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var normalizedPath = NormalizePath(route.Path);
|
||||
if (exactPathIndices.TryGetValue(normalizedPath, out var existingIndex))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{prefix}: Duplicate route path '{normalizedPath}' already defined by Route[{existingIndex}].");
|
||||
}
|
||||
|
||||
exactPathIndices[normalizedPath] = i;
|
||||
}
|
||||
|
||||
switch (route.Type)
|
||||
{
|
||||
@@ -124,4 +137,16 @@ public static class GatewayOptionsValidator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizePath(string value)
|
||||
{
|
||||
var normalized = value.Trim();
|
||||
if (!normalized.StartsWith('/'))
|
||||
{
|
||||
normalized = "/" + normalized;
|
||||
}
|
||||
|
||||
normalized = normalized.TrimEnd('/');
|
||||
return string.IsNullOrEmpty(normalized) ? "/" : normalized;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0-preview AS base
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8443
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0-preview AS build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN dotnet publish src/Router/StellaOps.Gateway.WebService/StellaOps.Gateway.WebService.csproj -c Release -o /app/publish
|
||||
|
||||
@@ -24,7 +24,10 @@ using StellaOps.Router.Gateway.Middleware;
|
||||
using StellaOps.Router.Gateway.OpenApi;
|
||||
using StellaOps.Router.Gateway.RateLimit;
|
||||
using StellaOps.Router.Gateway.Routing;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.Loader;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -157,7 +160,14 @@ var routerEnabled = builder.Services.AddRouterMicroservice(
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("router");
|
||||
if (ShouldApplyStellaOpsLocalBinding())
|
||||
{
|
||||
builder.TryAddStellaOpsLocalBinding("router");
|
||||
}
|
||||
else
|
||||
{
|
||||
ConfigureContainerFrontdoorBindings(builder);
|
||||
}
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("router");
|
||||
|
||||
@@ -378,6 +388,116 @@ static void ConfigureGatewayOptionsMapping(WebApplicationBuilder builder, Gatewa
|
||||
|
||||
}
|
||||
|
||||
static bool ShouldApplyStellaOpsLocalBinding()
|
||||
{
|
||||
var runningInContainer = string.Equals(
|
||||
Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER"),
|
||||
"true",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!runningInContainer)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compose-published container runs already define the frontdoor port contract.
|
||||
// Respect explicit container port settings instead of replacing them with 80/443.
|
||||
var explicitUrls = Environment.GetEnvironmentVariable("ASPNETCORE_URLS");
|
||||
var explicitHttpPorts = Environment.GetEnvironmentVariable("ASPNETCORE_HTTP_PORTS");
|
||||
var explicitHttpsPorts = Environment.GetEnvironmentVariable("ASPNETCORE_HTTPS_PORTS");
|
||||
|
||||
return string.IsNullOrWhiteSpace(explicitUrls)
|
||||
&& string.IsNullOrWhiteSpace(explicitHttpPorts)
|
||||
&& string.IsNullOrWhiteSpace(explicitHttpsPorts);
|
||||
}
|
||||
|
||||
static void ConfigureContainerFrontdoorBindings(WebApplicationBuilder builder)
|
||||
{
|
||||
var currentUrls = builder.WebHost.GetSetting(WebHostDefaults.ServerUrlsKey) ?? string.Empty;
|
||||
|
||||
builder.WebHost.ConfigureKestrel((context, kestrel) =>
|
||||
{
|
||||
var defaultCert = LoadDefaultCertificate(context.Configuration);
|
||||
|
||||
foreach (var rawUrl in currentUrls.Split(';', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (!Uri.TryCreate(rawUrl.Trim(), UriKind.Absolute, out var uri))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var address = ResolveListenAddress(uri.Host);
|
||||
if (string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
kestrel.Listen(address, uri.Port, listenOptions =>
|
||||
{
|
||||
if (defaultCert is not null)
|
||||
{
|
||||
listenOptions.UseHttps(defaultCert);
|
||||
}
|
||||
else
|
||||
{
|
||||
listenOptions.UseHttps();
|
||||
}
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
kestrel.Listen(address, uri.Port);
|
||||
}
|
||||
|
||||
if (defaultCert is not null && IsPortAvailable(443, IPAddress.Any))
|
||||
{
|
||||
kestrel.ListenAnyIP(443, listenOptions => listenOptions.UseHttps(defaultCert));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static X509Certificate2? LoadDefaultCertificate(IConfiguration configuration)
|
||||
{
|
||||
var certPath = configuration["Kestrel:Certificates:Default:Path"];
|
||||
var certPass = configuration["Kestrel:Certificates:Default:Password"];
|
||||
if (string.IsNullOrWhiteSpace(certPath) || !File.Exists(certPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return X509CertificateLoader.LoadPkcs12FromFile(certPath, certPass);
|
||||
}
|
||||
|
||||
static IPAddress ResolveListenAddress(string host)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host) ||
|
||||
string.Equals(host, "*", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(host, "+", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(host, "0.0.0.0", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return IPAddress.Any;
|
||||
}
|
||||
|
||||
if (string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return IPAddress.Loopback;
|
||||
}
|
||||
|
||||
return IPAddress.Parse(host);
|
||||
}
|
||||
|
||||
static bool IsPortAvailable(int port, IPAddress address)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var listener = new TcpListener(address, port);
|
||||
listener.Start();
|
||||
listener.Stop();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static string? ResolveAuthorityClaimsUrl(GatewayAuthorityOptions authorityOptions)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(authorityOptions.ClaimsOverridesUrl))
|
||||
@@ -418,5 +538,3 @@ static string? ResolveAuthorityClaimsUrl(GatewayAuthorityOptions authorityOption
|
||||
return builder.Uri.GetLeftPart(UriPartial.Authority).TrimEnd('/');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -72,10 +72,9 @@
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/notifier", "TranslatesTo": "http://notifier.stella-ops.local/api/v1/notifier" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/concelier", "TranslatesTo": "http://concelier.stella-ops.local/api/v1/concelier" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/cvss", "TranslatesTo": "http://policy-gateway.stella-ops.local/api/cvss", "PreserveAuthHeaders": true },
|
||||
{ "Type": "ReverseProxy", "Path": "/v1/evidence-packs", "TranslatesTo": "http://evidencelocker.stella-ops.local/v1/evidence-packs" },
|
||||
{ "Type": "ReverseProxy", "Path": "/v1/evidence-packs", "TranslatesTo": "http://advisoryai.stella-ops.local/v1/evidence-packs" },
|
||||
{ "Type": "ReverseProxy", "Path": "/v1/runs", "TranslatesTo": "http://orchestrator.stella-ops.local/v1/runs" },
|
||||
{ "Type": "ReverseProxy", "Path": "/v1/advisory-ai", "TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai" },
|
||||
{ "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", "PreserveAuthHeaders": true },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/risk", "TranslatesTo": "http://policy-engine.stella-ops.local/api/risk", "PreserveAuthHeaders": true },
|
||||
@@ -88,13 +87,11 @@
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/findings", "TranslatesTo": "http://findings.stella-ops.local/api/v1/findings", "PreserveAuthHeaders": true },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/integrations", "TranslatesTo": "http://integrations.stella-ops.local/api/v1/integrations", "PreserveAuthHeaders": true },
|
||||
{ "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", "PreserveAuthHeaders": true },
|
||||
{ "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/orchestrator", "TranslatesTo": "http://orchestrator.stella-ops.local/api/v1/orchestrator" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/authority/quotas", "TranslatesTo": "http://platform.stella-ops.local/api/v1/authority/quotas", "PreserveAuthHeaders": true },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/authority", "TranslatesTo": "http://authority.stella-ops.local/api/v1/authority", "PreserveAuthHeaders": true },
|
||||
@@ -105,7 +102,6 @@
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/search", "TranslatesTo": "http://advisoryai.stella-ops.local/v1/search" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/advisory-ai", "TranslatesTo": "http://advisoryai.stella-ops.local/v1/advisory-ai" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/advisory", "TranslatesTo": "http://advisoryai.stella-ops.local/api/v1/advisory" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/concelier", "TranslatesTo": "http://concelier.stella-ops.local/api/v1/concelier" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/vulnerabilities", "TranslatesTo": "http://scanner.stella-ops.local/api/v1/vulnerabilities" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/watchlist", "TranslatesTo": "http://scanner.stella-ops.local/api/v1/watchlist" },
|
||||
{ "Type": "ReverseProxy", "Path": "/api/v1/resolve", "TranslatesTo": "http://binaryindex.stella-ops.local/api/v1/resolve" },
|
||||
|
||||
Reference in New Issue
Block a user