mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-21 21:51:15 +00:00
5214a4a647
Step 14 (Security Audit) for cycle 2 — delta scan against the cycle-1 baseline. Verdict remains PASS_WITH_WARNINGS; no Critical/High. Scope: JWT auth boundary (AZ-487) and UAV multipart upload + ImageSharp decode of attacker-controlled bytes (AZ-488). Both new packages (JwtBearer 8.0.21, ImageSharp 3.1.11 in Services.TileDownloader) checked. Cycle-2 delta: * 0 Critical / 0 High * 2 Medium: F-AUTH-2 (iss/aud not validated — by design until admin team publishes values, AZ-487 § Constraints), F-UAV-1 (ImageSharp decode now runs on attacker-controlled bytes — mitigations sufficient; pin to GHSA subscribe-and-bump policy). * 4 Low: F-AUTH-1 (DEV-ONLY secret in appsettings.Development.json — accepted), F-AUTH-3 (rate-limit gap extends to 401 floods — folds into cycle-1 I3), F-UAV-2 (JsonDocument.Parse on signature-validated claims — bounded by Kestrel header cap), D3 (JwtBearer shares D1 patch line). * 1 Informational: F-UAV-3 (reject reasons disclose gate structure — accepted UX trade-off; documented in contract). OWASP refresh: A01 / A07 move from N/A (with caveat) to PASS_WITH_WARNINGS (per-tenant authz absent; iss/aud + revocation gaps tracked). Pre-deploy operational gate added: deploy pipeline must verify JWT_SECRET != DEV-ONLY placeholder before promoting api. Artifacts: dependency_scan.md, static_analysis.md, owasp_review.md, infrastructure_review.md, security_report.md — all appended with a "Cycle 2 Delta" section preserving cycle-1 finding IDs. Co-authored-by: Cursor <cursoragent@cursor.com>
9.5 KiB
9.5 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.