From 509b97a1a74487b361c1c03ce22af8657c29e925 Mon Sep 17 00:00:00 2001 From: master <> Date: Thu, 12 Mar 2026 13:12:32 +0200 Subject: [PATCH] Harden scratch setup bootstrap and authority admin scopes --- devops/compose/docker-compose.stella-ops.yml | 4 +- devops/compose/envsettings-override.json | 2 +- .../postgres-init/04-authority-schema.sql | 8 +- docs/INSTALL_GUIDE.md | 2 +- docs/dev/DEV_ENVIRONMENT_SETUP.md | 2 +- scripts/setup.ps1 | 74 ++++++++++++++++++- scripts/setup.sh | 64 +++++++++++++++- 7 files changed, 144 insertions(+), 12 deletions(-) diff --git a/devops/compose/docker-compose.stella-ops.yml b/devops/compose/docker-compose.stella-ops.yml index 92af040d7..c84a24c6f 100644 --- a/devops/compose/docker-compose.stella-ops.yml +++ b/devops/compose/docker-compose.stella-ops.yml @@ -406,7 +406,7 @@ services: Platform__EnvironmentSettings__TokenEndpoint: "https://stella-ops.local/connect/token" Platform__EnvironmentSettings__RedirectUri: "https://stella-ops.local/auth/callback" Platform__EnvironmentSettings__PostLogoutRedirectUri: "https://stella-ops.local/" - Platform__EnvironmentSettings__Scope: "openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:users.read authority:roles.read authority:clients.read authority:tokens.read authority:branding.read authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read orch:quota analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read release:write release:publish scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate packs.read packs.write packs.run packs.approve registry.admin timeline:read timeline:write trust:read trust:write trust:admin signer:read signer:sign signer:rotate signer:admin" + Platform__EnvironmentSettings__Scope: "openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:tenants.write authority:users.read authority:users.write authority:roles.read authority:roles.write authority:clients.read authority:clients.write authority:tokens.read authority:tokens.revoke authority:branding.read authority:branding.write authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read orch:quota analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read release:write release:publish scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate packs.read packs.write packs.run packs.approve registry.admin timeline:read timeline:write trust:read trust:write trust:admin signer:read signer:sign signer:rotate signer:admin" STELLAOPS_ROUTER_URL: "http://router.stella-ops.local" STELLAOPS_PLATFORM_URL: "http://platform.stella-ops.local" STELLAOPS_AUTHORITY_URL: "http://authority.stella-ops.local" @@ -509,7 +509,7 @@ services: STELLAOPS_AUTHORITY_AUTHORITY__PLUGINS__DESCRIPTORS__standard__BootstrapClients__0__ClientId: "stella-ops-ui" STELLAOPS_AUTHORITY_AUTHORITY__PLUGINS__DESCRIPTORS__standard__BootstrapClients__0__DisplayName: "Stella Ops Console" STELLAOPS_AUTHORITY_AUTHORITY__PLUGINS__DESCRIPTORS__standard__BootstrapClients__0__AllowedGrantTypes: "authorization_code refresh_token" - STELLAOPS_AUTHORITY_AUTHORITY__PLUGINS__DESCRIPTORS__standard__BootstrapClients__0__AllowedScopes: "openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:users.read authority:roles.read authority:clients.read authority:tokens.read authority:branding.read authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read orch:quota analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read release:write release:publish scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate packs.read packs.write packs.run packs.approve registry.admin timeline:read timeline:write trust:read trust:write trust:admin signer:read signer:sign signer:rotate signer:admin" + STELLAOPS_AUTHORITY_AUTHORITY__PLUGINS__DESCRIPTORS__standard__BootstrapClients__0__AllowedScopes: "openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:tenants.write authority:users.read authority:users.write authority:roles.read authority:roles.write authority:clients.read authority:clients.write authority:tokens.read authority:tokens.revoke authority:branding.read authority:branding.write authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read orch:quota analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read release:write release:publish scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate packs.read packs.write packs.run packs.approve registry.admin timeline:read timeline:write trust:read trust:write trust:admin signer:read signer:sign signer:rotate signer:admin" STELLAOPS_AUTHORITY_AUTHORITY__PLUGINS__DESCRIPTORS__standard__BootstrapClients__0__RedirectUris: "https://stella-ops.local/auth/callback https://stella-ops.local/auth/silent-refresh https://127.1.0.1/auth/callback https://127.1.0.1/auth/silent-refresh" STELLAOPS_AUTHORITY_AUTHORITY__PLUGINS__DESCRIPTORS__standard__BootstrapClients__0__PostLogoutRedirectUris: "https://stella-ops.local/ https://127.1.0.1/" STELLAOPS_AUTHORITY_AUTHORITY__PLUGINS__DESCRIPTORS__standard__BootstrapClients__0__RequirePkce: "true" diff --git a/devops/compose/envsettings-override.json b/devops/compose/envsettings-override.json index bda71bc6b..c10605a1c 100644 --- a/devops/compose/envsettings-override.json +++ b/devops/compose/envsettings-override.json @@ -6,7 +6,7 @@ "tokenEndpoint": "https://stella-ops.local/connect/token", "redirectUri": "https://stella-ops.local/auth/callback", "postLogoutRedirectUri": "https://stella-ops.local/", - "scope": "openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:users.read authority:roles.read authority:clients.read authority:tokens.read authority:branding.read authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read orch:quota analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read release:write release:publish scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate packs.read packs.write packs.run packs.approve registry.admin timeline:read timeline:write", + "scope": "openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:tenants.write authority:users.read authority:users.write authority:roles.read authority:roles.write authority:clients.read authority:clients.write authority:tokens.read authority:tokens.revoke authority:branding.read authority:branding.write authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read orch:quota analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read release:write release:publish scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate packs.read packs.write packs.run packs.approve registry.admin timeline:read timeline:write", "audience": "stella-ops-api", "dpopAlgorithms": [ "ES256" diff --git a/devops/compose/postgres-init/04-authority-schema.sql b/devops/compose/postgres-init/04-authority-schema.sql index 41a3aad3b..5ec0447d5 100644 --- a/devops/compose/postgres-init/04-authority-schema.sql +++ b/devops/compose/postgres-init/04-authority-schema.sql @@ -637,8 +637,12 @@ VALUES ARRAY['https://stella-ops.local/', 'https://127.1.0.1/'], ARRAY['openid', 'profile', 'email', 'offline_access', 'ui.read', 'ui.admin', 'ui.preferences.read', 'ui.preferences.write', - 'authority:tenants.read', 'authority:users.read', 'authority:roles.read', - 'authority:clients.read', 'authority:tokens.read', 'authority:branding.read', + 'authority:tenants.read', 'authority:tenants.write', + 'authority:users.read', 'authority:users.write', + 'authority:roles.read', 'authority:roles.write', + 'authority:clients.read', 'authority:clients.write', + 'authority:tokens.read', 'authority:tokens.revoke', + 'authority:branding.read', 'authority:branding.write', 'authority.audit.read', 'graph:read', 'sbom:read', 'scanner:read', 'policy:read', 'policy:simulate', 'policy:author', 'policy:review', 'policy:approve', diff --git a/docs/INSTALL_GUIDE.md b/docs/INSTALL_GUIDE.md index 5b4065f24..7904e4095 100755 --- a/docs/INSTALL_GUIDE.md +++ b/docs/INSTALL_GUIDE.md @@ -56,7 +56,7 @@ The scripts will: 4. Start infrastructure and wait for healthy containers 5. Create or reuse the external frontdoor Docker network from `.env` (`FRONTDOOR_NETWORK`, default `stellaops_frontdoor`) 6. Stop repo-local host-run Stella services that would lock build outputs, then build repo-owned .NET solutions and publish backend services locally into small Docker contexts before building hardened runtime images (vendored dependency trees such as `node_modules` are excluded) -7. Launch the full platform with health checks +7. Launch the full platform with health checks and wait for the first-user frontdoor bootstrap path (`/welcome`, `/envsettings.json`, OIDC discovery, `/connect/authorize`) before reporting success Open **https://stella-ops.local** when setup completes. diff --git a/docs/dev/DEV_ENVIRONMENT_SETUP.md b/docs/dev/DEV_ENVIRONMENT_SETUP.md index c943b3cb0..2a68fbea4 100644 --- a/docs/dev/DEV_ENVIRONMENT_SETUP.md +++ b/docs/dev/DEV_ENVIRONMENT_SETUP.md @@ -29,7 +29,7 @@ Setup scripts validate prerequisites, build solutions and Docker images, and lau ./scripts/setup.sh --images-only # only build Docker images ``` -The scripts will check for required tools (dotnet 10.x, node 20+, npm 10+, docker, git), warn about missing hosts file entries, copy `.env` from the example if needed, and stop repo-local host-run Stella services before the solution build so scratch bootstraps do not fail on locked `bin/Debug` outputs. See the manual steps below for details on each stage. +The scripts will check for required tools (dotnet 10.x, node 20+, npm 10+, docker, git), warn about missing hosts file entries, copy `.env` from the example if needed, and stop repo-local host-run Stella services before the solution build so scratch bootstraps do not fail on locked `bin/Debug` outputs. A full setup now waits for the first-user frontdoor bootstrap path as well: `/welcome`, `/envsettings.json`, OIDC discovery, and a PKCE-style `/connect/authorize` request must all be live before the script prints success. See the manual steps below for details on each stage. On Windows and Linux, the backend image builder now publishes each selected .NET service locally and builds the hardened runtime image from a small temporary context. That avoids repeatedly streaming the whole monorepo into Docker during scratch setup. diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 index 323c48317..afdf6df46 100644 --- a/scripts/setup.ps1 +++ b/scripts/setup.ps1 @@ -470,11 +470,21 @@ function Start-Platform { function Test-ExpectedHttpStatus([string]$url, [int[]]$allowedStatusCodes, [int]$timeoutSeconds = 5, [int]$attempts = 6, [int]$retryDelaySeconds = 2) { for ($attempt = 1; $attempt -le $attempts; $attempt++) { $statusCode = $null + $previousCertificateCallback = $null + $hasCertificateCallbackOverride = $false try { $request = [System.Net.WebRequest]::Create($url) $request.Method = 'GET' $request.Timeout = $timeoutSeconds * 1000 + if ($request -is [System.Net.HttpWebRequest]) { + $request.AllowAutoRedirect = $false + } + if ($url.StartsWith('https://', [System.StringComparison]::OrdinalIgnoreCase)) { + $previousCertificateCallback = [System.Net.ServicePointManager]::ServerCertificateValidationCallback + [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } + $hasCertificateCallbackOverride = $true + } $response = [System.Net.HttpWebResponse]$request.GetResponse() try { @@ -492,6 +502,10 @@ function Test-ExpectedHttpStatus([string]$url, [int[]]$allowedStatusCodes, [int] } } } catch { + } finally { + if ($hasCertificateCallbackOverride) { + [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $previousCertificateCallback + } } if ($null -ne $statusCode -and $allowedStatusCodes -contains $statusCode) { @@ -506,6 +520,52 @@ function Test-ExpectedHttpStatus([string]$url, [int[]]$allowedStatusCodes, [int] return $null } +function Test-FrontdoorBootstrap { + $baseUrl = 'https://stella-ops.local' + $probes = @( + @{ + Name = 'Frontdoor readiness' + Url = "$baseUrl/health/ready" + AllowedStatusCodes = @(200) + }, + @{ + Name = 'Frontdoor welcome page' + Url = "$baseUrl/welcome" + AllowedStatusCodes = @(200) + }, + @{ + Name = 'Frontdoor environment settings' + Url = "$baseUrl/envsettings.json" + AllowedStatusCodes = @(200) + }, + @{ + Name = 'Authority discovery' + Url = "$baseUrl/.well-known/openid-configuration" + AllowedStatusCodes = @(200) + }, + @{ + Name = 'Authority authorize bootstrap' + Url = "$baseUrl/connect/authorize?client_id=stella-ops-ui&redirect_uri=https%3A%2F%2Fstella-ops.local%2Fauth%2Fcallback&response_type=code&scope=openid%20profile%20email&state=setup-smoke&nonce=setup-smoke&code_challenge=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&code_challenge_method=S256" + AllowedStatusCodes = @(200, 302, 303) + } + ) + + Write-Step 'Waiting for frontdoor bootstrap readiness' + + foreach ($probe in $probes) { + $statusCode = Test-ExpectedHttpStatus $probe.Url $probe.AllowedStatusCodes -timeoutSeconds 5 -attempts 24 -retryDelaySeconds 5 + if ($null -ne $statusCode) { + Write-Ok "$($probe.Name) (HTTP $statusCode)" + continue + } + + Write-Fail "$($probe.Name) did not reach an expected status ($($probe.AllowedStatusCodes -join '/'))" + return $false + } + + return $true +} + # ─── 8. Smoke test ───────────────────────────────────────────────────────── function Test-Smoke { @@ -559,6 +619,14 @@ function Test-Smoke { $hasBlockingFailures = $true } + if (-not $InfraOnly) { + if (Test-FrontdoorBootstrap) { + Write-Ok 'Frontdoor bootstrap path is ready for first-user sign-in' + } else { + $hasBlockingFailures = $true + } + } + # Platform container health summary Write-Step 'Container health summary' Push-Location $ComposeDir @@ -679,7 +747,8 @@ if ($InfraOnly) { Start-Infrastructure $infraSmokeFailed = Test-Smoke if ($infraSmokeFailed) { - Write-Warn 'Infrastructure started with blocking smoke failures. Review output and docker compose logs.' + Write-Fail 'Infrastructure setup did not pass blocking smoke tests. Review output and docker compose logs.' + exit 1 } Write-Host "`nDone (infra only). Infrastructure is running." -ForegroundColor Green exit 0 @@ -696,7 +765,8 @@ if (-not $SkipImages) { Start-Platform $platformSmokeFailed = Test-Smoke if ($platformSmokeFailed) { - Write-Warn 'Setup completed with blocking smoke failures. Review output and docker compose logs.' + Write-Fail 'Setup did not pass blocking smoke tests. Review output and docker compose logs.' + exit 1 } Write-Host "`n=============================================" -ForegroundColor Green diff --git a/scripts/setup.sh b/scripts/setup.sh index 940679a6b..a6366a526 100644 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -339,7 +339,7 @@ http_status() { local status="" for (( attempt=1; attempt<=attempts; attempt++ )); do - status=$(curl -s -o /dev/null --connect-timeout 5 -w '%{http_code}' "$url" 2>/dev/null || true) + status=$(curl -sk -o /dev/null --connect-timeout 5 -w '%{http_code}' "$url" 2>/dev/null || true) if [[ -n "$status" && "$status" != "000" ]]; then printf '%s' "$status" return 0 @@ -353,16 +353,54 @@ http_status() { return 0 } +frontdoor_bootstrap_ready() { + step 'Waiting for frontdoor bootstrap readiness' + + local probes=( + "Frontdoor readiness|https://stella-ops.local/health/ready|200" + "Frontdoor welcome page|https://stella-ops.local/welcome|200" + "Frontdoor environment settings|https://stella-ops.local/envsettings.json|200" + "Authority discovery|https://stella-ops.local/.well-known/openid-configuration|200" + "Authority authorize bootstrap|https://stella-ops.local/connect/authorize?client_id=stella-ops-ui&redirect_uri=https%3A%2F%2Fstella-ops.local%2Fauth%2Fcallback&response_type=code&scope=openid%20profile%20email&state=setup-smoke&nonce=setup-smoke&code_challenge=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&code_challenge_method=S256|200,302,303" + ) + + local entry name url allowed status matched + for entry in "${probes[@]}"; do + IFS='|' read -r name url allowed <<<"$entry" + status="$(http_status "$url" 24 5)" + matched=false + IFS=',' read -ra allowed_codes <<<"$allowed" + for code in "${allowed_codes[@]}"; do + if [[ "$status" == "$code" ]]; then + matched=true + break + fi + done + + if [[ "$matched" == "true" ]]; then + ok "$name (HTTP $status)" + continue + fi + + fail "$name did not reach an expected status ($allowed)" + return 1 + done + + ok 'Frontdoor bootstrap path is ready for first-user sign-in' +} + # ─── 8. Smoke test ───────────────────────────────────────────────────────── smoke_test() { step 'Running smoke tests' + local has_blocking_failures=false # Infrastructure checks if docker exec stellaops-dev-postgres pg_isready -U stellaops &>/dev/null; then ok 'PostgreSQL' else warn 'PostgreSQL not responding' + has_blocking_failures=true fi local pong; pong=$(docker exec stellaops-dev-valkey valkey-cli ping 2>/dev/null || true) @@ -370,6 +408,7 @@ smoke_test() { ok 'Valkey' else warn 'Valkey not responding' + has_blocking_failures=true fi local rustfs_url rustfs_status @@ -379,6 +418,7 @@ smoke_test() { ok "RustFS S3 endpoint (HTTP $rustfs_status)" else warn 'RustFS S3 endpoint did not respond with an expected status (wanted 200/403)' + has_blocking_failures=true fi local registry_url registry_status @@ -388,6 +428,13 @@ smoke_test() { ok "Zot registry endpoint (HTTP $registry_status)" else warn 'Zot registry endpoint did not respond with an expected status (wanted 200/401)' + has_blocking_failures=true + fi + + if [[ "$INFRA_ONLY" != "true" ]]; then + if ! frontdoor_bootstrap_ready; then + has_blocking_failures=true + fi fi # Platform container health summary @@ -429,9 +476,14 @@ smoke_test() { ok 'Platform listening on https://stella-ops.local (TLS handshake pending)' else warn 'Platform not yet accessible at https://stella-ops.local (may still be starting)' + has_blocking_failures=true fi cd "$ROOT" + + if [[ "$has_blocking_failures" == "true" ]]; then + return 1 + fi } # ─── Main ─────────────────────────────────────────────────────────────────── @@ -454,7 +506,10 @@ ensure_env start_infra if [[ "$INFRA_ONLY" == "true" ]]; then - smoke_test + if ! smoke_test; then + fail 'Infrastructure setup did not pass blocking smoke tests. Review output and docker compose logs.' + exit 1 + fi echo '' echo 'Done (infra only). Infrastructure is running.' exit 0 @@ -473,7 +528,10 @@ if [[ "$SKIP_IMAGES" != "true" ]]; then fi start_platform -smoke_test +if ! smoke_test; then + fail 'Setup did not pass blocking smoke tests. Review output and docker compose logs.' + exit 1 +fi echo '' echo '============================================='