Updated JWT authentication to use configuration values instead of hardcoded secrets, improving security and flexibility. Enhanced CORS policy to conditionally allow origins based on configuration settings, with logging for permissive defaults. Updated README to reflect project renaming and clarify service context.
10 KiB
Security Approach — Azaion.Missions
Status: derived-from-code (autodev
/documentStep 6, 2026-05-14). All claims below trace to actual code, configuration, or a tracked suite-level finding. Items called out as "currently divergent" are intentional carry-forward — see_docs/02_document/architecture.md§ 8 ADRs and00_discovery.md§ Spec ↔ Code Divergences.
1. Authentication
Mechanism: JWT bearer (HS256) with local validation only — this service never calls back to the issuing admin service.
Trust model: a single shared HMAC secret (JWT_SECRET) is provisioned to admin (issuer) and to every backend service on each edge device (validators). Rotation requires a coordinated re-deploy across all of them.
Validation parameters (Auth/JwtExtensions.cs):
| Parameter | Value | Notes |
|---|---|---|
| Algorithm | HS256 (SymmetricSecurityKey(UTF-8(JWT_SECRET))) |
Symmetric → asymmetric switch is suite-wide concern, not in this Epic |
ValidateLifetime |
true |
Tokens with exp in the past are rejected |
ClockSkew |
TimeSpan.FromMinutes(1) |
Tighter than .NET's 5-min default |
ValidateIssuer |
false |
Known CMMC L2 finding (suite-tracked AZ-487/AZ-494); consistent with shared-secret trust |
ValidateAudience |
false |
Same finding as above |
ValidateIssuerSigningKey |
true |
Always required when ValidateLifetime/ValidateIssuer are set explicitly |
Failure outcomes:
| Condition | HTTP code |
|---|---|
Missing Authorization header |
401 |
| Invalid signature | 401 |
| Expired token (with 1-min skew) | 401 |
Token signed with old JWT_SECRET after rotation |
401 (until coordinated re-deploy + re-login) |
Valid signature + lifetime, but missing permissions=FL claim |
403 |
admin outage: tokens issued before the outage continue to validate locally. This service does not require admin to be reachable for any flow. Once issued tokens expire, new logins fail at admin's end (UI concern), but this service stays up.
2. Authorization
Single named policy: "FL". Every controller action in 01_vehicle_catalog and 02_mission_planning carries [Authorize(Policy = "FL")]. The policy is satisfied by a permissions claim with value "FL".
Role → permission matrix is suite-level (../../suite/_docs/00_roles_permissions.md); this service does NOT enforce roles, only the FL permission.
No per-method authz: every protected endpoint has the same gate. There is no notion of "read-only operator" vs "full-access operator" inside this service.
Hardcoded policy name carries legacy wording: the string "FL" (originally "Flight") survives the rename to missions. Renaming the permission code is a fleet-wide auth change (would invalidate every issued token until new ones are minted) and is NOT in this Epic. Tracked as a TODO in ../../suite/_docs/00_roles_permissions.md.
Typo risk: the "FL" string is repeated in feature controllers as a raw string. A typo silently turns into a permanent 403 with no compile-time detection. Mitigation: code review + the module-layout.md § Verification Needed #4 entry.
No per-user attribution / audit: the JWT's sub / user-id claim is parsed by JwtBearerHandler into ClaimsPrincipal, but nothing in this service consumes it. Logs are timestamp-only — incident reconstruction requires correlation by request time, not by user.
3. Data protection
At rest: PostgreSQL on-disk encryption is the device-level concern (suite-level, NOT this service). This service does NOT encrypt data at the column level.
In transit:
- The container
EXPOSE 8080is plain HTTP. TLS termination is handled by the suite's edge reverse proxy (per../../suite/_docs/00_top_level_architecture.md). - No
app.UseHttpsRedirection()in this service. If the reverse proxy is misconfigured or absent, traffic between the operator UI and this service may be cleartext on the local edge network.
Secrets management:
JWT_SECRETandDATABASE_URLare env vars (with hardcoded dev fallbacks). See § 6 below.- No secret manager (Vault, AWS SM, K8s Secrets) — secrets are baked into the device's docker compose env at provisioning time.
- No runtime gate prevents startup with the dev fallback in production (ADR-005 carry-forward).
4. Input validation
None at the application layer. No [Required], no [Range], no min-length attributes; no custom validators. The following are all accepted by ASP.NET Core model binding without rejection:
| Bad input | Accepted today |
|---|---|
CreateVehicleRequest.Name = "" |
yes |
CreateVehicleRequest.BatteryCapacity = -1 |
yes |
CreateVehicleRequest.Type = (VehicleType)999 |
yes — int casts to enum without range check |
CreateWaypointRequest.OrderNum = -1 |
yes |
CreateWaypointRequest.GeoPoint = null (or all three of Lat/Lon/Mgrs null) |
yes |
GetMissionsQuery.Page = -1 / PageSize = 1_000_000 |
yes — no bounds |
This is a carry-forward concern — input-shape testing is not a security gate today; the threat surface is mitigated by the closed edge network and authenticated single-operator workflow. Tightening is on the autodev backlog (Phase B feature cycle).
5. CORS
Open in every environment: AllowAnyOrigin / AllowAnyMethod / AllowAnyHeader in Program.cs. Spec does not mandate a CORS policy.
The closed edge network behind the suite reverse proxy is the deployment-shape mitigation. Worth confirming on the first production rollout that the upstream proxy whitelists origins; if not, this is a finding to surface.
6. Production-deploy footguns
These are explicit security-relevant risks the code carries today, all tracked at the suite level or as carry-forward:
| Footgun | Where | Mitigation |
|---|---|---|
Dev fallback for JWT_SECRET silently accepted in production if env var unset |
Program.cs (no IsDevelopment gate, ADR-005) |
Suite-level remediation pending; recommend "fail-fast at startup if JWT_SECRET is unset OR equals the well-known dev fallback" |
Dev fallback for DATABASE_URL silently accepted in production if env var unset |
Program.cs |
Same pattern as JWT_SECRET; misconfigured deploy hits localhost Postgres on the device, which usually doesn't exist → process exits, but the failure mode is loud (crash) not silent |
| Swagger UI mounted unconditionally | Program.cs (no IsDevelopment gate, ADR-005) |
Reverse-proxy-level allowlist on /swagger is the suite-level mitigation; verify on first production rollout |
CORS AllowAnyOrigin/Method/Header in production |
Program.cs |
Reverse-proxy origin whitelist is the suite-level mitigation |
| No HTTPS redirection | Program.cs (no app.UseHttpsRedirection()) |
Reverse proxy enforces TLS upstream |
| Stack trace logged for unhandled 500s | Middleware/ErrorHandlingMiddleware.cs LogError(ex, ...) |
Stack is logged only — NOT returned in the HTTP response body (the 500 body is the generic "Internal server error" message from middleware) |
JWT iss/aud validation disabled |
Auth/JwtExtensions.cs |
CMMC L2 row 3 finding; tracked at suite level under AZ-487 / AZ-494 |
| Cascade-delete is NOT transaction-wrapped (data-integrity, not auth) | Services/MissionService.cs, Services/WaypointService.cs (ADR-006) |
One-line fix queued; recommended to land with B6 |
Hardcoded permission string "FL" in feature controllers |
Controllers/{Vehicles,Missions}Controller.cs |
Risk: typo silently turns into permanent 403; mitigation by code review + module-layout.md |
Permission code "FL" retains legacy "Flight" wording post-rename |
Auth/JwtExtensions.cs |
Fleet-wide auth change deferred (not in this Epic); TODO in ../../suite/_docs/00_roles_permissions.md |
7. Audit logging
None at the application level today. The only structured log emitted by application code is _logger.LogError(ex, "Unhandled exception") in ErrorHandlingMiddleware for 500s. There is:
- No correlation ID per request
- No per-user attribution (the JWT user-id claim is not consumed)
- No security-event log (auth failures are logged by
JwtBearerHandlerat default ASP.NET Core levels — typically Information, not surfaced as a dedicated audit channel) - No data-access audit (writes/deletes go directly through linq2db with no wrapper that emits an audit row)
Production incident response on this service today requires grep-by-timestamp correlation against the operator UI's logs and admin's issuance logs.
8. Threat model summary (one-paragraph)
The deployment shape — closed edge network, single operator per device, suite reverse proxy enforcing TLS and origin allowlisting upstream, Watchtower restart on crash — is the primary defence-in-depth layer for everything not handled by HS256 JWT validation and the FL permission gate. The known weak points (dev fallbacks not gated on IsDevelopment(), no input validation, no application-level audit log, iss/aud not validated) are documented and tracked, with the most critical ones (CMMC L2 row 3, default-vehicle race) under suite-level or B-ticket Jira IDs. This Epic (rename + GPS-Denied removal) does not change the security posture; it preserves every current invariant.
9. References
| Concern | File |
|---|---|
| Auth registration | Auth/JwtExtensions.cs |
| Authorization attribute usage | Controllers/AircraftsController.cs (post-B6: VehiclesController.cs), Controllers/FlightsController.cs (post-B6: MissionsController.cs) |
| Error envelope (no stack-leak) | Middleware/ErrorHandlingMiddleware.cs |
| Env var resolution + dev fallbacks | Program.cs |
| CMMC L2 scorecard | ../../suite/_docs/05_security/cmmc_l2_scorecard.md |
Roles & FL permission origin |
../../suite/_docs/00_roles_permissions.md |
| ADR-005 (Swagger + dev fallbacks) | _docs/02_document/architecture.md § 8 |
| ADR-002 (PascalCase wire shape) | _docs/02_document/architecture.md § 8 |
| Component identity description | _docs/02_document/components/05_identity/description.md |
| Component http-conventions description | _docs/02_document/components/06_http_conventions/description.md |