Files
admin/_docs/05_security/security_report_cycle2.md
T
Oleksandr Bezdieniezhnykh 1bdbe8c96d [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>
2026-05-14 09:23:02 +03:00

14 KiB

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-1scripts/start-services.sh: replace require_env ... ASPNETCORE_JwtConfig__Secret with require_env ... ASPNETCORE_JwtConfig__KeysFolder ASPNETCORE_JwtConfig__ActiveKid.
  2. F-INFRA-2scripts/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-1UserService.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-2MfaService.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)

  1. F-AUTH-4 — attach LoginPerIpPolicy (or a dedicated MfaPerIpPolicy) to /users/me/mfa/{enroll,confirm,disable} in Program.cs.
  2. F-AUTH-5MissionTokenService.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.
  3. F-AUTH-6MissionTokenService.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.
  4. F-AUTH-7JwtSigningKeyProvider: 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.
  5. 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.
  6. 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)

  1. F-AUTH-8/health/ready: log ex.GetType().Name internally; return only { status: "not-ready" } externally.
  2. 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.
  3. 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 F-INFRA-1 Deploy script hard-blocks on obsolete JwtConfig__Secret Critical 1
AZ-553 F-INFRA-2 Bind-mount ES256 key folder in deploy script + host-side procedure Critical 2
AZ-554 F-INFRA-3 Persist DataProtection keys folder + fail-fast in Production High 2
AZ-555 F-INFRA-4 Rewrite secrets/README.md schema for ES256 + DataProtection High 1
AZ-556 F-AUTH-1 + F-AUTH-3 Collapse login error codes to InvalidCredentials + reorder IsEnabled check High 2
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

  • Every Critical / High in this report has location + remediation
  • OWASP delta is reconciled against owasp_review_cycle2.md
  • Cycle-1 closures verified by re-reading the cycle-2 code (F-6, F-7, F-8, F-13, A09)
  • Verdict matches finding severity (FAIL because of 2 deploy-blocking Criticals, not a clerical "PASS_WITH_WARNINGS")
  • Tracker follow-ups grouped by severity for prioritization