Skip to content

LastVet — Bitwarden Hygiene Plan (R940 Migration)

Date: 2026-08-10
Companion: BITWARDEN_ENV_AUDIT.md
Status: Proposal only. Do not execute renames until Ryan approves this plan.


Executive summary

Item Count
Live BWS secrets in lastvet project 32
Hygiene issues flagged 28
Secrets recommended rename 12
Secrets recommended keep as-is (after note fix) 8
Secrets recommended delete (after grace period) 4
New secrets to create before R940 deploy ~25

Issue breakdown:

Category Count Examples
Legacy pointer (Railway hosts) 6 DATABASE_APP_URL_STAGING, MIGRATION_DATABASE_URL_PRODUCTION
AWS-specific / ambiguous target 8 LASTVET_APP_PASSWORD_PRODUCTION, undifferentiated crypto keys
Ambiguous scope (env not in name) 6 VETERAN_JWT_SECRET, LAST1_TOKEN_ENCRYPTION_KEY
Orphaned (no API code path) 5 VA_FORMS_API_KEY, E2E_OTP_IMAP_*
Duplicated value 2 App password stored in URL + LASTVET_APP_PASSWORD_*
Missing from BWS (AWS SM only) ~30 keys SendGrid, Stripe, Google OAuth, portal URLs
Stale notes 1 OAUTH_JWT_SECRET veteran-fallback note

Goals

  1. Parallel run: AWS ECS and R940 VM can hold different connection strings and credentials without name collisions.
  2. Greppable: bws secret list | grep R940 returns every R940-scoped operator secret.
  3. Runtime clarity: Deployment scripts map BWS storage names → canonical process.env names (which never change in application code during migration).
  4. No blind sync: AWS SM remains authoritative for ECS until R940 cutover; Bitwarden is never pushed into AWS without verification.

Naming convention

Pattern

LASTVET_{TARGET}_{ENV}_{PURPOSE}
Segment Values Meaning
LASTVET Fixed prefix Product scope (contrast with LAST1_ for cross-product platform secrets)
{TARGET} AWS, R940, RAILWAY, EDGE, IOS, SHARED Where the value is consumed
{ENV} STAGING, PROD, omit for global Logical environment lane
{PURPOSE} Slug Describes role; for API secrets, suffix with runtime var name when helpful

Cross-product secrets

Scope Pattern Example
LastVet-only LASTVET_{TARGET}_{ENV}_… LASTVET_R940_STAGING_VETERAN_JWT_SECRET
Shared Last1 platform LAST1_{PURPOSE} or live in owning project LAST1_ONSET_IDENTITY_ASSERTION_SECRET
Non-secret config LASTVET_{TARGET}_{ENV}_… or plain env in systemd LASTVET_R940_STAGING_OAUTH_ISSUER

Rule: If a secret is shared with another repo (iOS, Onset, last1.id), the BWS note must name the peer and the canonical writer. One writer, many readers.

Runtime mapping (deploy time)

The API always reads DATABASE_APP_URL, not DATABASE_APP_URL_R940_STAGING. A systemd drop-in or restore-api-env-from-bws.sh performs:

# Example — not implemented yet
export DATABASE_APP_URL="$(bws secret get LASTVET_R940_STAGING_DATABASE_APP_URL)"
export MIGRATION_DATABASE_URL="$(compose from LASTVET_R940_STAGING_POSTGRES_MASTER_PASSWORD ...)"

Store both AWS and R940 URLs in BWS during parallel run; the deploy target selects which pair to inject.

grep cheatsheet

bws secret list | jq -r '.[].key' | grep LASTVET_R940      # all R940
bws secret list | jq -r '.[].key' | grep LASTVET_AWS        # all AWS operator copies
bws secret list | jq -r '.[].key' | grep LASTVET_RAILWAY   # rollback net only
bws secret list | jq -r '.[].key' | grep LASTVET_EDGE       # cloudflared
bws secret list | jq -r '.[].key' | grep LASTVET_IOS        # iOS-bound

Migration table — all 32 live BWS secrets

Current BWS key Issue Proposed BWS key Action Rationale
DATABASE_APP_URL_STAGING Legacy Railway host LASTVET_RAILWAY_STAGING_DATABASE_APP_URL Rename Target + env explicit; Railway rollback only
DATABASE_APP_URL_PRODUCTION Legacy Railway host LASTVET_RAILWAY_PROD_DATABASE_APP_URL Rename Same
MIGRATION_DATABASE_URL_STAGING Legacy Railway host LASTVET_RAILWAY_STAGING_MIGRATION_DATABASE_URL Rename Same
MIGRATION_DATABASE_URL_PRODUCTION Legacy Railway host LASTVET_RAILWAY_PROD_MIGRATION_DATABASE_URL Rename Same
LASTVET_APP_PASSWORD_STAGING Ambiguous; duplicates URL LASTVET_RAILWAY_STAGING_POSTGRES_APP_PASSWORD Rename Matches R940 password naming
LASTVET_APP_PASSWORD_PRODUCTION Ambiguous; duplicates URL LASTVET_RAILWAY_PROD_POSTGRES_APP_PASSWORD Rename Same
DATABASE_APP_URL_R940_STAGING OK intent; non-standard prefix LASTVET_R940_STAGING_DATABASE_APP_URL Rename Consistent LASTVET_R940_* grep
DATABASE_APP_URL_R940_PROD OK intent LASTVET_R940_PROD_DATABASE_APP_URL Rename Same
LASTVET_R940_POSTGRES_APP_PASSWORD OK Keep (already prefixed) Keep Add note linking to app URL
LASTVET_R940_POSTGRES_MASTER_PASSWORD Missing migration URL companion Keep + create LASTVET_R940_STAGING_MIGRATION_DATABASE_URL and …_PROD_… Create Week 2 migrate script needs owner URL
LASTVET_R940_MINIO_ROOT_USER Infra-only Keep Keep Not API env until MinIO wired
LASTVET_R940_MINIO_ROOT_PASSWORD Infra-only Keep Keep Same
LASTVET_R940_ZFS_IMAGING_KEY Infra-only Keep Keep ZFS recovery doc references this
LASTVET_R940_CLOUDFLARED_TUNNEL_TOKEN Edge, not API LASTVET_EDGE_CLOUDFLARED_TUNNEL_TOKEN Rename optional EDGE clearer than R940 for tunnel
LASTVET_R940_CLOUDFLARED_ORIGIN_CERT Edge LASTVET_EDGE_CLOUDFLARED_ORIGIN_CERT Rename optional Same
VETERAN_JWT_SECRET Ambiguous env; note says prod only LASTVET_AWS_PROD_VETERAN_JWT_SECRET + create …_AWS_STAGING_… + …_R940_* Split + rename Distinct per lane; Ryan decides rotation
SECURE_LINK_HMAC_SECRET Ambiguous env LASTVET_AWS_PROD_SECURE_LINK_HMAC_SECRET + staging + R940 copies Split + rename Same
OAUTH_JWT_SECRET Stale note (veteran fallback) LASTVET_AWS_STAGING_OAUTH_JWT_SECRET + prod + R940 Split + rename + fix note Onset OAuth only
PROVIDER_JWT_SECRET Ambiguous env LASTVET_AWS_STAGING_PROVIDER_JWT_SECRET + prod + R940 Split + rename Per-env distinct
LAST1_TOKEN_ENCRYPTION_KEY Ambiguous scope Ryan decision → either LASTVET_* per env or LAST1_TOKEN_ENCRYPTION_KEY in Last1 project Rename or move See decisions below
LAST1_WEBHOOK_SECRET Ambiguous env LASTVET_AWS_STAGING_LAST1_WEBHOOK_SECRET + prod + R940 Split + rename
LAST1_PROOF_DRAINER_SECRET Ambiguous env LASTVET_AWS_STAGING_LAST1_PROOF_DRAINER_SECRET + prod + R940 Split + rename
LASTVET_INTERNAL_ADMIN_SECRET Ambiguous env LASTVET_AWS_STAGING_INTERNAL_ADMIN_SECRET + prod + R940 Split + rename
LASTVET_APP_SECRET Shared with last1.id LASTVET_AWS_STAGING_APP_SECRET + prod (+ same value on last1.id) Split + rename last1.id is peer writer for app registration
ONSET_IDENTITY_ASSERTION_SECRET Cross-project LAST1_ONSET_IDENTITY_ASSERTION_SECRET in Last1 or Onset project Move Single canonical home
VETERAN_SESSION_EXCHANGE_SECRET iOS + API shared LASTVET_IOS_SHARED_VETERAN_SESSION_EXCHANGE_SECRET Rename iOS xcconfig + API inject same value
VA_OPEN_DATA_API_KEY iOS only LASTVET_IOS_SHARED_VA_OPEN_DATA_API_KEY Rename Not API
VA_FORMS_API_KEY Orphan / parked Keep or delete after iOS wiring Delete candidate Not consumed anywhere yet
E2E_OTP_IMAP_HOST Test harness LASTVET_TEST_E2E_OTP_IMAP_HOST Rename Move to test project long-term
E2E_OTP_IMAP_USER Test harness LASTVET_TEST_E2E_OTP_IMAP_USER Rename Same
E2E_OTP_IMAP_PASSWORD Test harness LASTVET_TEST_E2E_OTP_IMAP_PASSWORD Rename Same
CLOUDFLARE_ACCOUNT_ID Infra LAST1_SHARED_CLOUDFLARE_ACCOUNT_ID Move optional Shared across Last1 web projects

New secrets to create (not in BWS today)

Populate from AWS SM (lastvet-staging/app, lastvet-prod/app) as operator backups before R940 Week 2 deploy. Use LASTVET_AWS_{ENV}_{RUNTIME_VAR} naming.

Priority batch (required for functional R940 staging API):

  • LASTVET_AWS_STAGING_* and LASTVET_R940_STAGING_* for all Terraform app_secret_keys (40 keys)
  • LASTVET_R940_STAGING_MIGRATION_DATABASE_URL (compose from master password + host)
  • LASTVET_R940_PROD_MIGRATION_DATABASE_URL
  • Plain config: LASTVET_R940_STAGING_OAUTH_ISSUER, …_PROVIDER_PORTAL_URL, …_NODE_ENV, …_UPLOAD_DIR, …_DATABASE_SSL

Second batch (feature parity):

  • SendGrid block (5), Stripe block (6), Google OAuth (3), APNS (3)

Delete vs rename vs keep

Action Secrets
Keep as-is (short term) LASTVET_R940_POSTGRES_*, MinIO, ZFS imaging key until infra scripts reference new names
Rename first All Railway DATABASE_*, ambiguous crypto keys (create new name, copy value, deprecate old)
Delete only after Old names once (1) R940 deploy script uses new names, (2) AWS parallel run documented, (3) 30-day grace
Delete candidates VA_FORMS_API_KEY if iOS still unused at Gate 1; Railway URLs once rollback net torn down
Never delete Railway URLs until Railway teardown per invariants

Execution order (operations)

Phase 0 — This PR (done when merged)
Document only. No BWS writes.

Phase 1 — Create before rename (no deletes)
1. Export AWS SM lastvet-staging/app and lastvet-prod/app to local secure store.
2. Create all LASTVET_AWS_{STAGING,PROD}_{KEY} entries in BWS from SM (operator backup).
3. Create LASTVET_R940_STAGING_MIGRATION_DATABASE_URL / …_PROD_….
4. Create LASTVET_R940_{STAGING,PROD}_* copies for crypto keys (Ryan decides: rotate vs copy AWS staging).

Phase 2 — Rename with duplication
5. For each row in migration table: create proposed name with same value.
6. Update bitwarden/secrets-manifest.json and restore-*-from-bws.sh scripts to read new names.
7. Add deprecated note on old keys: DEPRECATED → use LASTVET_RAILWAY_STAGING_….

Phase 3 — R940 deploy wiring (Week 2 Task 1)
8. Implement restore-api-env-from-bws.sh on lastvet-api VM mapping LASTVET_R940_STAGING_*/etc/lastvet/api.env.
9. Smoke test: /health, DB connect, provider JWT, veteran exchange (staging).
10. Parallel run: AWS ECS unchanged; cloudflared tunnel to R940 for soak only.

Phase 4 — Cleanup (post-cutover)
11. Verify no script references old BWS names (rg DEPRECATED keys).
12. Delete deprecated Railway keys after Railway teardown.
13. Delete duplicate AWS backup keys if SM + R940 are authoritative.
14. Fix OAUTH_JWT_SECRET stale notes everywhere.

Never: Rename/delete a BWS key that is the sole copy of a production credential without verifying AWS SM or R940 runtime first.


Top 3 decisions for Ryan (block Week 2 Task 1)

1. R940 staging crypto: rotate or copy AWS staging?

Creating LASTVET_R940_STAGING_VETERAN_JWT_SECRET (etc.) requires choosing:

  • Copy AWS staging values: iOS + provider portal can hit either AWS or R940 staging interchangeably during soak.
  • Rotate new values: Stronger isolation; iOS xcconfig and last1.id callbacks must be updated for R940-only testing.

2. Canonical home for cross-product secrets

Secret Options
LAST1_TOKEN_ENCRYPTION_KEY (A) Per-env under LASTVET_* — implies LastVet-owned keys. (B) LAST1_* in a shared Last1 BWS project — implies platform key.
ONSET_IDENTITY_ASSERTION_SECRET (A) Keep in lastvet. (B) Move to onset project; lastvet reads via sync. (C) LAST1_* shared.

Until decided, do not rename these two.

3. R940 imaging env strategy

Code today expects IMAGING_S3_BUCKET + IMAGING_KMS_KEY_ARN (AWS). BWS already has MinIO + ZFS envelope material. Pick one before Week 2:

  • A) Week 2 deploy without imaging routes (cap disabled).
  • B) Add MinIO env vars + code adapter (scope creep — needs explicit gate approval).
  • C) Point R940 staging at AWS S3 temporarily (violates Case B intent).

Manifest reconciliation

bitwarden/secrets-manifest.json is stale relative to live BWS:

Manifest Live BWS
Lists SendGrid, Stripe, Railway DB keys Missing SendGrid, Stripe, Google
Missing all R940 keys Has R940 DB, MinIO, cloudflared, ZFS
Missing SECURE_LINK_HMAC_SECRET, VA_FORMS_API_KEY Present in BWS

Follow-up task (separate PR): Regenerate manifest from this plan after Phase 2 renames.


Sub-task B — Issue detail (cross-reference)

# Secret(s) Category Detail
1–4 DATABASE_APP_URL_{STAGING,PRODUCTION}, MIGRATION_* Legacy pointer *.railway.internal hosts
5–6 LASTVET_APP_PASSWORD_* Legacy + ambiguous Railway-era naming; no RAILWAY in key
7–8 DATABASE_APP_URL_R940_* Ambiguous scope Good values; inconsistent prefix order
9–14 JWT/HMAC/crypto keys (single undifferentiated copy) Ambiguous scope BWS holds one value; AWS has per-env SM
15–16 LASTVET_APP_PASSWORD_* + Railway URLs Duplicated Password embedded in URL string
17 OAUTH_JWT_SECRET note Stale metadata Claims veteran JWT fallback (removed)
18–20 VA_FORMS_API_KEY, VA_OPEN_DATA_API_KEY Orphan (API) iOS / parked
21–23 E2E_OTP_IMAP_* Orphan (API) Test harness only
24 CLOUDFLARE_ACCOUNT_ID Orphan (API) CI/operator
25–28 R940 MinIO/ZFS/cloudflared Infra-only Correct in BWS; not API env vars yet
29+ ~30 AWS SM keys Missing from BWS Operator cannot reconstruct ECS env from BWS

References