mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-21 14:31:13 +00:00
314d1dec39
All 5 phases refreshed against cycle-3 delta:
Phase 1 (Dependency Scan):
- D1 RESOLVED (AZ-496): Microsoft.AspNetCore.OpenApi 8.0.21 → 8.0.25
- D3 RESOLVED (AZ-496): JwtBearer 8.0.21 → 8.0.25
- D4 NEW (Low, test-only): System.IdentityModel.Tokens.Jwt 7.0.3 +
Microsoft.IdentityModel.Tokens 7.0.3 pinned in TestSupport carry
CVE-2024-21319 (JWE DoS). Bump to ≥ 7.1.2 tracked as future PBI.
Phase 2 (Static Analysis):
- F-AUTH-3 (Info): test runner Program.cs logs iss/aud at startup;
production API does NOT (verified by grep).
- F-AUTH-4 (Info): DEV-ONLY iss/aud placeholders in
appsettings.Development.json + .env.example — by design per
Option B for AZ-494.
- F-DBR-1: TRUNCATE string interpolation in
IntegrationTestDatabaseReset.cs — false positive (hard-coded
table list).
- F-DBR-2 (Low): TRUNCATE guard is operator-bypassable. Two-guard
model is conservative-by-default and unit-tested.
- F-PERF-1 (Low): perf-bootstrap --mint-only writes a 4-hour
GPS-permission token to stdout. Operator-trusted machine assumed.
Phase 3 (OWASP Top 10):
- A03 carries D1/D3 RESOLVED + D4 NEW.
- A07 flips F-AUTH-2 to RESOLVED (AZ-494); residual revocation-list
Low recorded.
- A05 status unchanged (F-DBR-1 false positive).
- A08 picks up F-DBR-2.
Phase 4 (Infrastructure):
- JWT_ISSUER / JWT_AUDIENCE flow .env → compose → Kestrel config,
same pattern as JWT_SECRET.
- INTEGRATION_TEST_DB_RESET + ASPNETCORE_ENVIRONMENT=Testing wired
for AZ-493 reset gate.
- SatelliteProvider.TestSupport is IsPackable=false — never ships
in a production container image.
- New operational gate added to deploy runbook: grep for DEV-ONLY-
in the rendered deploy environment must return zero hits.
Phase 5 (Security Report):
- Verdict: PASS_WITH_WARNINGS (cycle 3 does not escalate).
- 0 Critical, 0 High, 0 new Medium.
- Cycle-2 F-AUTH-2 (Medium) RESOLVED; cycle-1 D1 + cycle-2 D3
RESOLVED.
Autodev state advanced to Step 14 completed. Next: Step 15
(Performance Test, optional gate).
Co-authored-by: Cursor <cursoragent@cursor.com>
14 KiB
14 KiB
Phase 4 — Configuration & Infrastructure Review
Date: 2026-05-11
Scope: Dockerfile (API + integration tests), docker-compose.yml, docker-compose.tests.yml, .dockerignore, .woodpecker/01-test.yml, .woodpecker/02-build-push.yml, appsettings*.json, .env handling.
Findings
I1 — Dockerfile runs as root (Low)
- Location:
SatelliteProvider.Api/Dockerfile(noUSERdirective) - Description: The final stage of the API image inherits root from
mcr.microsoft.com/dotnet/aspnet:8.0(current Microsoft images default to root unless overridden). Any process compromise — even a low-impact one — has uid-0 inside the container. - Impact: Container escape primitives (e.g., kernel CVE, sloppy bind-mount of
/var/run/docker.sock) become host-root rather than host-uid-1000. The02-build-push.ymlstep itself bind-mounts/var/run/docker.sockinto the build container — that's a separate concern (build host, not runtime), but it underscores why "least privilege at runtime" matters even on a single-tenant box. - Remediation: Add to the
finalstage:Also verifyRUN adduser --disabled-password --gecos "" --uid 10001 satellite && \ chown -R satellite:satellite /app USER satellite./tiles,./ready,./logshost volumes are writable by uid 10001 in deployment manifests.
I2 — No security headers middleware (Low)
- Location:
SatelliteProvider.Api/Program.cs(noapp.UseSecurityHeaders()/app.Use(headers …)block) - Description: API responses do not set
X-Content-Type-Options: nosniff,Referrer-Policy: no-referrer,X-Frame-Options: DENY, or HSTS (Strict-Transport-Security) — onlyapp.UseHttpsRedirection()is wired. For a JSON-only API this is low impact (no browser is the primary client), but the missingCache-Controldefaults can let proxies cache 5xx responses. - Impact: Limited — JSON-only responses, no cookies, no browser session. The Swagger UI (Development-only) does render HTML; a permissive default there is more of a hygiene issue than a real risk.
- Remediation: Add a tiny middleware to set the standard hardening headers, OR install
NWebsec.AspNetCore.Middlewareand wireapp.UseHsts()+ thenosniff/frame-optionsdefaults. Cheap, no behavioural change.
I3 — No rate limiting on any HTTP endpoint (Medium)
- Location:
SatelliteProvider.Api/Program.cs(noapp.UseRateLimiter(), noAddRateLimiter()) - Description: There is internal concurrency control on outbound Google Maps calls (
SemaphoreSlim,MaxConcurrentDownloads), but no inbound rate limiting. An attacker can:- Submit
NPOST /api/satellite/requestcalls in a tight loop, filling the boundedIRegionRequestQueue(capacity 1000) and DoS-ing the background processor. - Submit
NGET /api/satellite/tiles/latloncalls with novel lat/lon pairs, forcing the upstream Google Maps quota to drain.
- Submit
- Impact: Service-degradation DoS. Combined with finding A01-caveat (no auth), the only protection is the network boundary.
- Remediation: Wire
Microsoft.AspNetCore.RateLimiting(built into .NET 8 — no new package). Conservative starting point:Tune per-endpoint after observing baseline production load.builder.Services.AddRateLimiter(options => { options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx => RateLimitPartition.GetFixedWindowLimiter( partitionKey: ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown", factory: _ => new FixedWindowRateLimiterOptions { PermitLimit = 60, Window = TimeSpan.FromMinutes(1) })); }); app.UseRateLimiter();
I4 — No security-event logs / alerting (Low)
- Location: Logging strategy across
Program.cs,GlobalExceptionHandler,CorsConfigurationValidator - Description: Operational logs are well-structured (Serilog → file rotation; correlationId propagation), but there are no log entries for what would be security-relevant events: validation failures (BadRequest stream), repeated 4xx from a single IP, malformed input bursts, or migration failures. The migration failure path does throw and crash startup (good signal), but this leaves no trail in the file logs.
- Impact: No way to detect abuse of the unauthenticated endpoints from logs alone. For an internal-only deployment this is acceptable; if the API ever moves toward a less-trusted network, post-deploy log-mining will not be able to reconstruct attack patterns.
- Remediation: Defer until/unless the trust boundary changes. When required: add a structured log line for each 400/404 (with
Method,Path,RemoteIp,correlationId) and a counter for "validation failures per minute per IP".
I5 — .env is NOT in .dockerignore (Medium)
- Location:
.dockerignore(line by line review — no.enventry);SatelliteProvider.Api/Dockerfile:15(COPY . .) - Description: The Dockerfile's
COPY . .step copies the entire build context into/src. The build context starts at the repo root, where.envlives..envIS in.gitignoreso the dev-only Google Maps key never reaches the git repo, but it WILL be baked into the build-stage image layer (and into the final image, sincefinaldoesCOPY --from=publish /app/publish .— only/app/publishsurvives, but thebuildstage retains.envand is reachable if anyone introspects an intermediate layer). - Impact:
- Anyone with read access to the registry can
docker pull <build-stage-tag>(if exported) and recover the API key from the layer. - Even just the
finalimage: BuildKit cache mounts and any future Dockerfile change that doesCOPY . /appinstead ofCOPY --from=publishwould silently include the file.
- Anyone with read access to the registry can
- Remediation: Add
.envto.dockerignore:This is a one-line fix and complements finding S4..env .env.* !.env.example
I6 — docker-compose.yml exposes Postgres on 0.0.0.0:5432 (Medium — duplicate of S2; restated here for infra-domain completeness)
- Location:
docker-compose.yml:9-10 - Description / Remediation: See S2.
Items checked clean
- Secrets management in CI:
.woodpecker/02-build-push.ymlusesfrom_secret: registry_host / registry_user / registry_token— no plaintext credentials. Thedocker loginstep pipes the token via--password-stdin, which avoids leaking the token via process list. ✓ - Image attribution: build step labels images with
org.opencontainers.image.revision,…created,…source— good provenance hygiene. ✓ - Healthcheck on Postgres:
pg_isready -U postgresconfigured. (Note: relies on the weak default user from S2.) - Log volume layout:
./logs:/app/logsmounted; not exposed via the API. ✓ - Test runner isolation:
docker-compose.tests.ymlextends the API service (good — same image) but usesrestart: "no"so a flapping integration test doesn't loop and amplify load.
Self-verification
- All Dockerfiles reviewed (Api + IntegrationTests)
- All CI/CD configs reviewed (
.woodpecker/01-test.yml,02-build-push.yml) - All env / config files reviewed (
appsettings*.json,.env,docker-compose*.yml)
Cycle 2 Delta (AZ-487 + AZ-488)
Infra changes this cycle
docker-compose.yml:32addsJWT_SECRET=${JWT_SECRET}to theapiserviceenvironmentblock (AZ-487). Sourced from the host env (or.env).docker-compose.tests.yml:21adds the sameJWT_SECRET=${JWT_SECRET}to the integration-test runner so tests can mint matching tokens..env.example:18adds an emptyJWT_SECRET=line as a deploy-time reminder..env(workspace root, not tracked) ships aDEV-ONLY-CHANGE-ME-…placeholder ≥ 32 bytes for local docker-compose use.- No new infrastructure for AZ-488 — the upload endpoint reuses the existing
./tiles/volume; UAV files land under./tiles/uav/{z}/{x}/{y}.jpginside the same mount.
Cycle-2 verdict — clean
- Secret distribution:
JWT_SECRETflows env-var → docker-composeenvironment→ KestrelIConfiguration→AddSatelliteJwt. No checked-in production secret. The DEV-ONLY value in.envis also explicitly labelled and is bound to the cycle-1 S4 follow-up (rotate-and-document workflow). ✓ .envin.dockerignore(cycle-1 I5): still tracked under that remediation; cycle 2 does not add a new.envexposure path. TheJWT_SECRETmirroring lives in compose, not the Dockerfile, so it doesn't bake into image layers. ✓- No new exposed ports: cycle 2 changes are HTTP-layer only — endpoint registration, middleware, and a multipart handler. No new listener.
- No new external services: ImageSharp decode is in-process; no new outbound network call introduced by AZ-487 / AZ-488.
- No CI workflow changes: existing
.woodpecker/01-test.ymlcontinues to run unit + integration tests; new cycle-2 unit + integration tests run inside the existing workflow.
Cycle-2 operational follow-ups (NOT findings — pre-deploy verification)
- The deploy pipeline must verify
JWT_SECRETis set to a ≥ 32-byte value distinct from the DEV-ONLY placeholder before promotingapi. The application throws at startup if the value is missing or short, so a misconfigured deploy fails fast — but a deploy that promotes the dev placeholder verbatim would still pass the 32-byte gate. Tracked insecurity_report.mdcycle-2 recommendations. - Coordinate with admin team on
iss/audvalues (F-AUTH-2). When values are defined, both theAddSatelliteJwtcall site and.env/compose docs must be updated together. RESOLVED in cycle 3 (AZ-494) —AddSatelliteJwtnow validates both; values flowJWT_ISSUER/JWT_AUDIENCEenv → composeenvironment→ Kestrel config. See cycle-3 delta below.
Cycle 3 Delta (AZ-491 / AZ-492 / AZ-493 / AZ-494 / AZ-495 / AZ-496)
Infra changes this cycle
docker-compose.yml:33-34addsJWT_ISSUER=${JWT_ISSUER}andJWT_AUDIENCE=${JWT_AUDIENCE}to theapiserviceenvironmentblock (AZ-494).docker-compose.tests.yml:24-25mirrors them onintegration-testsso the runner mints tokens against the same iss/aud the API validates..env.exampledocuments both new variables with the fail-fast contract and ships explicitDEV-ONLY-placeholder values (AZ-494). Replaces an earlier draft that left them blank with a stale "leave blank to fall back" comment — corrected during Step 16 setup..env(gitignored, developer-only) now also carriesJWT_ISSUER=DEV-ONLY-iss-admin-azaion-local+JWT_AUDIENCE=DEV-ONLY-aud-satellite-providerfor the local docker-compose flow.docker-compose.tests.yml:21setsASPNETCORE_ENVIRONMENT=Testingand:22setsDB_CONNECTION_STRING=Host=postgres;.... Both are consumed by AZ-493'sIntegrationTestResetGuard.EnsureGuardPassesOrThrow(env, host)— env must beTestingAND host must be in the allowlist (postgres/localhost/127.0.0.1), otherwise the reset throws.docker-compose.tests.yml:20addsINTEGRATION_KEEP_STATE=${INTEGRATION_KEEP_STATE:-}so the--keep-stateflag onscripts/run-tests.shshort-circuits the reset (debugging convenience).scripts/run-tests.sh+scripts/run-performance-tests.shboth load + export the new env vars and fail-fast if missing (mirrors the AZ-487JWT_SECRETpattern).- No new exposed ports. No new external services. No CI workflow changes (
.woodpecker/01-test.ymland02-build-push.ymlunchanged). - New
SatelliteProvider.TestSupportproject —IsPackable=false, never builds a container, never ships outside test images. Confirmed viaDockerfilereview: onlySatelliteProvider.Api/DockerfileandSatelliteProvider.IntegrationTests/Dockerfileexist; neither produces a published artifact from TestSupport.
Cycle-3 verdict — clean
- Secret distribution:
JWT_ISSUER/JWT_AUDIENCEflow.env→ docker-composeenvironment→ KestrelIConfiguration→AddSatelliteJwt. Same path asJWT_SECRET, no new mechanism introduced. Production values are NOT in the repo (the .env in git tracks only the example); production deploy supplies them via the deploy pipeline (operator-confirmed admin-team values). - Production safety contract: empty
Jwt.Issuer/Jwt.Audienceinappsettings.json+ the fail-fastResolveRequiredOrThrowextension guarantees that a production deploy without explicit env-var values cannot start. This is the user-selected Option B forcing function. - Test-side DB reset: AZ-493's two-guard logic (
ASPNETCORE_ENVIRONMENT == "Testing"+ Host allowlist) is the test-runner's equivalent of a fail-fast contract. Documented as F-DBR-2 (Low) in static_analysis.md. - Perf harness:
scripts/run-performance-tests.shusesmktemp -dfor fixture + response paths; cleaned up ontrap EXIT. Token minted viadotnet <integration-tests.dll> --mint-only— never written to disk by the harness (only to a shell variable). Documented as F-PERF-1 (Low) in static_analysis.md.
Cycle-3 operational follow-ups (NOT findings — pre-deploy verification)
- The deploy pipeline must supply real
JWT_ISSUER+JWT_AUDIENCEvalues (admin-team-confirmed) when promotingapi. The application throws at startup if either is empty or whitespace — a misconfigured deploy fails fast. - The DEV-ONLY iss/aud placeholders in
appsettings.Development.json+.env.exampleare deliberately committed (Option B). A grep forDEV-ONLY-surfaces every site that must NOT ship to production. Make this grep part of the deploy gate runbook. - The cross-repo
suite/_docs/10_auth.mdwrite (AC-7) is deferred — outside this workspace's boundary. The suite repo's owner must take this on as a follow-up.