mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 13:11:12 +00:00
f7dd6c98d8
ci/woodpecker/push/build-arm Pipeline failed
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>
237 lines
11 KiB
Markdown
237 lines
11 KiB
Markdown
# 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`
|
|
|
|
```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`:
|
|
|
|
```yaml
|
|
- 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**:
|
|
|
|
```yaml
|
|
- 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):
|
|
|
|
```nginx
|
|
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
|
|
|
|
- [x] `Dockerfile` reviewed
|
|
- [x] `.woodpecker/build-arm.yml` reviewed
|
|
- [x] `nginx.conf` reviewed
|
|
- [x] `e2e/docker-compose.suite-e2e.yml` reviewed
|
|
- [x] `.env.example` files reviewed (root + `mission-planner/`)
|
|
- [x] `.gitignore` reviewed
|
|
- [x] Cycle 2 deltas individually reviewed
|