Security Posture: Database Access Control¶
LastVet / Last 1 Enterprises LLC
Date: 2026-07-16
Status: Internal. Accurate as written. Ready to share if asked.
Home: feeds Gate 0.5 B7 (security status note)
Companion: SECURITY_POSTURE_TOKEN_CUSTODY.md (VA OAuth token custody; web client parked until prod access); SECURITY_POSTURE_IMAGING_STORAGE.md (S3 imaging; approved plan, not built)
Why this exists¶
Our own due diligence, not anyone's request. Nobody asked for this document. It is written so that the honest state of database access control is a known checklist item rather than tribal knowledge, and so that if a reviewer, counsel, or a provider asks, the answer is already precise and already true.
Audiences it serves without modification: VA Lighthouse (if raised), Hogan Lovells, Fullscript compliance review, SOC 2 evidence, provider diligence at pilot.
1. Scope and standing facts¶
- No real veteran PHI has ever existed in any LastVet environment. All veteran data in every environment is synthetic, sourced from the VA sandbox.
- VA production data access is not approved. Case #00015000 is in review.
- Production is AWS (ECS + RDS, us-west-2) under an executed AWS Business Associate Agreement, HIPAA-eligible services only.
api.last.vetis DNS-only to the ALB. No third party terminates TLS on the API path.- The prior Railway environment was fully decommissioned 2026-07-15, before any real data exists.
2. What is enforced at the database layer¶
Runtime role. The application connects as lastvet_app.
| Property | Value |
|---|---|
rolsuper |
false |
rolbypassrls |
false |
Four tables carry PostgreSQL row-level security with FORCE ROW LEVEL SECURITY. These
are the tables holding clinical content.
| Table | RLS | FORCE RLS | Policy |
|---|---|---|---|
documents |
yes | yes | documents_request_context |
health_record_items |
yes | yes | health_record_items_request_context |
provider_notes |
yes | yes | provider_notes_request_context |
self_reported_entries |
yes | yes | self_reported_entries_request_context |
FORCE ROW LEVEL SECURITY matters: without it a table owner bypasses RLS by default. With
it, the policy applies to the owner as well.
Empirically verified, not asserted. Probe against production, 2026-07-16, table
documents:
| Connection | Rows visible |
|---|---|
Owner (lastvet_master) |
13 |
lastvet_app, no request context set |
0 |
lastvet_app, app.veteran_id set |
1 |
This is the claim we are willing to make: on these four tables, a request that fails to establish veteran context sees nothing, and the failure occurs in the database rather than in application logic.
Production gap found and fixed (2026-07-21). Human E2E on prod (E2E_RUN_LOG_2026-07-21_VA_PARALLEL_TRACK_HUMAN.md)
showed signed ROI documents present under lastvet_master but record.documents: [] under
lastvet_app. Root cause: fetchConsentArtifactDocuments (and related provider document
helpers in src/lib/providerDocumentAccess.ts) queried documents without calling
applyRequestDbContext / withRequestDbContext, so the documents_request_context policy
never received app.accessor_type = provider and app.consent_veteran_id. Application-layer
JOIN filters ran, but RLS returned zero rows. Fixed 2026-07-21: provider document reads
now set provider consent context per veteran before query; covered by
tests/provider-document-access-rls.test.ts.
DDL attempt, grant denial (verified prod, 2026-07-16). Runtime code in
ConsentEngine.queryWithSchemaRecovery attempts idempotent DDL on the lastvet_app pool
when a query hits 42P01. Grants deny it: has_schema_privilege('lastvet_app', 'public',
'CREATE') is false on production. This is not a successful DDL path — we looked.
3. What is enforced in the application only¶
Thirteen tables with a veteran_id column have no row-level security. They are
protected by application logic, not by the database.
| Table | Notes |
|---|---|
consent_grants |
Consent state. Gates access to everything else. Metadata only (categories, purpose, optional purpose_detail free text) — not clinical records. |
part2_consents |
42 CFR Part 2 consent records. Phase 1 wedge is mental health. Metadata only (purpose, is_tpo, optional legal_proceedings_detail) — not clinical records. |
consent_audit_log |
Audit trail |
secure_access_links |
Provider access tokens |
secure_link_access_events |
Access audit |
connection_requests |
|
veteran_preferences |
|
social_determinants |
|
last1_proof_events |
|
coordination_loops |
|
oauth_pending_authorizations |
Onset OAuth flow; veteran_id nullable until bound |
oauth_authorization_codes |
Onset OAuth flow |
oauth_authorization_grants |
Onset OAuth flow |
Also veteran-scoped, no RLS, different key: veteran_push_devices uses
veteran_identifier (not veteran_id). Device tokens only.
This is the gap, stated plainly. Consent state is arguably more sensitive than the clinical record it governs: a defect in the record layer exposes one veteran's data, while a defect in the consent layer changes who is permitted to see data at all. Those tables are the ones without database-layer enforcement.
Remediation: RLS batch 2, mirroring the 046 pattern, staging proof before production.
Reordered so that part2_consents and consent_grants come first, ahead of lower-risk
tables, because Part 2 is the wedge and consent gates the rest. Scoped, not scheduled. It is
required before any real veteran data is admitted, not before Gate 0 is declared.
4. Deliberate bypass, by design¶
The four RLS policies honor app.bypass_rls = 'true'. health-record.repo.ts sets it for
provider reads that legitimately span multiple veterans.
This is a designed capability, not a defect. A provider with valid consent from several veterans must be able to read across them. We are naming it because a security posture that omits its own escape hatch is not a security posture.
What this means precisely: the runtime role cannot bypass RLS by role privilege
(rolbypassrls is false). It can bypass by policy, through a flag the application sets
deliberately in one repository, for a defined use case.
5. Owner credentials and boot-time DDL¶
MIGRATION_DATABASE_URL(owner,lastvet_master) is mounted on every ECS task. It is required for migration one-off tasks and for boot-time schema reconciliation.- The in-process owner pool (
migrationDb) is referenced in exactly one place:ensureCoreSchema(migrationDb)insrc/index.ts, at boot. No request handler, route, service, or middleware imports or queries viamigrationDb. One-off ops scripts (migrate.js,verify-aws-db-role.js, provision helpers) open separate connections to the same owner URL; they are not part of the request path. ensureCoreSchemaexecutes idempotent DDL on every boot, as owner, outside the migration ledger. Every table it creates also exists in a migration file; there are no schema-only tables. Column- and index-level DDL inschema.tsalso appears inmigrations/*.sql(verified 2026-07-16). But boot DDL can still create objects before the corresponding migration is recorded inschema_migrations, which means the ledger can understate what is present. Observed once:privacy_notice_campaignsexisted on production from 2026-07-12, while migration049was not recorded until 2026-07-16.
Known risks, both tracked:
1. A future code change that imported the owner pool into a request path would bypass RLS.
No such path exists today.
2. Two schema authorities (migrations/*.sql and schema.ts) mean schema_migrations is
not a complete record of production schema.
Planned: gate ensureCoreSchema off when APP_ENV=production, matching the existing
aws-start.sh guard, making migrations the single authority on production. Column- and
index-level coverage check passed 2026-07-16 (see engineering report in session notes).
Shipped 2026-07-16: ensureCoreSchema skipped when APP_ENV=production (src/index.ts).
Staging and dev keep boot DDL. Migrations are the single prod schema authority.
Request handlers do not use migrationDb, but ConsentEngine.queryWithSchemaRecovery
attempts idempotent DDL on the lastvet_app pool when a query hits 42P01. On production
this fails (42501; verified has_schema_privilege(..., 'CREATE') = false); it is not a
successful DDL path. Removal tracked in RLS batch 2 phase 1a (hygiene prerequisite).
6. Migration policy¶
Production never auto-migrates. Verified in code, not assumed:
- ECS entrypoint is
scripts/aws-start.sh, which checksAPP_ENV = productionand skips boot migrations beforeRUN_MIGRATIONS_ON_BOOTis ever evaluated. scripts/migrate.jsruns on production only via an explicit one-off task (infra/scripts/run-ecs-db-setup.sh). It is not triggered by image deploy or service rollout.- CloudWatch confirms production service boots log only "Production environment: skipping boot migrations."
Pre-flight (2026-07-16). migrate.js --list-pending prints the pending set without
changing the database. migrate.js --expect <set> fails if pending ≠ expected. Prod
run-ecs-db-setup.sh lists pending before apply.
Known limitation, addressed. The APP_ENV guard controls when migrations run, not
which. migrate.js applies every pending migration unless --expect gates the set.
Observed once: migration 049 was applied alongside an intended 050.
7. Other controls in place¶
- Encryption at rest and in transit.
- Secrets in AWS Secrets Manager. GitHub holds no long-lived AWS credentials; workflows authenticate via OIDC to a scoped role.
- Production does not log one-time passcodes. The E2E harness reads OTPs from a dedicated mailbox over IMAP. The prior logging flag was removed from Terraform, Secrets Manager, and the code path on 2026-07-16.
- Penetration test and remediation complete. Retest planned after RLS batch 2, before any real veteran data.
- Public legal changelog at
last.vet/legal/changelog, including corrections made against our own interest.
8. The precise claim we make¶
Accurate:
LastVet's runtime database role holds no superuser and no RLS-bypass privilege. Four tables containing clinical content enforce row-level security with FORCE, verified empirically against production rather than assumed: a connection without veteran context sees zero rows. Thirteen further tables with a
veteran_idcolumn, including consent records, are currently protected by application logic rather than by the database. Extending row-level security to those tables, beginning with 42 CFR Part 2 consents and consent grants, is scoped and is required before any real veteran data is admitted to the platform. The RLS policies include a deliberate, code-controlled bypass for provider reads spanning multiple consenting veterans.
Not accurate, and previously stated too broadly in an unsolicited technical aside on 2026-07-12:
"row-level security enforced at the database connection level, which means the application itself cannot bypass the access controls protecting a veteran's record"
The first clause is true. The second over-generalizes four tables into "a veteran's record," and does not account for the deliberate policy bypass. Corrected here, in writing, before any real data exists and before anyone asked.
9. Status¶
| Item | State |
|---|---|
| Real veteran PHI in any environment | None. Ever. |
| VA production access | In review, not approved. Case #00015000 |
| RLS on clinical tables | Live, forced, empirically proven |
| Provider document RLS context wiring | Fixed 2026-07-21 (prod gap: consent artifacts empty under lastvet_app) |
| RLS on consent tables | Gap. Scoped as batch 2. Required before real data. |
ensureCoreSchema production gate |
Shipped 2026-07-16 — skipped when APP_ENV=production |
| Migration pre-flight | Shipped 2026-07-16 |
| Pentest retest | Planned, after batch 2 |