- Enhanced `.env.example` with detailed CMake build flags and replay-mode strategy flags for development and CI environments. - Updated `.gitignore` to include a new deploy rollback bookmark. - Revised `_docs/_autodev_state.md` to reflect the current task status and steps. - Added new lessons to `_docs/LESSONS.md` regarding testing and architectural improvements. - Documented changes in `_docs/02_document/deployment/ci_cd_pipeline.md` to reflect the relaxed OpenCV version pin. - Updated test data documentation in `_docs/02_document/tests/test-data.md` to clarify fixture usage and paths. This commit continues the cycle-1 documentation sync and addresses various configuration updates for improved clarity and functionality.
16 KiB
Phase 3 — OWASP Top 10 (2021) Review
Review date: 2026-05-19
Scope: SUT production code path (src/gps_denied_onboard/**) and the surfaces it exposes / consumes in production.
Method: Walk each OWASP Top 10 (2021) category against the SUT's actual surface; cite the project's threat-model constraints (RESTRICT-OPS-*, NFT-SEC-*) and the Phase 1 + Phase 2 findings.
Surface Map (reference)
| Surface | Direction | Production exposure | Authentication | Notes |
|---|---|---|---|---|
| MAVLink to ArduPilot FC | bidirectional (C8) | UART/serial — physical wiring on UAV | MAVLink 2 message signing (Ed25519 derived passkey, AC-4.3 + D-C8-9) | Per-flight key rotation logged to FDR (NFT-SEC-03). iNav variant ships with documented residual risk. |
| Signed STATUSTEXT → GCS | outbound (C8) | UART → telemetry radio | Inherits MAVLink signing | Same channel as C8. |
Tile download HTTPS → satellite-provider |
outbound (C11 TileDownloader) |
Operator-workstation network only (RESTRICT-OPS-1, no airborne path) | TLS server-cert validation (httpx defaults, NEVER verify=False) + sidecar SHA-256 check on each tile |
Pinned base URL from C11Config. |
Tile upload HTTPS → satellite-provider ingest |
outbound (C11 TileUploader) |
Operator-workstation only (post-landing) | TLS + Ed25519 per-flight signing key (C11 PerFlightKeyManager) |
Public key envelope written to FDR for audit correlation. |
| Flights-service HTTPS → operator API | outbound (C12 FlightsApiClient) |
Operator-workstation only | TLS + bearer auth token (config-injected, _REDACTED in logs) |
Pinned base URL from C12.flights_api_base_url. |
| SSH from operator workstation → UAV companion | outbound (C12 ParamikoSshSessionFactory) |
Operator-workstation only | SSH key auth + RejectPolicy host-key validation (pinned known_hosts) |
Default mode is strict; AutoAddPolicy is forbidden by AZ-327. |
| FDR records → local FDR sink | local file write (all components) | Onboard tmpfs / local disk | N/A (local file) | Records are HMAC-chained per fdr_record_schema.md. |
Healthcheck python -m gps_denied_onboard.healthcheck |
inbound | Docker HEALTHCHECK probe, localhost only | N/A (process exit code) | No network surface. |
| No inbound network listeners in production | — | confirmed by NFT-SEC-05 — DNS blackhole + iptables OUTPUT REJECT in flight | — | This is the most important property of the threat model. |
The test-mode mock-suite-sat FastAPI fixture is the ONLY HTTP listener in any e2e topology; it lives in e2e/fixtures/, is excluded from the production Dockerfile, and runs only on the e2e-net.internal: true Docker network during tests.
A01:2021 — Broken Access Control
Status: N/A — no per-user authorization model exists.
- The SUT is a single-tenant onboard binary that runs as one OS user inside its container. There is no notion of multiple users, roles, or per-resource access policy in production.
- The two privileged operations that DO exist (start-session, end-session for the C11 upload key; arming the takeoff state in C5) are gated by the C8 FC's own armed/disarmed state — i.e., the safety gate is the physical aircraft state machine, not an application-level RBAC check.
- The operator-side C12 service runs ONLY on the operator workstation (RESTRICT-OPS-1); it has no airborne path and no role-based authorization beyond the SSH key + flights-API bearer token tied to the workstation identity.
Findings: 0.
A02:2021 — Cryptographic Failures
Status: Pass. All crypto choices are modern and well-implemented.
| Concern | Status | Evidence |
|---|---|---|
| Weak hashes for signing / integrity | None | All integrity = SHA-256 (sidecar, tile content_sha256 column, _canonical_hash.aggregate_tile_hash). All signatures = Ed25519. |
| Plaintext secrets at rest | None | C8 MAVLink signing passkey loaded from tmpfs-mounted runtime secret (production) or MAVLINK_SIGNING_PASSKEY_FILE Docker secret (test). C11 signing key is ephemeral per flight. C12 auth token is config-injected (no on-disk plaintext in production beyond the operator-side secret store). |
| Hardcoded keys / salts | None | Phase 2 SAST: 0 hardcoded credentials. All key material is generated per-flight via Ed25519PrivateKey.generate() or secrets.token_bytes(...). |
| TLS verification disabled | None | Phase 2 SAST: 0 verify=False matches; all httpx.Client(...) constructions use defaults (verify ON). |
| Secret zeroisation on session end | Best-effort with documented residual | c11_tile_manager/signing_key.py:340-365 zeros the project-controlled bytearray mirror of the secret. OpenSSL-side buffer freed via Ed25519PrivateKey refcount drop. Documented residual: bounded by upload-session lifetime + RESTRICT-OPS-1 (operator workstation no-swap). AZ-318 Risk-1. |
| Random source quality | Cryptographically secure | All key generation uses secrets.token_bytes or cryptography.hazmat's built-in CSPRNG. No random.random() on security paths. |
| CVE exposure in crypto libs | 1 informational (Phase 1 F4) | cryptography==42.0.8 has 4 GHSA advisories that touch pkcs12, OpenSSL FIPS provider config, and a CMS recipient bug — NONE of which are reachable from the SUT's actual usage (X.509 cert validation via httpx; Ed25519 sign/verify). Bump-when-convenient. |
Findings: 0 (the Phase 1 F4 informational item is recorded there; not duplicated here).
A03:2021 — Injection
Status: Pass. Cleared in Phase 2.
- SQL injection: 0 findings. All 19
cursor.execute(...)sites in C6 use psycopg%sparameterized placeholders. - Command injection: 0 findings. 1
subprocess.run(TensorRTtrtexec) — list args,shell=False, no untrusted input. - OS-path / file injection: 0 findings. All
Path(...)/open(...)calls take SUT-managed paths; no..traversal patterns matched. - No template / XSS surface (no HTML output).
Findings: 0.
A04:2021 — Insecure Design
Status: Pass — the design IS the security boundary.
The SUT's defining design property is "the airborne process has no network egress". That property is enforced at THREE layers:
- Architectural — no component constructs an
httpx.Clientor opens a socket on the airborne path. The C11 downloader / uploader and C12 flights-client are wired ONLY by the operator-workstation composition root (compose_root_operator), not the airbornecompose_root_airborne. - Operational (RESTRICT-OPS-1) — airborne container ships with iptables OUTPUT REJECT + DNS blackhole.
- Test (NFT-SEC-02 / NFT-SEC-05) — the airborne process is run inside a network namespace with no route to
satellite-provider's host; any attempted outbound TCP connection is a release-blocking test failure. The harness runs this test as part of every CI build.
| Design constraint | Where enforced | Verified by |
|---|---|---|
| No inbound network listeners on airborne path | compose_root_airborne does not wire any FastAPI / uvicorn / grpc server |
NFT-SEC-05 |
| No outbound network egress on airborne path | iptables + DNS blackhole + arch-level component placement | NFT-SEC-02 + NFT-SEC-05 |
| Tile downloads happen ONLY at operator workstation (pre-flight) | compose_root_operator is the only wiring that includes build_tile_downloader |
Architecture review + integration test FT-N-01 (operator) vs FT-P-17 (airborne) |
| Tile uploads happen ONLY post-landing on operator workstation | Same — build_tile_uploader is compose_root_operator only |
NFT-SEC-01 |
| MAVLink signing key generation happens on-bench, not at runtime | C8 documentation + boot sequence diagram | NFT-SEC-03 |
| Strategy implementations are EXCLUDED from the production binary at compile time | BUILD_* flags gate the import in runtime_root/*_factory.py |
CI pipeline gate (pyproject.toml [tool.gpsdo.build-flags]) |
Findings: 0.
Defensive observations:
- The "test substitute"
mock-suite-satis documented as non-architectural (_docs/02_document/architecture.md:277) precisely so it cannot accidentally become a production dependency. The fixture is retired the moment D-PROJ-2 lands the real ingest endpoint. - The strategy registry is keyed by string → module path through a CLOSED whitelist (
KNOWN_*_STRATEGIES), preventing config-controlled dynamic import vectors (Phase 2 F13).
A05:2021 — Security Misconfiguration
Status: Mostly pass; the configuration & infrastructure review (Phase 4, next) is the deeper check.
- No debug surface in production: SUT has no FastAPI / Flask / Django app; healthcheck binary uses structured exit codes, never a stack trace.
DEBUG=1is a developer-mode env var that only widens stderr verbosity for the CLI. - Pinned dependencies:
pyproject.tomlpins every direct dep;requirements*.lock(Phase 4 to verify) enforces transitive pins.opencv-python>=4.11.0.86,<4.12band is documented in_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md. - Build-flag gating: strategies excluded at compile time via
BUILD_PYTORCH_FP16_RUNTIME,BUILD_FAISS_INDEX,BUILD_VPR_*,BUILD_C7_TRT_FP16, etc. — non-built strategies cannot be selected even if config is wrong. - Test-only secrets clearly marked:
e2e/fixtures/secrets/mavlink-test-passkey.txtcarries a "TEST ONLY" annotation; production reads from a separate tmpfs-mounted secret. - TODO for Phase 4: container user (non-root?), file ownership in image layers, env var defaults, Docker secret mount permissions, network policy on
e2e-net.internal: true.
Findings (Phase 3 surface): 0. Configuration-layer concerns are deferred to Phase 4.
A06:2021 — Vulnerable and Outdated Components
Status: Covered in Phase 1. Summary:
| Severity (project context) | Count |
|---|---|
| Critical | 0 |
| High | 0 |
| Medium | 1 (onnx==1.18.0 GHSA-rcgw-4cgp-9q7q — reachable only via developer-mode model loading, not the production airborne path) |
| Low | 11 |
Findings (Phase 3-specific): 0 new — refer to _docs/05_security/dependency_scan.md.
A07:2021 — Identification and Authentication Failures
Status: Pass. All authenticated surfaces have a strong scheme; no obvious bypass.
| Surface | Scheme | Defense observed |
|---|---|---|
| MAVLink ↔ FC | MAVLink 2 message signing, per-flight key | Per-flight key rotation logged to FDR (NFT-SEC-03). iNav variant cannot enable signing — documented residual risk, surfaced in C8 docs (_docs/02_document/components/10_c8_fc_adapter/description.md). |
C11 upload session → satellite-provider |
Ed25519 per-flight signing key | Public-key envelope to FDR for audit correlation. Session lifetime ≤ upload window (minutes). |
| C12 → flights-service | Bearer token | Token loaded from config (operator-side secret store), _REDACTED in 100% of error messages and retry logs. |
| C12 → companion SSH | SSH key + RejectPolicy host-key validation |
paramiko.AutoAddPolicy is FORBIDDEN by AZ-327. Default mode strict rejects any unknown host. CVE-2026-44405 (Phase 1 F6, SHA-1 RSA) is materially mitigated. |
| C10 manifest verification | Ed25519 detached signature + SHA-256 of canonicalized JSON + trust-anchor public-key pinning | Fail-closed (verify_manifest returns outcome=FAIL on any deviation; never raises in the success path). |
Findings: 0.
A08:2021 — Software and Data Integrity Failures
Status: Pass. Integrity-checking is the spine of the manifest / tile / FDR contracts.
| Artifact | Integrity check | Where |
|---|---|---|
Manifest.json |
Ed25519 signature (Manifest.json.sig) + SHA-256 sidecar (Manifest.json.sha256) + canonical-JSON hashing of the embedded tile aggregate |
c10_provisioning/manifest_verifier.py |
| Each tile JPEG | SHA-256 sidecar (<tile>.sha256), enforced both at download (tile_downloader.py) and on read (postgres_filesystem_store.py SELECT + sidecar verify) |
C6 + C11 |
| FDR records | HMAC-chained schema per _docs/02_document/contracts/shared_fdr_client/fdr_record_schema.md |
fdr_client/records.py |
| TensorRT engine builds | Engine path includes content-hash of source ONNX + build-config; rebuild on mismatch | c7_inference/tensorrt_runtime.py |
| Replay fixtures | e2e/fixtures/sitl-replay-fixture-* ship with a manifest builder that derives SHA-256 over every artefact |
e2e/fixtures/builders/... |
Software-update integrity: out-of-scope for the onboard binary; operator-workstation provisioning is handled by the parent suite (D-PROJ-1) which Phase 4 will review at the Dockerfile / image layer.
Findings: 0.
A09:2021 — Security Logging and Monitoring Failures
Status: Pass — disciplined structured logging with security-event coverage.
| Concern | Status | Evidence |
|---|---|---|
| Security-relevant events emit a structured FDR record | Yes | c11.upload.session.key.public, c11.upload.session.key.zeroised, c11.upload.signature_rejected, c12.flights.fetch.failed, c10.manifest.verify.*, c8.mavlink.signing.* |
Failures (auth, integrity, transport) emit ERROR-level logs with a stable kind field |
Yes | kind="c12.flights.fetch.failed" + reason∈{auth,not_found,connect:*}, kind="c10.manifest.verify.failed", kind="c2.vpr.dim_mismatch", kind="c11.upload.signature_rejected" |
| Secrets are not logged | Yes | 13 files use the _REDACTED / redact pattern. Phase 2 confirmed: auth token, MAVLink passkey, and SSH private-key material are never logged in plaintext. |
| Logs are tamper-resistant | Best-effort | FDR records are HMAC-chained; structured logs to stderr/journal use a defined schema (logging/structured.py). Operator-side log retention is a parent-suite concern. |
| Monitoring / alerting plumbed | Out of scope | Parent-suite responsibility — _docs/02_document/deployment/observability.md documents what the SUT emits, not where it is shipped. |
Findings: 0.
A10:2021 — Server-Side Request Forgery (SSRF)
Status: Pass. No SUT code accepts a URL/host parameter from external input.
c11_tile_manager/{tile_downloader,tile_uploader}.pybuild URLs by concatenating a config-pinnedbase_urlwith internally-derived path segments (zoom/x/y.jpg,upload/<flight_id>/<tile_uuid>). No external?url=...parameter or redirect-following from untrusted source.c12_operator_orchestrator/flights_api/httpx_client.pyusesclient.get(url, ...)whereurlis built fromself._config.flights_api_base_url(config-pinned) + a derived path segment containing the validatedflight_idUUID. No external host control.httpx.Clientdefaults —follow_redirects=False. SSRF via 30x redirect to internal addresses is blocked unless the caller explicitly opts in (no SUT caller does).- DNS resolution happens against the operator's resolver only; in production (airborne) DNS is blackholed.
Findings: 0.
Cross-Reference Index
| Source | Phase 3 § | Note |
|---|---|---|
_docs/05_security/dependency_scan.md |
A06, A02 | Phase 1 — 12 vulnerabilities triaged |
_docs/05_security/static_analysis.md |
A03, A02 | Phase 2 — 1 informational hardening recommendation |
_docs/02_document/architecture.md § "Operating envelopes" |
A04 | RESTRICT-OPS-1 / NFT-SEC-05 enforcement |
_docs/02_document/tests/security-tests.md |
A04, A07, A08 | NFT-SEC-01..05 test contract |
_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md |
A06 | D-CROSS-CVE-1 deferred pin |
Self-Verification
- All 10 OWASP 2021 categories addressed with status + finding count + evidence
- N/A categories (A01) explicitly justified against the threat model, not silently skipped
- Findings from Phase 1 / Phase 2 cross-referenced where they map to OWASP categories rather than re-listed
- Surface map at top is consistent with
_docs/02_document/architecture.md - No new HTTP listeners / dynamic-input surfaces missed (confirmed by
Grepfor FastAPI/uvicorn/server.serve patterns)