Files
ui/_docs/05_security/infrastructure_review.md
Oleksandr Bezdieniezhnykh f7dd6c98d8
ci/woodpecker/push/build-arm Pipeline failed
[AZ-501] [AZ-502] Cycle 2 Step 14 security audit + inline fixes
Security audit (5 phases) → reports under _docs/05_security/.

AZ-501 (F-SAST-1, HIGH): Externalize hardcoded Google Geocode key
from mission-planner/src/config.ts to VITE_GOOGLE_GEOCODE_KEY via
new GeocodeService.ts; fail-soft warn when unset; STC-SEC1D static
deny-list gate; +5 unit tests in tests/mission_planner_geocode.test.ts.

AZ-502 (F-DEP-1, HIGH): Force vite>=6.4.2 and postcss>=8.5.10 via
package.json overrides in both roots; clean reinstall clears all
bun audit advisories.

Test-spec sync (Step 12) + Update Docs (Step 13) deltas: AC-43, AC-44,
NFT-SEC-09b, FT-P-61, FT-N-17, ripple log, batch_12 report.

Pending user actions: revoke Google + OWM keys (AC-6 / AZ-499 AC-7).

229 PASS / 13 SKIP / 0 FAIL on static + fast suites.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 05:31:11 +03:00

11 KiB

Infrastructure & Configuration Review — Azaion UI

Date: 2026-05-12 Scope: Dockerfile, nginx.conf, .woodpecker/build-arm.yml, e2e/docker-compose.suite-e2e.yml, .env.example files, .gitignore Cycle: Phase B / Cycle 2


Summary

Severity Count
Critical 0
High 0
Medium 4 (F-INF-1 .. F-INF-4)
Low 1 (F-INF-5)

All findings are pre-existing infrastructure hardening gaps — no new findings introduced by Cycle 2. Several findings here overlap with owasp_review.md A05/A08 entries and are cross-referenced.


Container Security

Dockerfile

FROM --platform=$BUILDPLATFORM oven/bun:1.3.11-alpine AS build
WORKDIR /app
COPY package.json bun.lock* ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build

FROM nginx:alpine
ARG CI_COMMIT_SHA=unknown
ENV AZAION_REVISION=$CI_COMMIT_SHA
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

Verified controls:

  • Multi-stage build → only static assets land in the runtime image; no Bun, no node_modules, no source.
  • Alpine base images → minimal attack surface.
  • --frozen-lockfile → no transitive drift between bun install and what was tested.
  • CI_COMMIT_SHA baked in for traceability.
  • EXPOSE 80 — port surface limited to nginx HTTP.

Findings:

  • F-INF-5 — runs as nginx default root for the master process. The nginx:alpine image's default config drops worker processes to nginx user; the master remains root. Consider switching to nginxinc/nginx-unprivileged if the suite ingress permits a non-80 listen port. Severity: LOW (industry-standard pattern; minor improvement).
  • No HEALTHCHECK directive in the Dockerfile (the e2e compose adds one externally). For Kubernetes / external orchestration, add HEALTHCHECK CMD wget -qO- http://localhost:80/ || exit 1. Severity: LOW (operational, not security).

No HIGH/MEDIUM issues with the Dockerfile itself.


CI/CD Security

.woodpecker/build-arm.yml

Verified controls:

  • Registry credentials sourced from secrets (from_secret: registry_token etc.) — never in repo.
  • docker login --password-stdin — token not in argv (would otherwise leak via ps).
  • Branch-restricted (when.branch: [dev, stage, main]) — feature branches do NOT push to registry.
  • OCI labels (org.opencontainers.image.revision, created, source) stamped at build time.
  • The image tag is branch-derived (${CI_COMMIT_BRANCH}-arm) — production deployments pin to the SHA via OCI label.

Findings:

F-INF-1 — bun audit not gated in CI — MEDIUM

Evidence: .woodpecker/build-arm.yml runs only docker build + docker push. The static-test pipeline runs in the developer's scripts/run-tests.sh, NOT in CI. The Cycle 2 dependency findings (F-DEP-1 vite High, F-DEP-2/F-DEP-3 Moderate) would not have failed CI.

Risk: a future dependency upgrade introducing a Critical/High CVE could ship to dev-arm undetected.

Remediation: insert a step before build-push:

- name: dep-audit
  image: oven/bun:1.3.11-alpine
  commands:
    - bun audit --severity high     # exits non-zero if any High/Critical

Severity: MEDIUM (visible CVE exposure, easy to fix).

F-INF-3 — No vulnerability scan on the produced image — MEDIUM

Evidence: .woodpecker/build-arm.yml does not invoke Trivy, Grype, or any image scanner. Base-image CVEs in nginx:alpine are invisible to CI.

Risk: nginx alpine releases ship with periodic CVEs (latest run-time vulns in the OS packages). Without a scan, the image can ship vulnerable.

Remediation:

- name: image-scan
  image: aquasec/trivy:latest
  commands:
    - trivy image --severity HIGH,CRITICAL --exit-code 1 \
        $REGISTRY_HOST/azaion/ui:$TAG

Severity: MEDIUM.

F-INF-4 — No SBOM emission and no image signing — MEDIUM

Evidence: pipeline produces and pushes images but does not emit an SBOM (Syft/cyclonedx) and does not sign images (cosign).

Risk: registry compromise or MITM during pull cannot be detected. Post-deploy SBOM-based vulnerability triage is impossible.

Remediation: best owned at the suite level — coordinate with the registry team. Adding cosign requires a key management decision (KMS vs. file-based). Typical ordering:

  1. Add syft packages docker:$image -o cyclonedx-json > sbom.json — emit and store SBOM as a build artifact.
  2. Configure cosign keyless via OIDC (if Woodpecker integrates) OR file-based key from secrets.
  3. cosign sign --key cosign.key $image step + cosign verify step in the deploy pipeline.

Severity: MEDIUM (supply-chain integrity).


Network Security & Headers

nginx.conf

Verified controls:

  • Strict proxy_pass to fixed upstream service names (no user-controlled URL routing).
  • client_max_body_size 500M — bounded.
  • SPA fallback try_files $uri $uri/ /index.html — safe (no upstream rewrite).
  • proxy_read_timeout 86400 on /api/annotations/ (SSE) and 600 on /api/detect/ (long video) — explicit per-route limits, not a global config.

Findings:

F-INF-2 — Missing security response headers — MEDIUM

Evidence: zero add_header directives in nginx.conf. None of the standard hardening headers are emitted to the browser:

  • Content-Security-Policy
  • X-Frame-Options / CSP frame-ancestors
  • Strict-Transport-Security (depends on suite ingress decision — coordinate)
  • Referrer-Policy: strict-origin-when-cross-origin
  • X-Content-Type-Options: nosniff

Bearer-redaction: SSE URLs include ?access_token=… → currently logged in plaintext to nginx access logs. No redaction directive.

Risk:

  • Without CSP, any future XSS (we have none today, but the surface evolves) gets unrestricted execution.
  • Without frame-ancestors/X-Frame-Options, the SPA can be framed → clickjacking on the operator's session.
  • Without Referrer-Policy, internal SPA URLs leak to external sites if the operator clicks an outbound link.
  • Bearers persist in nginx access logs (operator-internal but still — log retention compounds).

Remediation (one PR; recommended starting point per _docs/00_problem/security_approach.md §9):

add_header Content-Security-Policy "default-src 'self'; img-src 'self' https: data:; connect-src 'self' https://api.openweathermap.org/; frame-ancestors 'none'; object-src 'none'" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Content-Type-Options "nosniff" always;
# add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;  # decide with suite ingress

# In the SSE location (e.g. /api/annotations/):
log_format azaion_redact '$remote_addr - $remote_user [$time_local] '
                         '"$request_method $uri ' # drop ?args from the access log
                         '$server_protocol" $status $body_bytes_sent';
access_log /var/log/nginx/access.log azaion_redact;

Cycle 2 specific: the new <TileLayer crossOrigin="use-credentials"> (AZ-498) needs the production env to point at the same-origin nginx path. The CSP connect-src 'self' above already permits this; if the suite-internal satellite-provider lives on a different origin it must be added explicitly. This is captured in the AZ-498 deploy gate (Step 16).

Severity: MEDIUM.


Environment Configuration

.env.example files

File Status
.env.example (ui/) Clean — only documentation comments and empty/placeholder values. Cycle 2 added VITE_OWM_API_KEY=<your-openweathermap-api-key>, VITE_OWM_BASE_URL=, VITE_SATELLITE_TILE_URL= placeholders.
mission-planner/.env.example Clean — same pattern. Includes VITE_SATELLITE_TILE_URL=https://server.arcgisonline.com/... (legacy default; does NOT contain a real auth key).

Verified: no real secrets committed to either .env.example. Both use the <your-...> convention.

.gitignore

Verified (grep against root .gitignore): excludes .env.local, .env.development.local, .env.test.local, .env.production.local. Real secrets are properly kept out of git.

Recommendation: add mission-planner/.env.local and equivalents to mission-planner/.gitignore (or a root-level pattern that catches both roots) for symmetry. Verify by grep — not part of this audit's automated checks.


E2E Harness Security

e2e/docker-compose.suite-e2e.yml

Verified controls:

  • Isolated azaion-test-net bridge network — no host network access for the runner.
  • Stubbed external endpoints (owm-stub, tile-stub) — Playwright tests cannot accidentally hit production OWM or external tile providers.
  • Test DB password is azaion/azaion — visible in plaintext, but acceptable: the DB is bound to the isolated network and lives only for the e2e run.
  • ENABLE_TEST_ONLY_ENDPOINTS: "true" is gated to the e2e profile.
  • The azaion-ui image build wires VITE_SATELLITE_TILE_URL to the in-cluster tile-stub — confirming AZ-498's env-driven design works end-to-end with no real-world tile auth required.

No findings.


Cycle-2 specific infrastructure review (AZ-498, AZ-499)

Change Infra review
VITE_SATELLITE_TILE_URL introduction OK. .env.example documents prod requirement (same-origin nginx path) explicitly. E2E compose wires the test value. No infra regression.
Cookie-credentialed tile fetch (crossOrigin="use-credentials") OK conditional on prod env override. The default (http://localhost:5100/...) only works in local dev; misconfiguration in stage/prod (forgetting to set the env var) results in tile failure (UX impact, no security regression). The Step 16 deploy gate covers this.
STC-SEC1C static check (OWM key in mission-planner/) OK — added to scripts/run-tests.sh. No CI integration today (see F-INF-1) — same gap that affects everything else.
mission-planner/.env.example updated OK — placeholder convention preserved, no real secrets.

Recommendations roll-up

ID Severity Effort Owner Recommendation
F-INF-1 MEDIUM 1 SP UI Add bun audit --severity high step to .woodpecker/build-arm.yml
F-INF-2 MEDIUM 2 SP UI Add CSP / X-Frame-Options / Referrer-Policy / X-Content-Type-Options + bearer-redaction log format to nginx.conf
F-INF-3 MEDIUM 2 SP UI / DevOps Add Trivy image scan step to .woodpecker/build-arm.yml
F-INF-4 MEDIUM 3-5 SP Suite-wide SBOM + cosign — coordinate registry decision suite-wide
F-INF-5 LOW 1 SP UI Switch to nginxinc/nginx-unprivileged and add HEALTHCHECK directive

Self-verification

  • Dockerfile reviewed
  • .woodpecker/build-arm.yml reviewed
  • nginx.conf reviewed
  • e2e/docker-compose.suite-e2e.yml reviewed
  • .env.example files reviewed (root + mission-planner/)
  • .gitignore reviewed
  • Cycle 2 deltas individually reviewed