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.
Option A — CLI (recommended)
./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)
- Snapshot
data-protection-keys/(or export Key Vault blob). - Deploy new key ring alongside the old one (Data Protection supports multiple keys; newest encrypts, all decrypt).
- Change
PhiEncryption:ProtectorPurposeto a new version string (e.g.VigilCare.PatientPhi.v2). - Re-run encrypt command to re-encrypt all patient rows with the new protector:
dotnet run --project VigilCareClinicalAPI -- encrypt-phi - Verify API reads and raw DB ciphertext look correct.
- Retire old keys only after confirming all rows decrypt successfully.
Search HMAC key (SearchTokenKey)
- Store new key in secrets manager.
- Update
PhiEncryption:SearchTokenKeyin configuration. - Re-run
encrypt-phi(recomputes allname_search_tokenvalues). - 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/ ProtectorPurposechanged without re-runningencrypt-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_tokenis null on older rows → runencrypt-phiSearchTokenKeychanged 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 (
PhiAccessLogServiceskips unauthenticated requests) PhiEncryption:LogListAccessisfalse(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:SearchTokenKeyis set in configuration - Confirm
data-protection-keys/is writable in the API working directory
Related files
| 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 |