# Security Audit — Consolidated Report **Audit window**: 2026-05-19 **Auditor**: autodev Step 14 (greenfield flow) — `.cursor/skills/security/SKILL.md` § Phases 1–5 **Scope**: `gps-denied-onboard` SUT — production code path (`src/gps_denied_onboard/**`), pinned dependencies, container build artefacts, docker-compose topology, committed secret hygiene. **Out of scope**: parent-suite components (D-PROJ-1 deploy/observability stack, D-PROJ-2 ingest service, satellite-provider, operator GCS UI); third-party SITL images beyond their tagging hygiene. ## Executive Summary The SUT's security posture is built around a single defining design property: **the airborne process has no inbound or outbound network surface**. That property is enforced at three independent layers (architectural, operational iptables + DNS blackhole, test-harness verification via NFT-SEC-02 / NFT-SEC-05), which keeps the externally-reachable attack surface at zero in flight. The pre-flight (operator-workstation) phases DO touch the network — over TLS, with pinned hosts, parameterized SQL, redacted logs, and Ed25519-signed manifests / uploads. Result: **0 Critical, 0 High, 5 Medium, 17 Low.** No release-blocking finding. The Medium findings are container-hardening gaps (root user in the production image, dev extras in the production image, moving-tag base images for SITL) that would be raised to High under a multi-tenant or internet-exposed threat model but are bounded to Medium here by the closed-system invariant. | Severity | Count | Phase 1 (Deps) | Phase 2 (SAST) | Phase 3 (OWASP) | Phase 4 (Infra) | |---|---|---|---|---|---| | Critical | 0 | 0 | 0 | 0 | 0 | | High | 0 | 0 | 0 | 0 | 0 | | Medium | 5 | 1 | 0 | 0 | 4 | | Low | 17 | 11 | 1 (informational) | 0 | 5 | | Total | 22 | 12 | 1 | 0 | 9 | **Release-blocking findings**: 0. **Remediation expected before next major dependency refresh**: F14 (non-root container), F15 (production `[dev]` extras), F16 (`mavproxy:latest`). **Remediation deferred to a future hardening pass**: F13 (plugin-loader assertion), F17 (digest-pin production base images), F18-F22 (housekeeping). **Cross-component leftover already tracked**: D-CROSS-CVE-1 — `opencv-python` pin coupling with `gtsam` numpy<2 ABI block (`_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md`). ## Threat Model Snapshot | Property | Status | Phase | Evidence | |---|---|---|---| | Airborne process has no network listener | enforced | A04 | `compose_root_airborne` wires no HTTP server; NFT-SEC-05 enforces at runtime | | Airborne process has no network egress | enforced | A04 | iptables OUTPUT REJECT + DNS blackhole (RESTRICT-OPS-1); NFT-SEC-02 verifies in CI | | Operator-workstation HTTP traffic is TLS-validated | enforced | A02 | 0 `verify=False` matches; pinned base URLs only | | Tile / Manifest integrity end-to-end | enforced | A08 | SHA-256 sidecar + Ed25519 detached signatures | | MAVLink ↔ FC channel is signed | enforced (AP) / documented residual (iNav) | A07 | Per-flight key rotation logged to FDR (NFT-SEC-03) | | In-container runtime executes as non-root | **GAP — F14** | A05, F14 | Production Dockerfiles missing `USER` directive | | Production runtime image excludes dev tools | **GAP — F15** | A05, F15 | `pip install -e ".[dev]"` ships pytest, ruff, mypy into prod | | All container images are digest-pinned | **GAP — F16/F17** | A05, F16, F17 | Mixed: `tile-cache-builder` digest-pinned, production images use floating tags | | Single-tenant binary (no per-user RBAC) | by design | A01 | One-OS-user container, FC arm/disarm IS the privilege gate | | Secrets never logged | enforced | A09 | `_REDACTED` convention across 13 files | | Test secrets are synthetic, marked TEST ONLY | enforced | P6 | Both committed passkey files contain `0123…ef`-repeated bytes | ## Per-Phase Outcomes ### Phase 1 — Dependency Scan (`_docs/05_security/dependency_scan.md`) `pip-audit` against the pinned environment surfaced 12 advisories across 5 packages. | Package | Advisories | Project-specific finding | |---|---|---| | `onnx==1.18.0` | 1 (GHSA-rcgw-4cgp-9q7q) | Reachable only via developer-mode model loading; SUT serves prebuilt engines on the production path. Medium. | | `cryptography==42.0.8` | 4 | None reachable from SUT usage (Ed25519 sign/verify + httpx default X.509). Low. | | `numpy==1.26.4` | 4 | None reachable from SUT usage (no untrusted-input numerical ingestion). Low. | | `protobuf==4.25.3` | 2 | None reachable; protobuf usage is internal serialization to the FDR layer. Low. | | `setuptools` | 1 | Dev-time only. Low. | **D-CROSS-CVE-1 outcome**: re-running `pip-audit` against the relaxed pin (`opencv-python>=4.11.0.86,<4.12`) confirmed that CVE-2025-53644 is NOT flagged for `opencv-python==4.11.0.86`. The deferred-bump leftover was updated to record this; the residual risk window is effectively closed at the current pin. Bump to 4.12.x remains coupled to the `gtsam` numpy-2 ABI work. ### Phase 2 — Static Analysis (SAST) (`_docs/05_security/static_analysis.md`) Pattern-driven ripgrep scan across 259 Python files in `src/`. | Category | Findings | |---|---| | SQL injection | 0 — all 19 `cur.execute(...)` sites are parameterized | | Command injection | 0 — 1 `subprocess.run` (TensorRT `trtexec`), list args, no untrusted input | | Hardcoded secrets / weak crypto | 0 — Ed25519, SHA-256, `secrets.token_bytes` | | Insecure deserialization | 0 — no `pickle.loads` / `eval` / `exec` on untrusted data; `json.loads` only on local SUT-managed files | | TLS / verification bypass | 0 — all `httpx.Client` defaults preserved | | **F13** (informational) — Dynamic `__import__` via plugin registry | 1 — bounded by closed `KNOWN_*_STRATEGIES` whitelist; hardening recommended | Defense-in-depth observations: per-flight ephemeral Ed25519 signing in C11; `secrets.token_bytes` for MAVLink passkey generation; `paramiko.RejectPolicy` (NOT `AutoAddPolicy`) for SSH; `_REDACTED` convention enforced across 13 files; secret-buffer zeroising on session end (AZ-318). ### Phase 3 — OWASP Top 10 Review (`_docs/05_security/owasp_top_10_review.md`) All 10 categories assessed against the SUT's actual surface. A01 (Broken Access Control) is N/A by design (single-tenant binary, no per-user RBAC). A02 through A10 each cleared with 0 findings beyond those already reported in Phases 1, 2, and 4. The closed-system threat model is itself the strongest A04 (Insecure Design) property and is enforced at three layers. ### Phase 4 — Configuration & Infrastructure (`_docs/05_security/config_infra_review.md`) 9 findings (4 Medium, 5 Low) — all container-hardening gaps in the production images. The test stack is well-secured (`internal: true` network, digest-pinned tile-cache-builder, public-boundary discipline on the runner). The gap is consistency: the project already KNOWS how to do this (see `e2e/fixtures/tile-cache-builder/Dockerfile`) — production images need to match that bar. ## Remediation Plan ### Recommended before next dependency-refresh cycle | ID | Title | Effort | Coupling | |---|---|---|---| | F14 | Add non-root `USER` directive to `docker/companion-tier1.Dockerfile` + `docker/operator-orchestrator.Dockerfile` | 2 pts | Couples with F22 (chown WORKDIR) | | F15 | Replace `pip install -e ".[dev]"` with runtime-only install in both production Dockerfiles | 2 pts | Standalone | | F16 | Pin `ardupilot/mavproxy:latest` and `ardupilot/ardupilot-sitl:plane-stable` to explicit versions or SHA256 digests | 1 pt | Standalone | | F1 | Bump `onnx` from 1.18.0 → 1.19+ when next dep cycle lands (GHSA-rcgw-4cgp-9q7q) | 2 pts | Couples with C7 model-loader review | ### Recommended at next hardening pass | ID | Title | Effort | |---|---|---| | F13 | Add `assert strategy in KNOWN_*_STRATEGIES` at each `__import__` call site (defense-in-depth, redundant today) | 2 pts | | F17 | Pin production base images by SHA256 digest, with explicit refresh cadence | 1 pt | | F18 | Delete or fix the orphan `docker/mock-suite-sat-service.Dockerfile` | 1 pt | | F19 | Remove unused `curl` from `docker/operator-orchestrator.Dockerfile` | 1 pt | | F20 | Add upper bound to runner's `opencv-python` pin | 1 pt | | F21 | Update `.env.example` to current secret paths + env var names | 1 pt | | F22 | Coupled with F14 — chown WORKDIR after adding `USER` directive | included in F14 | ### Deferred / external - **D-CROSS-CVE-1** — `opencv-python` 4.12.x bump deferred until `gtsam` ships a numpy-2 compatible release. Tracked at `_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md`. Residual exposure window is currently closed (CVE-2025-53644 no longer flagged at 4.11.0.86). - **Per-flight rekeying audit cadence** — out of scope for this audit; will be revisited when D-PROJ-2 ingest endpoint lands and the upload contract is finalised. - **Operator-workstation host hardening** (no-swap RESTRICT-OPS-1, etc.) — parent-suite responsibility; cross-referenced in `_docs/02_document/architecture.md` § Operating envelopes. ## Audit Method Footnote - Phase 1 used `pip-audit` against a freshly resolved `pip freeze` of the SUT venv (the project's editable install was filtered out per the project-package exclusion rule). - Phase 2 used `ripgrep` with the patterns enumerated in `.cursor/skills/security/SKILL.md` § Phase 2; every match was cross-validated by reading the call site. - Phase 3 was a desk review against the OWASP 2021 list, with each category mapped to the SUT's actual production surface (see `static_analysis.md` "Surface Map") rather than a generic web-app threat model. - Phase 4 was a desk review of all 6 Dockerfiles, 2 docker-compose files, all committed secrets, `.env.example`, and `.gitignore`. ## Self-Verification - [x] Each phase has its own dedicated artefact and is cross-referenced here - [x] Severity counts in the Executive Summary sum to the per-phase counts in the Outcomes section (22 = 12 + 1 + 0 + 9) - [x] No finding is silently downgraded — every project-specific severity calibration is justified at the finding site - [x] D-CROSS-CVE-1 leftover updated and cross-referenced; no stale or missing tracker entry - [x] Remediation effort estimates use the user's complexity-points rule (2-3 pts default, 5 pts max for big items, no 13-point monolith)