mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 15:51:10 +00:00
[AZ-529] [AZ-530] Cycle-2 security audit reports
Step 14 (Security Audit) output for cycle 2. Verdict: FAIL — 2 Critical (F-INFRA-1, F-INFRA-2) + 4 High (F-INFRA-3, F-INFRA-4, F-AUTH-1, F-AUTH-2) block deploy. 13 cycle-2 findings total; cycle-1 closures confirmed for F-6, F-7, F-8, F-13, A09. Files: - security_report_cycle2.md (delta on cycle-1 report; FAIL verdict, tracker follow-ups filed as AZ-552..AZ-557 + 9 deferred Medium/Low) - owasp_review_cycle2.md (A01..A09 delta; 2 FAIL / 2 PASS_W_W / 5 PASS) - static_analysis_cycle2.md (F-AUTH-1..9 with locations + remediation) - infrastructure_review_cycle2.md (F-INFRA-1..6 with locations + remediation) - dependency_scan_cycle2.md (no new CVEs; cycle-1 deprecations re-flagged) Cycle-1 reports remain authoritative for non-cycle-2 surface. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
# Security Audit Report — Cycle 2 (Auth Modernization Delta)
|
||||
|
||||
**Date**: 2026-05-14
|
||||
**Scope**: cycle-2 surface only (AZ-531 through AZ-538 — refresh tokens, ES256/JWKS, mission tokens, MFA, Argon2id, lockout/rate-limit, CORS/HSTS/HTTPS). Cycle-1 findings and closures remain authoritative in `security_report.md`.
|
||||
**Verdict**: **FAIL — DO NOT DEPLOY.** 2 Critical deploy-blockers + 2 High auth findings discovered. Cycle-2 also closes 3 cycle-1 hardening gaps (F-7 weak hashing, F-8 no rate-limit, F-13 no HTTPS) and 1 cycle-1 PASS_WITH_WARNINGS (A09 audit trail).
|
||||
|
||||
## Summary
|
||||
|
||||
| Severity | Count | Closed in this audit |
|
||||
|----------|-------|----------------------|
|
||||
| Critical | 2 | 0 |
|
||||
| High | 4 | 0 (3 cycle-1 hardening gaps closed by cycle-2 code itself: F-7, F-8, F-13 — see "Cycle-1 closures" below) |
|
||||
| Medium | 7 | 0 |
|
||||
| Low | 2 | 0 |
|
||||
|
||||
## Findings (cycle-2 only, severity-ranked)
|
||||
|
||||
| # | Severity | Category | Location | Title |
|
||||
|---|----------|----------|----------|-------|
|
||||
| F-2026Q2-INFRA-1 | **Critical** | A05 | `scripts/start-services.sh:32` | Deploy script hard-blocks on obsolete `JwtConfig__Secret` — cycle-2 cannot deploy with current scripts |
|
||||
| F-2026Q2-INFRA-2 | **Critical** | A05 / A07 | `scripts/start-services.sh:48-56`, `appsettings.json:15` | ES256 keys folder not bind-mounted into container — `JwtSigningKeyProvider` fails-fast on startup |
|
||||
| F-2026Q2-AUTH-1 | **High** | A07 | `Azaion.Services/UserService.cs:120-148`, `BusinessException.cs:33-52` | User enumeration via login error codes (`NoEmailFound` vs `WrongPassword`); upgraded to High because lockout amplifies the threat |
|
||||
| F-2026Q2-AUTH-2 | **High** | A07 | `Azaion.Services/MfaService.cs:247-278`, `Azaion.Services/AuditLog.cs:53-63` | MFA brute-force not rate-limited — `MfaLoginFailed` events don't feed `failed_login_count` and aren't counted by `CountRecentFailedLogins` |
|
||||
| F-2026Q2-INFRA-3 | **High** | A05 | `Program.cs:147-160`, `scripts/start-services.sh` | DataProtection key store ephemeral by default — every container restart locks every MFA user out |
|
||||
| F-2026Q2-INFRA-4 | **High** | A05 | `secrets/README.md:50-55` | Operator README still lists HS256-era `JwtConfig__Secret`; misses `KeysFolder` / `ActiveKid` / `DataProtection__KeysFolder` |
|
||||
| F-2026Q2-AUTH-3 | Medium | A07 | `Azaion.Services/UserService.cs:144-152` | Disabled-account leak via auth ordering (`IsEnabled` checked AFTER password verify) |
|
||||
| F-2026Q2-AUTH-4 | Medium | A01 | `Azaion.AdminApi/Program.cs:452-481` | `/users/me/mfa/{enroll,confirm,disable}` not rate-limited; stolen access token can brute-force MFA disable |
|
||||
| F-2026Q2-AUTH-5 | Medium | A04 | `Azaion.Services/MissionTokenService.cs:103-111`, `Program.cs:489` | Mission tokens missing `amr` claim; verifiers can't enforce MFA-pilot policy |
|
||||
| F-2026Q2-AUTH-6 | Medium | A04 | `Azaion.Services/MissionTokenService.cs:46-87` | Mission token issuance does NOT auto-revoke prior aircraft missions; violates AZ-533 AC-4 |
|
||||
| F-2026Q2-AUTH-7 | Medium | A04 / A05 | `Azaion.Services/JwtSigningKeyProvider.cs:73-86` | Silent fallback to first PEM if `ActiveKid` unset; rotation accidents silently change signing key |
|
||||
| F-2026Q2-INFRA-5 | Medium | A05 | `Program.cs:217-225` | HSTS preload+includeSubDomains may break legacy `*.azaion.com` subdomains; preload registration is hard to undo |
|
||||
| F-2026Q2-AUTH-9 | Medium (deployment-coupled) | A07 | `Azaion.Services/UserService.GetByEmail` (cycle-1) consumed by cycle-2 lockout/MFA reads | 4-hour user cache holds lockout / MFA state stale across instances; safe at single-instance, breaks horizontal scaling |
|
||||
| F-2026Q2-AUTH-8 | Low | A09 | `Program.cs:261-280` | `/health/ready` leaks DB exception type to anonymous callers (mitigated by management-interface-only exposure) |
|
||||
| F-2026Q2-INFRA-6 | Low | A05 | `env/db/07_auth_lockout_and_audit.sql` | `audit_events` table has no documented retention policy |
|
||||
| F-2026Q2-DOCS-1 | Low | — | `_docs/02_document/components/03_auth_and_security/description.md`, `data_model.md` | Cycle-2 docs drift from code (Argon2 params, lockout field names, recovery-code hash algorithm) |
|
||||
|
||||
Detailed evidence and remediation steps are in `static_analysis_cycle2.md` (F-AUTH-*) and `infrastructure_review_cycle2.md` (F-INFRA-*).
|
||||
|
||||
## OWASP Top 10 (2021) — Cycle-2 Status
|
||||
|
||||
Full reasoning is in `owasp_review_cycle2.md`. Net category posture moves from cycle-1 **3 FAIL / 4 PASS_WITH_WARNINGS / 2 PASS / 1 N/A** to cycle-2 **2 FAIL / 2 PASS_WITH_WARNINGS / 5 PASS / 1 N/A**.
|
||||
|
||||
| Category | Cycle-1 | Cycle-2 | Reason |
|
||||
|----------|---------|---------|--------|
|
||||
| A01 Broken Access Control | FAIL | FAIL | F-2 still open + new F-AUTH-4 (MFA endpoints not rate-limited) |
|
||||
| A02 Cryptographic Failures | PASS_WITH_WARNINGS | **PASS** | F-7 closed by Argon2id (AZ-536); ES256, IDataProtector, SHA-256 (high-entropy) all RFC-aligned |
|
||||
| A04 Insecure Design | PASS_WITH_WARNINGS | PASS_WITH_WARNINGS | F-8 closed by hybrid rate-limit (AZ-537); new design risks F-AUTH-5/6/7 |
|
||||
| A05 Security Misconfiguration | FAIL | FAIL | F-13 closed by HSTS+HttpsRedirection; F-6 closed by `USER app`; new criticals F-INFRA-1/2/3 + medium F-INFRA-5 |
|
||||
| A07 Auth Failures | PASS_WITH_WARNINGS | PASS_WITH_WARNINGS | Cycle-2 modernized auth; new findings F-AUTH-1/2/3 |
|
||||
| A09 Logging Failures | PASS_WITH_WARNINGS | **PASS** | `IAuditLog` + `audit_events` table closes the cycle-1 warning |
|
||||
|
||||
## Cycle-1 Closures Confirmed in Cycle-2 Code
|
||||
|
||||
The cycle-2 implementation directly closes three cycle-1 open hardening items and one cycle-1 PASS_WITH_WARNINGS. These are NOT new audit work — they are verifications that the AZ-53x tickets did the security-relevant thing they promised.
|
||||
|
||||
| Cycle-1 finding | Cycle-2 resolution | Evidence |
|
||||
|-----------------|-------------------|----------|
|
||||
| **F-6** (container runs as root) | `USER app` directive added; `/app/Content` and `/app/logs` chowned | `Dockerfile:7-11, 39-40` |
|
||||
| **F-7** (SHA-384 password hash, no salt/KDF) | Argon2id with PHC string format, lazy migration on next login | `Azaion.Services/Security.cs:1-135`, AZ-536 spec |
|
||||
| **F-8** (no rate limiting on `/login`) | Per-IP sliding window (`RateLimiter`) + per-account DB-backed sliding window + lockout | `Program.cs:172-199, 308, 330`, `AuthConfig.cs`, `UserService.ValidateUser` |
|
||||
| **F-13** (no HTTPS enforcement in code) | `app.UseHsts()` + `app.UseHttpsRedirection()` in non-Development | `Program.cs:217-240` |
|
||||
| **A09** (no security-event audit log) | `IAuditLog` writes login_success/failed/lockout, MFA enroll/confirm/disable/login_success/failed/recovery_used to `audit_events` with email + IP + timestamp | `Azaion.Services/AuditLog.cs:1-80`, `env/db/07_auth_lockout_and_audit.sql` |
|
||||
|
||||
These closures can be verified by inspection during the next deploy gate.
|
||||
|
||||
## Verdict Logic
|
||||
|
||||
**FAIL — do not deploy** because:
|
||||
|
||||
1. **F-INFRA-1** (Critical): the deploy script's `require_env ASPNETCORE_JwtConfig__Secret` will fail-fast on every cycle-2 deploy attempt that follows the new `.env.example`. The deploy literally cannot start.
|
||||
2. **F-INFRA-2** (Critical): even if the operator works around F-INFRA-1, the container will then fail-fast inside `JwtSigningKeyProvider` because `secrets/jwt-keys` is not bind-mounted.
|
||||
3. **F-INFRA-3** (High): even if F-INFRA-1 and F-INFRA-2 are bypassed by `docker cp` and a dummy env var, the first container restart will lock every MFA-enrolled user out of their account because DataProtection master keys vanish.
|
||||
4. **F-AUTH-1 / F-AUTH-2** (Highs): the new auth surface is exploitable — user enumeration is amplified by lockout, and MFA can be brute-forced from rotating IPs without ever locking the account.
|
||||
|
||||
The combination of (1)+(2)+(3) means cycle-2 has **no working deploy path**. (4) means even if the deploy did work, the auth surface is below the cycle-1 baseline despite the modernization effort.
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Pre-deploy (must be done before the next deploy attempt)
|
||||
|
||||
The following CRITICAL and HIGH findings block deploy. Each is small enough to fit a single PR; bundle them as one cycle-2 hotfix sprint.
|
||||
|
||||
1. **F-INFRA-1** — `scripts/start-services.sh`: replace `require_env ... ASPNETCORE_JwtConfig__Secret` with `require_env ... ASPNETCORE_JwtConfig__KeysFolder ASPNETCORE_JwtConfig__ActiveKid`.
|
||||
2. **F-INFRA-2** — `scripts/start-services.sh`: add a `--volume "$DEPLOY_HOST_JWT_KEYS_DIR:/etc/azaion/jwt-keys:ro"` line; document `DEPLOY_HOST_JWT_KEYS_DIR` in `.env.example` and `secrets/README.md`.
|
||||
3. **F-INFRA-3** — (a) add a `--volume "$DEPLOY_HOST_DP_KEYS_DIR:/var/lib/azaion/dp-keys"` line; (b) set `ASPNETCORE_DataProtection__KeysFolder=/var/lib/azaion/dp-keys` in `secrets/<env>.public.env`; (c) fail-fast in `Program.cs:151-160` if Production and `KeysFolder` is unset.
|
||||
4. **F-INFRA-4** — rewrite `secrets/README.md` "Schema" section to drop `JwtConfig__Secret` and add `JwtConfig__KeysFolder`, `JwtConfig__ActiveKid`, `DataProtection__KeysFolder`.
|
||||
5. **F-AUTH-1** — `UserService.ValidateUser` + `BusinessException.cs`: introduce a single `InvalidCredentials` error code (e.g. 70). Map both `NoEmailFound`-equivalent and `WrongPassword`-equivalent paths to it. Move the `IsEnabled` check before the password verify (closes F-AUTH-3 too).
|
||||
6. **F-AUTH-2** — `MfaService.VerifyForLogin`: increment `failed_login_count` and check the per-account rate limit (call into `UserService` or duplicate the logic). Also: extend `AuditLog.CountRecentFailedLogins` to count `MfaLoginFailed` rows alongside `LoginFailed`.
|
||||
|
||||
### Short-term (Medium — file as separate tickets)
|
||||
|
||||
7. **F-AUTH-4** — attach `LoginPerIpPolicy` (or a dedicated `MfaPerIpPolicy`) to `/users/me/mfa/{enroll,confirm,disable}` in `Program.cs`.
|
||||
8. **F-AUTH-5** — `MissionTokenService.MintToken`: read the pilot's actual `amr` from the access token (or look it up via `Session.MfaAuthenticated`) and stamp it on the mission token. Reject if mission policy requires MFA and pilot has none.
|
||||
9. **F-AUTH-6** — `MissionTokenService.Issue`: call `SessionService.RevokeMissionsForAircraft(aircraftId)` immediately before inserting the new mission session row. Also fix the cycle-2 docs which falsely claim this auto-revoke is happening.
|
||||
10. **F-AUTH-7** — `JwtSigningKeyProvider`: fail-fast when `ActiveKid` is unset and >1 PEM exists; warn-only when exactly 1 PEM exists. Update `.env.example` to mark `ActiveKid` as required for prod.
|
||||
11. **F-INFRA-5** — audit all `*.azaion.com` subdomains for HTTPS-only; document the inventory in `_docs/04_deploy/`; gate `Preload = true` behind an env var so staging doesn't trigger preload-list submission attempts.
|
||||
12. **F-AUTH-9** — bypass the user cache for security-critical fields (`LockoutUntil`, `FailedLoginCount`, `MfaEnabled`, `MfaSecret`, `IsEnabled`) by reading them directly in `ValidateUser` and `MfaService`. Required before any horizontal scaling.
|
||||
|
||||
### Long-term (Low / hardening)
|
||||
|
||||
13. **F-AUTH-8** — `/health/ready`: log `ex.GetType().Name` internally; return only `{ status: "not-ready" }` externally.
|
||||
14. **F-INFRA-6** — agree audit retention (≥1 year per CMMC); add a nightly cleanup job; document in `_docs/04_deploy/`. Consider monthly partitioning if volume warrants.
|
||||
15. **F-DOCS-1** — sync cycle-2 docs to code: `AuthConfig` does not have `PasswordHashing`; `LockoutOptions.MaxAttempts` (not `ConsecutiveFailureThreshold`); `LockoutOptions.DurationSeconds` (not `LockoutSeconds`); `RateLimitOptions.PerAccountPermitLimit` (not `PerAccountFailedThreshold`); recovery codes are SHA-256 (not Argon2id).
|
||||
|
||||
## Dependency Vulnerabilities (cycle 2)
|
||||
|
||||
`dependency_scan_cycle2.md` is authoritative. Summary: no new CVEs in cycle-2 packages (`Konscious.Security.Cryptography.Argon2 1.3.1`, `Otp.NET 1.4.0`, `QRCoder 1.6.0`, ASP.NET Core `RateLimiting`, `DataProtection`). Two cycle-1 deprecation items are re-emphasized: `FluentValidation.AspNetCore 11.3.0` and `System.IdentityModel.Tokens.Jwt 7.1.2` should both move to maintained alternatives in a focused upgrade ticket.
|
||||
|
||||
## Tracker Follow-Ups
|
||||
|
||||
The 2 Critical + 4 High findings were filed as Jira tickets on 2026-05-14 as the cycle-2 hotfix sprint. Medium / Low items remain unfiled pending prioritization decision (see "Open" row below).
|
||||
|
||||
### Filed (cycle-2 hotfix sprint — 2026-05-14)
|
||||
|
||||
| Ticket | Finding | Title | Severity | Points |
|
||||
|--------|---------|-------|----------|--------|
|
||||
| [AZ-552](https://denyspopov.atlassian.net/browse/AZ-552) | F-INFRA-1 | Deploy script hard-blocks on obsolete `JwtConfig__Secret` | Critical | 1 |
|
||||
| [AZ-553](https://denyspopov.atlassian.net/browse/AZ-553) | F-INFRA-2 | Bind-mount ES256 key folder in deploy script + host-side procedure | Critical | 2 |
|
||||
| [AZ-554](https://denyspopov.atlassian.net/browse/AZ-554) | F-INFRA-3 | Persist DataProtection keys folder + fail-fast in Production | High | 2 |
|
||||
| [AZ-555](https://denyspopov.atlassian.net/browse/AZ-555) | F-INFRA-4 | Rewrite `secrets/README.md` schema for ES256 + DataProtection | High | 1 |
|
||||
| [AZ-556](https://denyspopov.atlassian.net/browse/AZ-556) | F-AUTH-1 + F-AUTH-3 | Collapse login error codes to `InvalidCredentials` + reorder IsEnabled check | High | 2 |
|
||||
| [AZ-557](https://denyspopov.atlassian.net/browse/AZ-557) | F-AUTH-2 | Lock MFA brute-force into per-account lockout/rate-limit pipeline | High | 3 |
|
||||
|
||||
Bundle: 11 story points. Labels: `security`, `cycle-2-hotfix`, `AZ-530-followup`. All gate the next deploy.
|
||||
|
||||
### Open (Medium / Low — to be triaged)
|
||||
|
||||
| Ticket suggestion | Finding | Title | Severity | Points |
|
||||
|-------------------|---------|-------|----------|--------|
|
||||
| AZ-NEW-7 | F-AUTH-4 | Add per-IP rate limiting to `/users/me/mfa/*` endpoints | Medium | 2 |
|
||||
| AZ-NEW-8 | F-AUTH-5 | Stamp `amr` on mission tokens; gate by mission policy | Medium | 2 |
|
||||
| AZ-NEW-9 | F-AUTH-6 | Auto-revoke prior aircraft mission on issuance (and fix the docs) | Medium | 2 |
|
||||
| AZ-NEW-10 | F-AUTH-7 | Fail-fast on ambiguous `ActiveKid`; tighten `.env.example` | Medium | 1 |
|
||||
| AZ-NEW-11 | F-INFRA-5 | Audit `*.azaion.com` subdomains for HTTPS-only before preload | Medium | 2 |
|
||||
| AZ-NEW-12 | F-AUTH-9 | Bypass user-cache for security-critical fields (pre-scaling) | Medium | 3 |
|
||||
| AZ-NEW-13 | F-AUTH-8 | `/health/ready` body redaction | Low | 1 |
|
||||
| AZ-NEW-14 | F-INFRA-6 | Audit retention policy + cleanup job | Low | 2 |
|
||||
| AZ-NEW-15 | F-DOCS-1 | Sync cycle-2 auth/security docs to code | Low | 1 |
|
||||
|
||||
## Self-Verification
|
||||
|
||||
- [x] Every Critical / High in this report has location + remediation
|
||||
- [x] OWASP delta is reconciled against `owasp_review_cycle2.md`
|
||||
- [x] Cycle-1 closures verified by re-reading the cycle-2 code (F-6, F-7, F-8, F-13, A09)
|
||||
- [x] Verdict matches finding severity (FAIL because of 2 deploy-blocking Criticals, not a clerical "PASS_WITH_WARNINGS")
|
||||
- [x] Tracker follow-ups grouped by severity for prioritization
|
||||
Reference in New Issue
Block a user