Environment topology — LastVet AWS (single account, hard walls)¶
Superseded for R940 (August 2026). Operational hard walls, deploy sequence, and prod confirmation gate live in
lastvet-gates/docs/deployment/R940_ENVIRONMENT_TOPOLOGY.mdandlastvet-gates/docs/runbooks/r940-prod-deploy.md. This document is retained for AWS-era archaeology only.
Status: Approved plan 2026-07-23. Superseded on R940 2026-08-18. P1–P2 pre–VA-reply; P3 at VA approval; P4 deferred.
AWS account: 525382240840 (us-west-2), BAA executed.
Decision: One account with structural hard walls — not separate prod/non-prod accounts at current scale.
Why before VA production access¶
Prod today holds one synthetic veteran (Sheba703). After case #00015000 approval, prod holds real PHI and reconfiguring the permanent sandbox lane becomes a migration, not a configuration change. Same reasoning as moving off Railway while the DB was nearly empty.
Second reason: Staging had no seed data. The provider RLS fix (2026-07-21) was verified on production because prod was the only populated environment. Staging must become the permanent security-boundary test lane.
Logical environments (two deployed lanes + local)¶
| Lane | API | VA Lighthouse | Data | Purpose |
|---|---|---|---|---|
| Staging (permanent non-prod) | api-staging.last.vet |
Sandbox only, forever | Synthetic fixtures | Dev, API matrix experiments, all security-boundary proof |
| Production | api.last.vet |
Production only (after P3) | Real veterans | App Store / pilot |
| Local | optional | Sandbox | Staging RDS or docker | Engineer workspace — not a deployed env |
Not a fourth deployed env: veteran web client (parked until VA approval) rides staging for build/test; prod web track waits on P3.
Meridian M0: local Ollama + synthetic fixtures only (last1-meridian private repo). No Meridian container on staging/prod API until PHI hosting gate + counsel sign-off. Bedrock is prod-VPC future path only.
Account split trigger (P4 — deferred)¶
Split prod into a second AWS account. Must complete before the first real veteran. Triggered by whichever comes first:
- First engineer hire (month four in the operating plan), or
- Counsel explicitly requires account isolation.
Do not wait until the first real veteran is onboarded — that is when migration is most expensive.
Until P4: VPC isolation + IAM-scoped Secrets Manager + operator guards (below) provide the hard walls.
Hard walls¶
Network and data¶
- Separate VPCs: staging
10.10.0.0/16, prod10.20.0.0/16 - Separate RDS, ECS, EFS, Secrets Manager namespaces:
lastvet-staging/*vslastvet-prod/* - Never seed prod. Fixture scripts refuse prod connection strings.
VA OAuth credentials (P1)¶
Lighthouse issues separate sandbox and production OAuth clients. Using prod credentials against sandbox (or sandbox against api.va.gov) is a policy violation.
| Credential | Staging / Debug iOS | App Store iOS | Storage |
|---|---|---|---|
| Sandbox OAuth client IDs | ✅ | ❌ (sandbox VA env only) | Secrets.xcconfig + build settings |
| Production OAuth client IDs | ❌ | ✅ (after P3) | Bitwarden → prod-only xcconfig slot |
Enforcement:
- iOS build script
scripts/verify-va-oauth-config.sh— fails Release build ifLASTVET_VA_ENVIRONMENT=productionwhile prod IDs are empty or match sandbox. - Runtime
VAOAuthGuard.validateCurrentConfiguration()— blocks VA auth flows if production host + sandbox IDs.
Prod IDs live in Bitwarden keys VA_OAUTH_PROD_* (empty until VA issues apps). Sandbox IDs are public in repo build settings.
Operator guards (P2)¶
load-aws-env.sh staging|prodsetsLASTVET_AWS_ENV_GUARDdeploy-api-image.shverifies guard matches target env- Prod deploy requires
LASTVET_ALLOW_PROD_DEPLOY=1(intentional confirmation) - Project boundary:
lastvet-project-boundary.shrejects non-LastVet hosts (e.g.angel.report) in deploy/build env vars. Provider portal builds must use hardcodedapi.last.vet/api-staging.last.vetonly. - Security-boundary changes: verify on staging only after
./infra/scripts/run-ecs-seed-staging-fixtures.sh
ECS secrets wiring (P2)¶
Terraform app_secret_keys includes VETERAN_JWT_SECRET and SECURE_LINK_HMAC_SECRET (previously unwired). The compute module is shared — the next terraform apply in either environment registers a task definition that references both keys.
Apply order (mandatory):
- Add
VETERAN_JWT_SECRETandSECURE_LINK_HMAC_SECRETto bothlastvet-staging/appandlastvet-prod/appin AWS Secrets Manager (not staging-first). - Use distinct values (
openssl rand -base64 32each). Do not copyOAUTH_JWT_SECRETorLAST1_TOKEN_ENCRYPTION_KEY— key reuse across domains is the defect being fixed. Invalidation cost: ~60 min veteran JWTs, ~10 min pending OTPs on synthetic data only.
source infra/scripts/load-aws-env.sh staging
./infra/scripts/merge-app-secret-keys.sh staging # generates + merges; refuses overwrite
source infra/scripts/load-aws-env.sh prod
./infra/scripts/merge-app-secret-keys.sh prod
# Then update Bitwarden lastvet project to match AWS (AWS is authoritative for ECS).
# sync-bitwarden-secrets.sh api — local dev only, never push Bitwarden → AWS blind.
- Only then run
terraform apply(staging or prod).
Missing JSON keys → ECS task placement fails (ResourceInitializationError). Empty string keys satisfy Terraform but are a footgun: VETERAN_JWT_SECRET="" disables the ?? fallback in environment.ts (service layer still falls through via ||).
Prod apply today (before populate): Running prod tasks stay on revision :7 because the ECS service has lifecycle { ignore_changes = [task_definition] }. A apply creates revision :8 that cannot start until keys exist. deploy-api-image.sh force-new-deployment reuses the service's pinned revision — safe. Unsafe: switching the prod service to the new task definition before both keys exist in SM.
Staging fixture suite (P0)¶
Run: ./infra/scripts/run-ecs-seed-staging-fixtures.sh staging
Source: scripts/seed-staging-fixtures.js, UUIDs in scripts/staging-fixtures.js
| Fixture | Purpose |
|---|---|
| Veteran A | Merged dual-grant grant; signed ROI PDF + authorizations; both Part 2 paths — revoked signed_roi row + active sheet row (HL issue #2 defect state for promotion verify) |
| Veteran B | Single grant, revoked |
| Veteran C | Two providers with active grants (RLS cross-veteran denial) |
| Providers 1 & 2 | staging-dual-grant@last.vet, staging-provider-two@last.vet |
Bar: Any consent, RLS, revoke, or Part 2 enforcement change is fully verified on staging — never prod once real PHI exists.
Phase order¶
| Phase | When | Work |
|---|---|---|
| P1 | Pre–VA-reply ✅ | VA OAuth build-time + runtime guards |
| P0 | Pre–VA-reply ✅ | Staging fixture suite |
| P2 | Pre–VA-reply ✅ | Terraform secret wiring, deploy guards, runbook |
| P3 | VA approval | Flip App Store to VAEnvironment.production + prod OAuth IDs; prod never seeded |
| P4 | Engineer hire or counsel | Optional second AWS account split |
Deploy sequence (three logical steps)¶
1. STAGING (always first)
source infra/scripts/load-aws-env.sh staging
./infra/scripts/deploy-api-image.sh staging --push-only
./infra/scripts/run-ecs-db-setup.sh staging
./infra/scripts/run-ecs-seed-staging-fixtures.sh staging
./infra/scripts/run-ecs-verify-*.sh staging # live API paths only — mirror/DB checks do not count
./infra/scripts/deploy-api-image.sh staging --rollout-only
(provider) npm run cf:deploy:dev
document_share (Gate 1): run-ecs-verify-document-share-guards.sh must POST to
/veteran/me/secure-links and get 400 document_share_requires_service_records for ROI.
Until deploy + that live path passes, allowlist is NOT verified — even if migrations 057/058 applied.
2. PRODUCTION (after staging green + explicit approval)
export LASTVET_ALLOW_PROD_DEPLOY=1
source infra/scripts/load-aws-env.sh prod
./infra/scripts/deploy-api-image.sh prod --push-only
./infra/scripts/run-ecs-db-setup.sh prod --expect <migrations>
./infra/scripts/deploy-api-image.sh prod --rollout-only
(provider) cf:deploy prod
(iOS) App Store with VA production credentials (P3)
3. NEVER
- Seed scripts on prod
- Sandbox VA credentials in App Store release
- Security-boundary verification on prod after real PHI
Related docs¶
docs/deployment/DEV_STAGING.md— day-to-day opsdocs/security/SECURITY_POSTURE_TOKEN_CUSTODY.md— veteran web parked until VA approvaldocs/runbooks/VA_PRODUCTION_CUTOVER_RUNBOOK.md— P3 VA + iOS cutoverdocs/compliance/VA_PRODUCTION_ACCESS_TRACKER.md— case #00015000
Revision history¶
| Date | Change |
|---|---|
| 2026-07-23 | Plan approved: single account + hard walls; P1→P0→P2 order; split trigger = engineer hire or counsel, not first veteran |
| 2026-07-23 | document_share: migrations 057/058 applied on staging (injection workaround used initially); live allowlist verified after image deploy + task def :4 + run-ecs-verify-document-share-guards.sh |