refactor: enhance JWT authentication and CORS configuration

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.
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-14 19:48:25 +03:00
parent 2fe394d732
commit 7025f4d075
74 changed files with 8494 additions and 19 deletions
+132
View File
@@ -0,0 +1,132 @@
# Security Approach — Azaion.Missions
> **Status**: derived-from-code (autodev `/document` Step 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 and `00_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 8080` is **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_SECRET` and `DATABASE_URL` are 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 `JwtBearerHandler` at 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` |