Security & pentest remediation status¶
Last updated: 2026-07-15 (A1 human TestFlight confirm on AWS)
Scope: LastVet API (api.last.vet) + iOS veteran client + provider secure-link landing
Environment note: Production is AWS ECS + RDS (us-west-2), DNS-only at api.last.vet, running against VA sandbox data until VA production Lighthouse access is granted.
Summary¶
| Area | Staging | Production | Notes |
|---|---|---|---|
| Bearer JWT veteran auth | ✅ | ✅ | POST /api/auth/veteran/session + Authorization: Bearer |
Legacy x-user-id header auth |
✅ Off | ✅ Off | ALLOW_LEGACY_HEADER_AUTH=false |
| Session exchange secret configured | ✅ | ✅ | Bitwarden SM VETERAN_SESSION_EXCHANGE_SECRET |
| Session mismatch / IDOR guards | ✅ | ✅ | 403 on veteran session mismatch |
| Malformed JSON → 400 | ✅ | ✅ | invalid_json |
| Provider portal CORS | ✅ | ✅ | dev.provider.last.vet / provider.last.vet |
| HTML escaping (OAuth consent, admin email) | ✅ | ✅ | Deployed main |
| Secure-link iframe sandbox + HTML blob guard | ✅ | ✅ | Provider portal |
| Signed ROI AES-256-GCM at rest | ✅ | ✅ | roiArtifactStorage |
Health record RLS (042) |
✅ | ✅ | Prod migrate applied 2026-07-09 |
PHI raw field encryption (043) |
✅ | ✅ | raw_ciphertext via LAST1_TOKEN_ENCRYPTION_KEY |
| Provider login enumeration + IP rate limit | ✅ | ✅ | Unknown email → 401; 30/15min IP limit (PR #30, prod verified 2026-07-11) |
| iOS Bearer JWT client | ✅ | ✅ | TestFlight device pass Gate 0.5 B6 (E2E_RUN_LOG_B6_TESTFLIGHT.md) |
Postgres RLS batch 1 (046) |
✅ | ✅ | lastvet_app + DATABASE_APP_URL on staging + prod (2026-07-11); batch 2 tables deferred |
| SMS OTP for secure links | ⏸️ | ⏸️ | Deferred — email OTP path (docs/planning/gate-0.5/DECISIONS.md) |
| 42 CFR Part 2 counsel-locked copy | 🟡 | 🟡 | Parameterized; HL lock pending (Gate 0.5 D10) |
Veteran auth model (current)¶
- iOS exchanges VA
patientid +x-veteran-session-exchange→ short-lived Bearer JWT. - All veteran API routes use
Authorization: Bearer. - Legacy header bootstrap disabled in staging and production.
Secure access (ROI delivery)¶
- Opaque token URLs only — no PHI in link, QR, SMS, or email body.
- Designated-recipient email OTP before artifact view.
- Encrypted signed-ROI PDF at rest; single-use view session for download.
- Revoke from veteran Consent tab disables link immediately.
Prod database migrate policy (B7)¶
- Prod never auto-migrates — guard is
APP_ENV=productioninscripts/aws-start.sh(ported from Railway'sRAILWAY_ENVIRONMENT_NAMEcheck). scripts/migrate.json prod runs only via explicit one-off — e.g.infra/scripts/run-ecs-db-setup.sh(ECS task command override).ensureCoreSchema()runs inline DDL on every boot viasrc/index.ts→migrationDb; it does not readmigrations/*.sql. Skipped on prod whenAPP_ENV=production(2026-07-16).- Idempotent
Skipping … (already applied)lines appear only whenmigrate.jsis invoked; they are not prod service-boot behavior. - Database access posture (RLS, owner pool, consent vs clinical split):
docs/security/SECURITY_POSTURE_DATABASE_ACCESS.md— expands this note; B7 remains closed. - Migration pre-flight:
migrate.js --list-pending+--expecton prod viarun-ecs-db-setup.sh(2026-07-16).
Gate 0.5 cross-reference¶
| Gate item | Status |
|---|---|
| A1 E2E proof on AWS | ✅ Scripted + human TestFlight 2026-07-15 (E2E_RUN_LOG_2026-07-15.md) |
| A2 Bearer harness + CloudWatch OTP | ✅ e2e-roi-gate0-full.sh pass 2026-07-15 |
| A3 Facilities | ✅ 11 SD facilities, 2026-07-15 |
| B5 042/043 prod on RDS | ✅ Cutover 001→048 |
| B6 TestFlight | ✅ Device pass logged; confirm AWS API URLs |
| B7 This doc | ✅ |
| C8 VA API matrix removed from onboarding | ✅ 2026-07-11 |
| C9 SMS defer | ✅ |
Full checklist: docs/runbooks/gate-0.5/GATE_0.5_STATUS.md
Open / follow-up¶
- [x] A1 — Human iOS full-loop E2E (B6 TestFlight log)
- [x] A2 —
e2e-roi-gate0.shBearer JWT harness - [x] B6 — TestFlight device smoke
- [x] 046 prod —
lastvet_appcutover +046–048on prod (docs/api/TRACK3_PHI_RLS_PLAN.md) - [ ] RLS batch 2 —
docs/api/RLS_BATCH2_PLAN.md(consent first, then credentials/tokens) - [ ] D10 — Hogan Lovells Part 2 language lock before any real veteran pilot
- [ ] Shannon retest — Phase 2 closure (
last1-security/docs/PENTEST-PHASE2-STATUS.mdTrack 5)
Verification commands¶
# Session exchange (expect 200 or 404 veteran_not_found — not 503)
curl -s -o /dev/null -w "%{http_code}" -X POST https://api.last.vet/api/auth/veteran/session \
-H "Content-Type: application/json" \
-H "x-veteran-session-exchange: $VETERAN_SESSION_EXCHANGE_SECRET" \
-d '{"vaPatientId":"test"}'
# Legacy headers blocked (expect 401)
curl -s -o /dev/null -w "%{http_code}" https://api.last.vet/api/veteran/me \
-H "x-user-id: test" -H "x-user-type: veteran" -H "x-veteran-id: test"
# Provider login enumeration (expect 401, not 404)
curl -s -X POST https://api.last.vet/api/provider/login \
-H "Content-Type: application/json" \
-d '{"email":"nonexistent@example.com","password":"wrong"}'