Technical safeguards are the controls this assessment can speak to with the most confidence, because they are visible in source code and in live infrastructure configuration. 16 controls assessed, 15 scored.
__Secure- prefix, HttpOnly, SameSite=Strict, path-scoped,
with Cache-Control: no-store on session responses.| # | Citation | Requirement | Status | Evidence (masked) |
|---|---|---|---|---|
| T1 | §164.312(a)(2)(i) | Unique User Identification (Required) | ✅ Compliant | Every principal resolves to a single identity. Native path derives userId/nativeUserId from the verified Cognito subject (src/auth/native-request-guard.ts:103); legacy path resolves identity server-side via the legacy identity API. AWS side: 9 individual IAM users, no shared login account observed. |
| T2 | §164.312(a)(1) | Access Control — tenant (practice) isolation | ✅ Compliant | Fail-closed tenant assertion on every native route: assertTenantContext() (src/auth/native-request-guard.ts:363) requires a practiceId in params/query/body and rejects the request when it is absent (“Native route tenant authorization is unavailable”) or mismatched. Client-supplied identity headers are stripped before evaluation (stripLegacyIdentityHeaders(), line 306). Defence in depth: every repository query is additionally scoped by practice_id. |
| T3 | §164.312(a)(1) | Access Control — role-based “minimum necessary” within a tenant | ⚠️ Partial | An rbac_* schema with roles, feature keys and per-action permissions exists and is enforced by requireAnyRbacFeaturePermission() (src/modules/rbac/rbac.auth.ts:265). However only 9 of 48 route modules call it. PHI-heavy modules — patients, patient-files, face-charts, appointment-summaries — enforce tenant scope but no feature-level permission, so any authenticated user of a practice can reach that practice’s full patient record set. |
| T4 | §164.312(a)(2)(ii) | Emergency Access Procedure (Required) | 🔍 Manual-Review | No break-glass role, documented emergency-access runbook, or dedicated emergency account was identified in code or AWS configuration. This is a procedural control — it must be evidenced by documentation, not inferred. |
| T5 | §164.312(a)(2)(iii) | Automatic Logoff (Addressable) | ⚠️ Partial | The native design is strong: Cognito access and ID tokens are capped at 15 minutes and refresh tokens at 12 hours (infra/cdk/lib/revique-api-stack.ts:2305–2351), with a __Secure--prefixed, HttpOnly, SameSite=Strict, path-scoped refresh cookie and Cache-Control: no-store on session responses (src/auth/provider-sessions.ts:437–471). But production does not run that path (see T12) — effective session lifetime is governed by the legacy platform and was not verifiable here. |
| T6 | §164.312(a)(2)(iv) | Encryption of ePHI at Rest (Addressable) | ⚠️ Partial | Native estate is fully encrypted: Aurora PostgreSQL cluster revique-api-prod-database-**** StorageEncrypted=true under KMS key 74e****; all EBS volumes encrypted with account-level EBS default encryption ON in both regions; all 22 S3 buckets have default SSE (AES256 or KMS). However the legacy MySQL secondary instance (vm1****, created 2019) reports StorageEncrypted=false while holding legacy platform data. |
| T7 | §164.312(b) | Audit Controls — infrastructure / API activity | ✅ Compliant | CloudTrail trail auditlog-hipaa: multi-region, IsLogging=true (delivery confirmed same-day), log-file validation enabled, encrypted with customer KMS key f83****, global service events included. Event selectors cover management events (Read+Write) and S3 object-level data events across arn:aws:s3 — so object access to PHI document buckets is captured at the infrastructure layer. |
| T8 | §164.312(b) | Audit Controls — application-level PHI access trail | ❌ Gap | rbac_audit_events exists (src/database/migrations.ts:997) but its 9 call sites record only RBAC administration (rbac.user.upsert, rbac.role.create, …) plus one payment-provisioning event — not record access. A 3-hour Logs Insights sample of /revique/revique-api/prod/api found exactly two event types: api_request_timing and revique_error_occurrence. The request record carries route template, method, status and correlation id but no user or subject identifier, so the logs cannot answer “which workforce member viewed which patient record, and when” — the core of §164.312(b). |
| T9 | §164.312(b) / §164.316(b)(2)(i) | Audit record retention | ❌ Gap | Native API log groups retain 180 days (/revique/revique-api/prod/api and siblings). Legacy platform groups mostly retain 1827 days (5 years). HIPAA expects documentation — including audit records relied on for compliance — to be retained 6 years (2192 days). Both tiers fall short; the native tier falls short by an order of magnitude. |
| T10 | §164.312(c)(1) | Integrity — protection from improper alteration/destruction | ⚠️ Partial | In place: CloudTrail log-file validation; S3 versioning on the native patient-files bucket and 2 legacy stores; RDS deletion protection and copyTagsToSnapshot; read paths wrapped in BEGIN TRANSACTION READ ONLY; and no SQL injection exposure — all 386 query call sites use $n placeholders, with dynamic fragments (sort columns, filters) resolved through allow-list maps keyed by schema-validated enums. Weaknesses: versioning is off on several legacy PHI buckets including the patient-documents store, and no S3 Object Lock / WORM is configured anywhere. |
| T11 | §164.312(c)(2) | Mechanism to Authenticate ePHI (Addressable) | ⚠️ Partial | SHA-256 content digests are computed for capture, backfill and capability-parity flows (e.g. src/modules/patient-master-data/, src/platform/capability-parity/), giving tamper-evidence for migration payloads. There is no end-to-end integrity check (checksum/signature) on stored PHI records or on patient documents in S3 outside of what S3 provides natively. |
| T12 | §164.312(d) | Person or Entity Authentication — application | ⚠️ Partial | Production runs in legacy-compatible mode: AUTH_PROVIDER is unset in the deployed ECS task definition, so it defaults to "disabled" (src/config/env.ts:36) and the native Cognito guard short-circuits. Authentication is instead performed by LEGACY_RBAC_SESSION_VERIFICATION_ENABLED=true — a genuine server-side HTTPS call that validates the bearer session against the legacy identity API, with an HTTPS-only base URL, redirect: "error", bounded JSON reads and an abort timeout (src/modules/rbac/rbac.legacy-session.ts:372–500). Positive: the risky unverified-JWT path is off — RBAC_TRUSTED_AUTH_CONTEXT_HEADERS=false in production. Rated Partial because the hardened native path (short-lived verified tokens, per-request live user status re-check) is implemented but not yet active, and no MFA is enforced for application users. |
| T13 | §164.312(d) | Person or Entity Authentication — AWS console MFA | ❌ Gap | Of 9 IAM users, 5 have console sign-in enabled and only 1 of those 5 has an MFA device registered. Four identities can therefore authenticate to the production account holding PHI with a password alone. Two further users are SES SMTP service accounts (no console, expected). |
| T14 | §164.312(e)(1) | Transmission Security — external (client → API) | ✅ Compliant | ALB HTTPS listener uses ELBSecurityPolicy-TLS13-1-2-2021-06 (TLS 1.2 floor, TLS 1.3 available); port 80 exists only to redirect to 443. @fastify/helmet is registered (src/app.ts:953) supplying HSTS and related headers; the native guard independently rejects any request whose x-forwarded-proto is not https (assertForwardedHttps(), line 434). Session cookies are Secure + HttpOnly + SameSite=Strict. |
| T15 | §164.312(e)(1) | Transmission Security — internal (API → database) | ❌ Gap | The deployed ECS task definition sets DATABASE_SSL_REJECT_UNAUTHORIZED=false, and the client honours it verbatim (src/database/connection.ts:47–60). The connection to the PHI database is therefore encrypted but the server certificate is not validated, leaving it open to in-VPC interception or endpoint spoofing. The fix is already demonstrated in this repo: the sibling capture stack sets DATABASE_SSL_REJECT_UNAUTHORIZED="true" and ships the AWS RDS CA bundle (certs/aws-rds-global-bundle.pem). |
| T16 | §164.312(e)(2) | Integrity Controls in Transit (Addressable) | ⚠️ Partial | TLS supplies transit integrity on the public edge (T14). Inside AWS, only 2 of 22 S3 buckets carry a bucket policy denying non-TLS access (aws:SecureTransport) — the native patient-files bucket and the CDK asset bucket. The legacy PHI document stores do not enforce it, so a plain-HTTP request to those buckets would not be rejected at the policy layer. |
| Store | Region | Encrypted | Notes |
|---|---|---|---|
Aurora PostgreSQL cluster revique-api-prod-database-**** | us-e****-1 | ✅ Yes | KMS key 74e****; Multi-AZ; 14-day backups; deletion protection; IAM auth enabled; private subnets; PostgreSQL logs exported |
Legacy MySQL primary vm1**** (2021) | us-e****-2 | ✅ Yes | MySQL 8.0; publicly accessible; backup retention 1 day |
Legacy MySQL secondary vm1**** (2019) | us-e****-2 | ❌ No | MySQL 8.0; publicly accessible; backup retention 1 day — the only unencrypted PHI store found |
| EBS volumes (all) | both | ✅ Yes | 100% encrypted; account-level EBS default encryption enabled in both regions |
| S3 — all 22 buckets | both | ✅ Yes | Default SSE on every bucket (AES256, one KMS). Weakness is in transit and access policy, not at rest — see below |
| Bucket (masked) | Role | SSE | Public access block | Versioning | TLS-only policy |
|---|---|---|---|---|---|
revique-api-prod-patientfilesbucket**** | Native patient files | AES256 | Full | On | ✅ Enforced |
v1-****-hpt-patientdocs | Legacy patient documents | AES256 | Partial | Off | ❌ None |
v1-****-hpt-uploadpatientstore | Legacy patient uploads | AES256 | Partial | On | ❌ None |
revique-healthscribe-**** | Clinical transcription output | AES256 | Full | Off | ❌ None |
Across the whole account: 22 buckets, 22 encrypted at rest, 2 enforcing TLS-only access, and 6 without a complete public-access block. The native patient-files bucket is the reference implementation — the legacy stores should be brought up to it.
A single 3-hour Logs Insights window over /revique/revique-api/prod/api, counts only, no message bodies retrieved:
| Query | Result | What it establishes |
|---|---|---|
| Event types emitted | api_request_timing × 3,997 · revique_error_occurrence × 183 |
Request-level access logging exists and is active. No PHI-access event type exists — direct evidence for T8. |
| HTTP status distribution | 200 × 2,994 · 202 × 148 · 401 × 432 · 403 × 382 · 404 × 32 · 503 × 5 | Authentication and authorization failures are logged and observable — the raw material for §164.308(a)(1)(ii)(D) review. The volume (~4.5 denials/minute) also merits investigation on its own. |
| Log level distribution | info × 3,986 · warn × 170 · error × 0 | Service is healthy over the sampled window. |
| Record structure | Structured JSON: level, time, reqId, correlationId, durationMs, event, method, route, statusCode |
Route is stored as a template (/v1/…/:id), not an interpolated path — so no identifiers leak into logs. Good for confidentiality; but with no actor or subject field, the log cannot support §164.312(b). |
§164.312(b) requires mechanisms that “record and examine activity in information systems that contain or use electronic
protected health information.” Infrastructure-layer auditing (T7) satisfies this for documents in S3, because CloudTrail data
events capture object access. It does not satisfy it for records in the database: a clinician reading 400 patient charts through
the API produces 400 indistinguishable api_request_timing lines with no patient identifier and no user identifier.
That is the gap — and it is also why breach assessment would be difficult. Fixing it is mostly plumbing: the
rbac_audit_events table, the repository pattern and the correlation-id propagation all already exist.
****).