Technical Safeguards — §164.312

Access control, audit controls, integrity, authentication and transmission security
revique-api  ·  environment hptprod  ·  account 501****  ·  regions us-e****-1 / us-e****-2  ·  generated 2026-08-03  ·  values masked
Disclaimer. This is an internal engineering HIPAA gap-assessment. It is not a certified HIPAA audit, a compliance attestation, or a legal determination. Formal compliance requires a qualified assessor and an administrative review of policies, procedures, training records and Business Associate Agreements — none of which can be established from source code and infrastructure telemetry. The percentage below measures technically-verifiable posture only and must not be quoted as a compliance score.

Technical Safeguards 45 CFR §164.312

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.

4
Compliant
7
Partial
4
Gap
1
Manual-Review
Section score. 7.5 points earned across 15 scored controls → 50.0% for Technical Safeguards, carrying 45% of the overall weighting. Manual-Review controls are excluded from this figure and listed separately.

Notable strengths worth preserving

Control detail

#CitationRequirementStatusEvidence (masked)
T1§164.312(a)(2)(i)Unique User Identification (Required)✅ CompliantEvery 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✅ CompliantFail-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⚠️ PartialAn 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-ReviewNo 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)⚠️ PartialThe 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)⚠️ PartialNative 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✅ CompliantCloudTrail 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❌ Gaprbac_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❌ GapNative 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⚠️ PartialIn 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)⚠️ PartialSHA-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⚠️ PartialProduction 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 offRBAC_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❌ GapOf 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)✅ CompliantALB 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)❌ GapThe 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)⚠️ PartialTLS 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.

Encryption at rest — observed state

StoreRegionEncryptedNotes
Aurora PostgreSQL cluster revique-api-prod-database-****us-e****-1 ✅ YesKMS 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 ✅ YesMySQL 8.0; publicly accessible; backup retention 1 day
Legacy MySQL secondary vm1**** (2019)us-e****-2 ❌ NoMySQL 8.0; publicly accessible; backup retention 1 day — the only unencrypted PHI store found
EBS volumes (all)both ✅ Yes100% encrypted; account-level EBS default encryption enabled in both regions
S3 — all 22 bucketsboth ✅ YesDefault SSE on every bucket (AES256, one KMS). Weakness is in transit and access policy, not at rest — see below

S3 posture on the PHI-bearing buckets

Bucket (masked)RoleSSEPublic access blockVersioningTLS-only policy
revique-api-prod-patientfilesbucket****Native patient filesAES256 FullOn✅ Enforced
v1-****-hpt-patientdocsLegacy patient documentsAES256 PartialOff❌ None
v1-****-hpt-uploadpatientstoreLegacy patient uploadsAES256 PartialOn❌ None
revique-healthscribe-****Clinical transcription outputAES256 FullOff❌ 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.

Operational evidence from production logs

A single 3-hour Logs Insights window over /revique/revique-api/prod/api, counts only, no message bodies retrieved:

QueryResultWhat it establishes
Event types emittedapi_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 distribution200 × 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 distributioninfo × 3,986 · warn × 170 · error × 0Service is healthy over the sampled window.
Record structureStructured 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).

Why T8 matters more than its single row suggests

§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.

CONFIDENTIAL — INTERNAL USE ONLY
HIPAA Security Rule engineering gap-assessment · revique-api · generated 2026-08-03
All account identifiers, ARNs, endpoints, bucket names, IP addresses, resource IDs and domains are masked (first 3 characters retained, remainder replaced with ****).
Evidence was collected with read-only AWS API calls. No PHI and no secret values were read, extracted or reproduced in this report.