Skip to content

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.vet is 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) in src/index.ts, at boot. No request handler, route, service, or middleware imports or queries via migrationDb. 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.
  • ensureCoreSchema executes 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 in schema.ts also appears in migrations/*.sql (verified 2026-07-16). But boot DDL can still create objects before the corresponding migration is recorded in schema_migrations, which means the ledger can understate what is present. Observed once: privacy_notice_campaigns existed on production from 2026-07-12, while migration 049 was 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 checks APP_ENV = production and skips boot migrations before RUN_MIGRATIONS_ON_BOOT is ever evaluated.
  • scripts/migrate.js runs 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_id column, 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