Files
vigilcare-clinical/docs/ops/phi-encryption-runbook.md
T
2026-06-23 00:16:39 +08:00

7.3 KiB

PHI Encryption & Access Logging — Operations Runbook

Phase 32 encrypts sensitive patient demographics at rest and records who accessed PHI. This runbook covers key management, migration, rotation, compliance, and troubleshooting.


Architecture summary

Layer Mechanism
Encryption at rest ASP.NET Data Protection API (AES-256-GCM) via EF Core value converters
Encrypted columns first_name, last_name, date_of_birth, allergies, emergency_contact_name, emergency_contact_phone
Plaintext (by design) mrn — exact-match lookup only
Name search HMAC-SHA256 name_search_token index — search without decrypting all rows
Access audit Append-only phi_access_logs table; written by PhiAccessLogService on patient Get/List/Register/Update

Configuration lives in appsettings.json under PhiEncryption and DataProtection. Implementation: PhiEncryptionService, PatientPhiConverterConfigurator, PatientService.


Secrets and key storage

Never commit:

  • data-protection-keys/ (Data Protection key ring)
  • Production PhiEncryption:SearchTokenKey
  • Any Key Vault / KMS credentials

data-protection-keys/ is listed in .gitignore. Treat loss of this directory as unrecoverable data loss for encrypted PHI columns.

Environment Data Protection key ring Search HMAC key (SearchTokenKey)
Development ./data-protection-keys/ on disk (DataProtection:KeyPath) appsettings.json (dev placeholder only)
Production Azure Key Vault XML blob or AWS KMS-backed store Key Vault / Secrets Manager secret — not appsettings

Initial deployment / encrypting existing rows

New patients are encrypted automatically on save. Existing plaintext rows need a one-time re-save.

./scripts/encrypt-existing-patient-phi.sh

Or directly:

dotnet run --project VigilCareClinicalAPI -- encrypt-phi

Loads every patient through EF, applies value converters, and recomputes name_search_token.

Option B — Startup hosted service

PatientPhiMigrationService runs on application start and backfills search tokens for rows that are not yet encrypted. It is registered in Program.cs. Disable or remove after the first successful production deploy to avoid redundant work on every restart.


Verifying encryption

Automated tests

dotnet test VigilCareClinicalAPI.Tests --filter "FullyQualifiedName~PhiEncryption"

Live stack (API + Postgres running)

./scripts/run-phase32-verification.sh

Manual DB check

Encrypted first_name values will not equal the patient's plaintext name:

SELECT id, mrn, left(first_name, 30) AS encrypted_prefix
FROM patients
LIMIT 5;

Prefix often starts with CfDJ8 (Data Protection default).

PHI access logs

# Admin token required (AuditRead permission)
curl -H "Authorization: Bearer $TOKEN" \
  "http://localhost:5270/api/v1/phi-access-logs?patientId=<uuid>"

Prometheus metric: phi_access_logs_total{access_type="VIEW|LIST|SEARCH|CREATE|UPDATE"}.


Key rotation

Always back up the current key ring before rotating.

Encryption keys (Data Protection)

  1. Snapshot data-protection-keys/ (or export Key Vault blob).
  2. Deploy new key ring alongside the old one (Data Protection supports multiple keys; newest encrypts, all decrypt).
  3. Change PhiEncryption:ProtectorPurpose to a new version string (e.g. VigilCare.PatientPhi.v2).
  4. Re-run encrypt command to re-encrypt all patient rows with the new protector:
    dotnet run --project VigilCareClinicalAPI -- encrypt-phi
    
  5. Verify API reads and raw DB ciphertext look correct.
  6. Retire old keys only after confirming all rows decrypt successfully.

Search HMAC key (SearchTokenKey)

  1. Store new key in secrets manager.
  2. Update PhiEncryption:SearchTokenKey in configuration.
  3. Re-run encrypt-phi (recomputes all name_search_token values).
  4. Confirm name search still works (GET /api/v1/patients?q=First+Last).

Rotating one key without the other does not require touching the other, but both rotations need a full patient re-save.


PHI access log retention (HIPAA)

phi_access_logs records who accessed patient demographics, when, from which path, and (hashed) search terms. HIPAA requires audit controls; retention guidance is minimum 6 years.

This phase creates the table and query API (GET /api/v1/phi-access-logs). Automated retention/archival policy is out of scope — plan for Phase 33+ (partitioning, cold storage, or purge job with legal review).

Query endpoints:

Endpoint Permission
GET /api/v1/phi-access-logs AuditRead
GET /api/v1/phi-access-logs/patients/{id} PatientsRead

Cross-system PHI (out of scope)

These paths are not covered by column encryption in PostgreSQL:

System Risk Action
Elasticsearch patient documents May index decrypted names at index time Exclude PHI fields or accept decrypted-at-index policy
Kafka / data lake Parquet events patientName in outbox payloads (e.g. encounter events) Review lake schemas; redact or tokenize in a future phase
Application logs Serilog must not log PHI fields Audit log configuration

Troubleshooting

CryptographicException / cannot decrypt patient

  • Key ring missing or wrong DataProtection:KeyPath
  • App deployed to new host without copying data-protection-keys/
  • ProtectorPurpose changed without re-running encrypt-phi

Fix: Restore key ring from backup. Do not delete old keys until all data is re-encrypted.

Name search returns no results

  • name_search_token is null on older rows → run encrypt-phi
  • SearchTokenKey changed without token recompute
  • Query format: two-term search uses First Last (space-separated); single term matches first or last name only

PHI access logs empty

  • Caller not authenticated (PhiAccessLogService skips unauthenticated requests)
  • PhiEncryption:LogListAccess is false (list/search aggregate logs suppressed)
  • Integration/FHIR paths must still authenticate (Phase 31 Integration role)

Column length errors on encrypt

Migration WidenPhiEncryptedColumns widens first_name, last_name, and emergency contact columns to text. Ciphertext is longer than plaintext. If new columns are added to encryption, ensure DB column types accommodate protected payload size.

Tests fail with 500 on patient register

  • Confirm migrations applied (AddPatientNameSearchToken, AddPhiAccessLogs, WidenPhiEncryptedColumns)
  • Confirm PhiEncryption:SearchTokenKey is set in configuration
  • Confirm data-protection-keys/ is writable in the API working directory

File Purpose
VigilCareClinicalAPI/Services/PhiEncryptionService.cs Encrypt/decrypt + HMAC tokens
VigilCareClinicalAPI/Services/PhiAccessLogService.cs Access audit writes
VigilCareClinicalAPI/Commands/EncryptPhiCommand.cs Bulk re-save CLI
VigilCareClinicalAPI/BackgroundServices/PatientPhiMigrationService.cs Startup token backfill
scripts/encrypt-existing-patient-phi.sh Wrapper for encrypt CLI
scripts/run-phase32-verification.sh End-to-end verification
docs/plans/phase-32-plan.md Implementation plan and design rationale