HIPAA PHI Access Controls Specification
Document ID: CODITECT-BIO-HIPAA-AC-001 Version: 1.0.0 Effective Date: 2026-02-16 Classification: Internal - Restricted Owner: Chief Information Security Officer (CISO) & Privacy Officer
Document Control
Approval History
| Role | Name | Signature | Date |
|---|---|---|---|
| Chief Information Security Officer | [Pending] | [Digital Signature] | YYYY-MM-DD |
| Privacy Officer | [Pending] | [Digital Signature] | YYYY-MM-DD |
| VP Quality Assurance | [Pending] | [Digital Signature] | YYYY-MM-DD |
| Regulatory Affairs Director | [Pending] | [Digital Signature] | YYYY-MM-DD |
Revision History
| Version | Date | Author | Changes | Approval Status |
|---|---|---|---|---|
| 1.0.0 | 2026-02-16 | CISO Office + Privacy Office | Initial release | Draft |
Distribution List
- Executive Leadership Team
- Information Security Team
- Privacy Office
- Quality Assurance Team
- Compliance and Regulatory Affairs
- Clinical Operations
- Internal Audit
Review Schedule
| Review Type | Frequency | Next Review Date | Responsible Party |
|---|---|---|---|
| Annual Review | 12 months | 2027-02-16 | CISO + Privacy Officer |
| Regulatory Update Review | As needed | N/A | Regulatory Affairs |
| Post-Breach Review | As needed | N/A | Privacy Officer + Security Team |
| Access Review Audit | Quarterly | 2026-05-16 | Privacy Officer |
1. Executive Summary
1.1 Purpose
This HIPAA PHI Access Controls Specification establishes comprehensive technical and administrative safeguards for controlling access to Protected Health Information (PHI) within the CODITECT Biosciences Quality Management System (BIO-QMS) Platform. This document ensures compliance with:
- HIPAA Security Rule 45 CFR §164.312(a) — Access Control
- HIPAA Privacy Rule 45 CFR §164.514(d) — Minimum Necessary Standard
- NIST SP 800-66 Rev 2 — HIPAA Security Rule Implementation Guide
- HHS Guidance on Access Controls and Minimum Necessary
1.2 Scope
In Scope:
- All PHI data elements across the BIO-QMS platform
- Authentication and authorization mechanisms for PHI access
- Multi-factor authentication (MFA) requirements
- Minimum necessary principle enforcement
- Session management and timeout controls
- Break-glass emergency access procedures
- Access review and recertification processes
- Technical implementation architecture
Out of Scope:
- Physical access controls (covered in Physical Security Policy)
- Business Associate Agreements (covered in Legal/Contracts)
- Breach notification procedures (covered in Incident Response Plan)
- End-user device security (covered in Endpoint Security Policy)
1.3 Regulatory Requirements Overview
| HIPAA Section | Requirement | Implementation Standard | BIO-QMS Control |
|---|---|---|---|
| §164.312(a)(1) | Access Control | Required | RBAC + field-level masking + session management |
| §164.312(a)(2)(i) | Unique User Identification | Required | ECDSA P-256 signing keys per user + SSO identity |
| §164.312(a)(2)(ii) | Emergency Access Procedure | Required | Break-glass workflow with 4-hour expiration |
| §164.312(a)(2)(iii) | Automatic Logoff | Addressable (IMPLEMENTED) | 15-minute idle timeout, configurable per role |
| §164.312(a)(2)(iv) | Encryption and Decryption | Addressable (IMPLEMENTED) | AES-256-GCM for PHI at rest, TLS 1.3 in transit |
| §164.312(b) | Audit Controls | Required | Tamper-evident audit log with HMAC-SHA256 integrity chain |
| §164.312(d) | Person or Entity Authentication | Required | MFA (TOTP/WebAuthn) required for all PHI access |
2. PHI Data Classification
2.1 HIPAA Identifier Inventory
The HIPAA Privacy Rule (45 CFR §164.514(b)) defines 18 identifiers that constitute PHI when linked to health information:
| Identifier Category | HIPAA Identifier | BIO-QMS Data Fields | System Location | Access Control Level |
|---|---|---|---|---|
| Direct Identifiers | ||||
| 1. Names | Names (full, last, first, maiden) | patient.full_name, patient.first_name, patient.last_name | patients table, Work Order description | L3 — Clinical staff only |
| 2. Geographic subdivisions | All addresses smaller than state | patient.street_address, patient.city, patient.zip_code | patient_addresses table | L3 — Clinical staff only |
| 3. Dates | All dates except year (birth, admission, discharge, death) | patient.date_of_birth, work_order.service_date | patients table, work_orders table | L3 — Clinical + QA |
| 4. Telephone numbers | All telephone numbers | patient.phone_number, patient.mobile_number | patient_contacts table | L3 — Clinical staff only |
| 5. Fax numbers | All fax numbers | facility.fax_number | facilities table | L2 — Administrative |
| 6. Email addresses | All email addresses | patient.email | patient_contacts table | L3 — Clinical staff only |
| 7. Social Security numbers | SSN | patient.ssn (encrypted) | patients table, encrypted column | L4 — Billing only, audit logged |
| 8. Medical record numbers | MRN, account numbers | patient.mrn, work_order.account_number | patients table, work_orders table | L3 — Clinical + Billing |
| 9. Health plan numbers | Insurance policy/subscriber numbers | patient_insurance.policy_number | patient_insurance table | L3 — Billing only |
| 10. Device identifiers | Device serial numbers (implants, equipment) | asset.serial_number, work_order.device_id | assets table, work_orders table | L2 — Clinical + IT |
| 11. Vehicle identifiers | License plate numbers | patient.vehicle_license_plate | patient_metadata JSONB | L3 — Clinical staff only |
| 12. URLs | URLs containing patient identifiers | patient_documents.file_url | documents table | L3 — Clinical staff only |
| 13. IP addresses | IP addresses linked to patients | audit_trail.ip_address (when patient-linked) | audit_trail table | L2 — Security + Auditors |
| 14. Biometric identifiers | Fingerprints, retinal scans, voice prints | patient.biometric_hash | patient_biometrics table | L4 — Clinical + Security |
| 15. Photos | Full-face photos | patient.photo_url, patient_documents.image_url | S3 storage with KMS encryption | L3 — Clinical staff only |
| 16. Unique identifying numbers | Any unique number/code/characteristic | patient.uuid, patient.custom_id | patients table | L3 — Clinical staff only |
| Indirect Identifiers | ||||
| 17. Relatives' names | Names of relatives, employers, household members | patient.emergency_contact_name, patient.next_of_kin | patient_contacts table | L3 — Clinical staff only |
| 18. Other characteristics | Any unique characteristic that could identify individual | patient_metadata JSONB (free-text notes) | patient_metadata table | L3 — Clinical staff only |
Access Control Levels:
- L1 (Public): No PHI, publicly available information
- L2 (Internal): De-identified data, administrative use, no direct identifiers
- L3 (Restricted): PHI accessible to authorized clinical/billing staff, minimum necessary enforcement
- L4 (Highly Restricted): Sensitive PHI (SSN, biometrics), enhanced audit logging, CISO approval required
2.2 PHI Sensitivity Classification
Beyond the 18 HIPAA identifiers, certain PHI requires additional protection due to sensitivity:
| Sensitivity Category | Description | BIO-QMS Fields | Additional Controls |
|---|---|---|---|
| Standard PHI | General health information linked to identifiers | All L3 fields above | Standard RBAC + minimum necessary |
| Sensitive PHI — Mental Health | Psychotherapy notes, mental health diagnoses | clinical_notes.psychotherapy_note, diagnosis.mental_health_code | Additional consent required, separate authorization |
| Sensitive PHI — Substance Abuse | Substance abuse treatment records | treatment.substance_abuse_flag, diagnosis.substance_use_disorder | 42 CFR Part 2 compliance, separate consent |
| Sensitive PHI — HIV/AIDS | HIV test results, AIDS diagnoses | lab_results.hiv_status, diagnosis.hiv_code | State law compliance, enhanced confidentiality |
| Sensitive PHI — Genetic | Genetic testing results, family history | lab_results.genetic_test, patient.genetic_markers | GINA compliance, special authorization |
| Sensitive PHI — Reproductive | Pregnancy status, reproductive health | clinical_notes.pregnancy_status, procedure.reproductive_code | Enhanced privacy, separate authorization |
Enhanced Controls for Sensitive PHI:
- Dual Authorization: Requires both user role permission AND patient-specific consent
- Enhanced Audit Logging: All access logged with justification field (required)
- No Caching: Sensitive PHI NEVER cached in application or browser
- Redaction by Default: Masked in reports/exports unless explicitly authorized
2.3 PHI Data Flow Diagram
┌─────────────────────────────────────────────────────────────────────┐
│ PHI ENTRY POINTS (Data Ingress) │
├─────────────────────────────────────────────────────────────────────┤
│ 1. Clinical User Entry → Work Order creation, patient registration │
│ 2. HL7 FHIR API → External EHR integration (lab results, ADT) │
│ 3. File Upload → Medical device outputs, imaging reports │
│ 4. Vendor Systems → Equipment service records, IQ/OQ documentation │
└──────────────────┬──────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PHI PROCESSING (Data in Use) │
├─────────────────────────────────────────────────────────────────────┤
│ • Field-level encryption (AES-256-GCM) for L4 PHI │
│ • Row-level security (RLS) enforces tenant isolation │
│ • Application middleware filters PHI fields based on role │
│ • Audit log captures all PHI access with user, timestamp, purpose │
└──────────────────┬──────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PHI STORAGE (Data at Rest) │
├─────────────────────────────────────────────────────────────────────┤
│ • PostgreSQL database with KMS-encrypted columns (L3+) │
│ • S3 storage with server-side encryption (SSE-KMS) for documents │
│ • Backup encrypted with separate KMS key, cross-region replication │
│ • No PHI in application logs (scrubbed via regex + ML detection) │
└──────────────────┬──────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PHI EXIT POINTS (Data Egress) │
├─────────────────────────────────────────────────────────────────────┤
│ 1. User Interface → Filtered by role, field-level masking applied │
│ 2. API Responses → JSON responses filtered per minimum necessary │
│ 3. Reports/Exports → De-identification option, audit trail required│
│ 4. HL7 FHIR API → External systems via BAA, TLS 1.3 encryption │
│ 5. Audit Logs → Immutable S3 bucket, 10-year retention │
└─────────────────────────────────────────────────────────────────────┘
2.4 PHI Inventory by System Module
| Module | PHI Fields | Field Count | Access Roles | Sensitivity Level |
|---|---|---|---|---|
| Patient Registry | patients.* (all fields) | 28 | CLINICAL, BILLING, QA | L3 (Standard PHI) |
| Work Orders | work_orders.description, work_orders.service_date, work_orders.account_number | 3 | CLINICAL, SYSTEM_OWNER, QA | L3 (Standard PHI) |
| Clinical Notes | clinical_notes.* (all text fields) | 12 | CLINICAL (assigned provider only) | L3-L4 (Sensitive PHI if psychotherapy) |
| Lab Results | lab_results.* (all result fields) | 18 | CLINICAL, QA | L3-L4 (Sensitive if genetic/HIV) |
| Audit Trail | audit_trail.patient_id, audit_trail.phi_accessed | 2 | AUDITOR, PRIVACY_OFFICER | L2 (De-identified when possible) |
| Documents | documents.file_url, documents.description | 2 | CLINICAL, QA | L3 (Standard PHI) |
| Billing | invoices.*, patient_insurance.* | 15 | BILLING, QA | L3 (Standard PHI + L4 for SSN) |
Total PHI Fields: 80+ across 7 modules Total Sensitive PHI Fields: 12 Total L4 (Highly Restricted) Fields: 4 (SSN, biometric identifiers, genetic results, HIV status)
3. Multi-Factor Authentication (MFA) for PHI Access
3.1 MFA Requirement Policy
Policy Statement: All users accessing Protected Health Information (PHI) MUST authenticate using multi-factor authentication (MFA). Single-factor authentication (password-only) is PROHIBITED for PHI access.
Regulatory Basis:
- HIPAA §164.312(d) — Person or Entity Authentication
- NIST SP 800-66 Rev 2, Section 3.6 — Multi-Factor Authentication
- HHS Cybersecurity Newsletter (March 2023) — MFA strongly recommended for PHI access
Scope:
- Human users accessing PHI via web interface, API, or mobile app
- Service-to-service authentication requires mTLS + API key rotation (equivalent to MFA for non-human entities)
- Break-glass emergency access requires MFA re-authentication (no exceptions)
3.2 Supported MFA Methods
| MFA Method | Technology | Security Level | Recommended For | Implementation |
|---|---|---|---|---|
| TOTP (Time-based One-Time Password) | RFC 6238 (6-digit codes, 30-second window) | High | All users (default) | Google Authenticator, Authy, Microsoft Authenticator |
| WebAuthn/FIDO2 Hardware Keys | FIDO2 standard (YubiKey, Titan Key) | Very High | System Owners, QA, Admins (required) | WebAuthn API, passwordless authentication |
| Push Notification | Mobile app push (Duo, Okta Verify) | High | Clinical users (mobile workflow) | Push notification via auth provider SDK |
| SMS OTP | SMS text message (6-digit code) | Medium (FALLBACK ONLY) | Emergency account recovery | Twilio SMS gateway, rate-limited |
| Biometric (Mobile) | Fingerprint, Face ID (device-local) | High | Mobile app users | Device biometric + TOTP seed |
MFA Method Selection Criteria:
| User Role | Primary MFA | Backup MFA | Rationale |
|---|---|---|---|
| CLINICAL | TOTP | Push Notification | Balance of security and workflow efficiency |
| SYSTEM_OWNER | FIDO2 Hardware Key | TOTP | Highest privilege, hardware-backed security required |
| QA | FIDO2 Hardware Key | TOTP | Approval authority, hardware-backed security required |
| BILLING | TOTP | Push Notification | Standard security, frequent access |
| ADMIN | FIDO2 Hardware Key | TOTP + SMS (recovery only) | System-wide access, highest security required |
| AUDITOR | TOTP | Push Notification | Read-only access, standard security |
| VENDOR | TOTP | SMS (recovery only) | Limited scope, standard security |
Prohibited MFA Methods:
- Email-based OTP (email not considered secure channel per NIST SP 800-63B)
- Security questions (not cryptographically secure)
- Single-factor passwordless (e.g., magic links without second factor)
3.3 MFA Enrollment Workflow
Initial Enrollment (Mandatory for all new users):
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 1: Account Creation │
│ • User receives account invitation email (signed, tamper-evident) │
│ • Invitation link expires after 24 hours │
│ • User clicks link, redirected to enrollment portal │
└──────────────────┬──────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 2: Password Setup │
│ • User creates password (PBKDF2-HMAC-SHA256, 600k iterations) │
│ • Password requirements: 14+ chars, upper+lower+digit+symbol │
│ • Password strength meter displayed (zxcvbn library) │
└──────────────────┬──────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 3: MFA Method Selection │
│ • User selects primary MFA method (TOTP/FIDO2/Push) │
│ • System displays QR code (TOTP) or prompts hardware key (FIDO2) │
│ • User scans QR code with authenticator app or inserts hardware key│
└──────────────────┬──────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 4: MFA Verification │
│ • User enters first TOTP code or completes FIDO2 challenge │
│ • System validates code/challenge within 30-second window │
│ • If validation fails, allow 2 retries before locking account │
└──────────────────┬──────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 5: Backup Codes Generation │
│ • System generates 10 single-use backup codes (16-digit) │
│ • User MUST download backup codes (PDF, encrypted with user pw) │
│ • User acknowledges backup codes saved securely │
└──────────────────┬──────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 6: Enrollment Complete │
│ • User logged in, MFA status: ENROLLED │
│ • Audit log entry: USER_MFA_ENROLLED (includes method type) │
│ • User redirected to dashboard │
└─────────────────────────────────────────────────────────────────────┘
MFA Enrollment Enforcement:
- Users without MFA CANNOT access any PHI-containing pages/APIs
- Login redirects to MFA enrollment page until complete
- Grace period: 7 days for existing users (legacy migration), 0 days for new users
- Bypass: NONE (no exceptions, including executives)
3.4 Adaptive MFA (Risk-Based Step-Up)
Trigger Conditions for Step-Up Authentication:
| Risk Indicator | Detection Method | Action | Example |
|---|---|---|---|
| New Device | Device fingerprint (User-Agent + canvas hash + screen resolution) | Require MFA re-authentication | User logs in from new laptop → step-up required |
| Unusual Location | GeoIP detection (>100 miles from previous login) | Require MFA + email notification | User in New York logs in from California → step-up + alert |
| Sensitive Operation | PHI bulk export (>100 records), SSN access, break-glass activation | Require MFA re-authentication within 5 minutes | User attempts to export patient list → step-up required |
| Failed Login Attempts | 3+ failed logins in 15 minutes | Require CAPTCHA + MFA | Potential brute-force attack → enhanced verification |
| Impossible Travel | Login from geographically distant locations <1 hour apart | Block login + security review | User in NYC at 9am, login from London at 9:15am → blocked |
| Tor/VPN/Proxy Detected | IP reputation check (IPQualityScore API) | Require MFA + manager approval | User accesses via Tor exit node → blocked pending approval |
Step-Up Authentication Flow:
# Example step-up logic (Python pseudocode)
def check_adaptive_mfa(user: User, request: HttpRequest) -> MFARequirement:
risk_score = 0
# Device fingerprint check
device_hash = hash_device_fingerprint(request)
if device_hash not in user.known_devices:
risk_score += 30
log_event('NEW_DEVICE_DETECTED', user, device_hash)
# GeoIP check
current_location = geoip_lookup(request.ip_address)
last_location = user.last_login_location
distance_miles = calculate_distance(current_location, last_location)
if distance_miles > 100:
risk_score += 20
log_event('UNUSUAL_LOCATION', user, f'{distance_miles} miles from previous login')
# Impossible travel check
time_since_last_login = (datetime.utcnow() - user.last_login_time).seconds / 3600
travel_speed = distance_miles / time_since_last_login if time_since_last_login > 0 else 0
if travel_speed > 600: # Faster than commercial flight
risk_score = 100 # Immediate block
log_event('IMPOSSIBLE_TRAVEL', user, f'{travel_speed} mph calculated')
# IP reputation check
ip_reputation = check_ip_reputation(request.ip_address)
if ip_reputation.is_vpn or ip_reputation.is_proxy or ip_reputation.is_tor:
risk_score += 40
log_event('VPN_PROXY_DETECTED', user, ip_reputation)
# Determine MFA requirement
if risk_score >= 100:
return MFARequirement.BLOCK # Block login, require security review
elif risk_score >= 50:
return MFARequirement.STEP_UP # Require MFA re-authentication
elif risk_score >= 20:
return MFARequirement.EMAIL_NOTIFICATION # Allow login, send notification
else:
return MFARequirement.STANDARD # Standard MFA (no step-up)
3.5 MFA for API Access
Service-to-Service Authentication (equivalent to MFA for non-human entities):
| Authentication Factor | Technology | Purpose | Rotation Period |
|---|---|---|---|
| Factor 1: Service Identity | mTLS client certificate (ECDSA P-256) | Cryptographically proves service identity | 90 days (automated via cert-manager) |
| Factor 2: API Key | HMAC-SHA256 signed API key with KMS secret | Proves possession of secret | 90 days (automated via Vault dynamic secrets) |
| Factor 3 (Optional): JWT Token | Short-lived JWT (1-hour expiration) signed with KMS key | Time-bound authorization | 1 hour (refresh token rotation on use) |
API MFA Workflow:
Client Service → API Gateway
1. TLS handshake: Client presents mTLS certificate (Factor 1)
2. API Gateway validates certificate against internal CA
3. HTTP request includes API-Key header (Factor 2)
4. API Gateway validates API key signature with KMS
5. Optional: JWT token in Authorization header (Factor 3)
6. All factors valid → request forwarded to backend service
7. Any factor invalid → 401 Unauthorized + audit log entry
API Key Rotation Procedure:
#!/bin/bash
# Automated API key rotation (runs every 90 days via cron)
SERVICE_NAME="work-order-api"
NEW_API_KEY=$(vault write -field=api_key secret/api-keys/$SERVICE_NAME rotation=true)
# Update service configuration (Kubernetes Secret)
kubectl create secret generic ${SERVICE_NAME}-api-key \
--from-literal=api-key=$NEW_API_KEY \
--dry-run=client -o yaml | kubectl apply -f -
# Restart service to pick up new key (zero-downtime rolling restart)
kubectl rollout restart deployment/$SERVICE_NAME
# Audit log entry
echo "$(date -Iseconds) - API_KEY_ROTATED - service=$SERVICE_NAME" >> /var/log/api-key-rotation.log
3.6 MFA Recovery Procedures
Lost Device Scenarios:
| Scenario | Recovery Method | Approval Required | Timeframe |
|---|---|---|---|
| Lost TOTP Device (backup codes available) | User enters backup code from saved list | None | Immediate |
| Lost TOTP Device (no backup codes) | Manager submits recovery request via help desk | Manager approval + Security team verification | 4 hours (business hours) |
| Lost FIDO2 Hardware Key | User authenticates with backup TOTP, orders new key | None (if backup enrolled) | Immediate (new key: 3-5 days shipping) |
| Lost Phone (SMS fallback) | Temporary SMS OTP to registered phone number | Security team approval (verify identity) | 2 hours |
| Complete MFA Loss (no backups) | In-person identity verification at facility | CISO approval + government ID verification | 24 hours |
MFA Recovery Request Form:
# MFA Recovery Request
**Request ID:** MFA-REC-YYYY-NNNN
**User:** [Full Name, User ID]
**Date:** [YYYY-MM-DD]
**Requestor Role:** [CLINICAL / ADMIN / etc.]
## Reason for Recovery
- [ ] Lost TOTP device (no backup codes)
- [ ] Lost FIDO2 hardware key (no backup method)
- [ ] Lost phone (SMS recovery unavailable)
- [ ] Other: [Describe]
## Identity Verification
- **Government ID Verified:** [ ] Yes [ ] No (attach photo of ID)
- **Manager Confirmation:** [Manager name, signature, date]
- **Known Device Verification:** [ ] User accessing from known device (approved without additional verification)
- **Security Questions:** [Answer 3 pre-configured questions]
## Temporary Access Method
- [ ] SMS OTP to registered phone: [Phone number]
- [ ] Backup TOTP enrollment (new QR code)
- [ ] Temporary FIDO2 key issued (return required)
## Approval
| Role | Name | Signature | Date |
|------|------|-----------|------|
| Manager | | | |
| Security Team | | | |
| CISO (if required) | | | |
## Audit Trail
- Recovery initiated: [Timestamp]
- Temporary access granted: [Timestamp]
- Permanent MFA re-enrolled: [Timestamp]
- Temporary access revoked: [Timestamp]
3.7 MFA Audit and Monitoring
Logged MFA Events:
| Event | Audit Level | Logged Attributes | Retention |
|---|---|---|---|
MFA_ENROLLED | L2 | user_id, mfa_method, timestamp, enrollment_ip | 7 years |
MFA_AUTHENTICATION_SUCCESS | L1 | user_id, mfa_method, timestamp, ip_address | 1 year (sampled at 10%) |
MFA_AUTHENTICATION_FAILURE | L2 | user_id, mfa_method, failure_reason, timestamp, ip_address | 7 years |
MFA_BACKUP_CODE_USED | L3 | user_id, timestamp, ip_address, remaining_codes | 7 years |
MFA_RECOVERY_REQUESTED | L3 | user_id, recovery_reason, approver, timestamp | 10 years |
MFA_METHOD_CHANGED | L3 | user_id, old_method, new_method, timestamp | 7 years |
MFA_STEP_UP_TRIGGERED | L2 | user_id, risk_reason, timestamp | 7 years |
Anomaly Detection Alerts:
| Alert Condition | Threshold | Action | Notification |
|---|---|---|---|
| High MFA Failure Rate | >5 failures in 15 minutes (single user) | Lock account for 15 minutes | User + Security Team |
| Backup Code Exhaustion | <3 backup codes remaining | Prompt user to regenerate | User |
| Unusual MFA Method Change | User changes from FIDO2 to SMS (downgrade) | Require manager approval | Manager + Security Team |
| MFA Bypass Attempt | API call without MFA token | Block request + P1 alert | Security Team + CISO |
4. Minimum Necessary Principle Implementation
4.1 Regulatory Requirement
HIPAA Privacy Rule §164.514(d)(3):
"A covered entity must limit the protected health information disclosed to the amount reasonably necessary to achieve the purpose of the disclosure."
BIO-QMS Implementation Strategy:
- Role-Based Field Filtering: Users see only PHI fields required for their job function
- Query-Level Enforcement: Database queries automatically filter PHI based on role
- Dynamic Masking: PHI fields masked/redacted in UI based on role authorization
- Purpose-Based Access: PHI access requires explicit purpose (e.g., "patient care", "billing", "quality review")
4.2 Role-Based PHI Access Matrix
| Role | PHI Access Scope | Accessible Fields | Restricted Fields | Use Cases |
|---|---|---|---|---|
| CLINICAL (Assigned Provider) | Full PHI for assigned patients only | All PHI fields (18 identifiers + clinical notes) | Other providers' patients (unless shared care) | Direct patient care, treatment documentation |
| CLINICAL (Unassigned) | De-identified data only | Work order metadata (no names/MRN/DOB) | All direct identifiers | Coverage, consultations (requires explicit sharing) |
| BILLING | Limited PHI for billing purposes | Name, DOB, MRN, SSN (last 4 only), insurance numbers | Clinical notes, diagnosis details, photos | Insurance claims, payment processing |
| QA (Quality Assurance) | Limited PHI for compliance review | Name, MRN, service dates, work order details | Contact info (phone/email), full SSN, photos | Audit compliance, quality metrics |
| SYSTEM_OWNER | Limited PHI for asset management | Work order descriptions (redacted), service dates | Patient names, contact info, clinical details | Equipment maintenance, change control |
| ADMIN (IT Support) | No PHI access (masked in support tools) | None (all PHI fields masked with "PHI") | All PHI fields | System troubleshooting, database maintenance |
| AUDITOR | Read-only PHI (all fields) | All PHI fields (for assigned audit scope) | No write access | Regulatory audits, compliance reviews |
| VENDOR | No PHI access | Equipment serial numbers, facility codes (no patient link) | All PHI fields | Equipment service, IQ/OQ testing |
Field-Level Masking Examples:
| Field | CLINICAL | BILLING | QA | ADMIN |
|---|---|---|---|---|
patient.full_name | "John Doe" | "John Doe" | "John Doe" | "PHI" |
patient.date_of_birth | "1985-03-15" | "1985-03-15" | "1985-XX-XX" (year only) | "PHI" |
patient.ssn | "*--6789" (last 4) | "*--6789" (last 4) | "--***" (masked) | "PHI" |
patient.phone_number | "+1-555-123-4567" | "+1-555-XXX-XXXX" (masked) | "PHI" | "PHI" |
patient.email | "john.doe@example.com" | "PHI" | "PHI" | "PHI" |
clinical_notes.text | "Patient reports headache..." | "PHI" | "Patient reports headache..." (first 100 chars) | "PHI" |
4.3 Query-Level Enforcement (Database Middleware)
PostgreSQL Row-Level Security (RLS) + Application Middleware:
-- Example RLS policy: CLINICAL role sees only assigned patients
CREATE POLICY clinical_assigned_patients ON patients
FOR SELECT
USING (
patient_id IN (
SELECT patient_id FROM work_orders
WHERE assignee_id = current_setting('app.user_id')::uuid
)
OR patient_id IN (
SELECT patient_id FROM patient_care_team
WHERE provider_id = current_setting('app.user_id')::uuid
)
);
-- Example RLS policy: BILLING role sees limited fields
-- (Note: Field-level RLS requires views with SECURITY DEFINER functions)
CREATE VIEW patients_billing_view AS
SELECT
patient_id,
full_name,
date_of_birth,
mrn,
substring(ssn, 8, 4) AS ssn_last4, -- Last 4 digits only
'***PHI***'::text AS phone_number, -- Masked
'***PHI***'::text AS email -- Masked
FROM patients;
GRANT SELECT ON patients_billing_view TO billing_role;
Application Middleware (TypeScript):
// Example: PHI field filtering middleware
import { Request, Response, NextFunction } from 'express';
interface PHIFieldConfig {
[role: string]: {
allowedFields: string[];
maskedFields: { [field: string]: (value: any) => string };
};
}
const PHI_ACCESS_CONFIG: PHIFieldConfig = {
CLINICAL: {
allowedFields: ['patient_id', 'full_name', 'date_of_birth', 'mrn', 'phone_number', 'email', 'ssn', 'clinical_notes'],
maskedFields: {
ssn: (value) => `***-**-${value.slice(-4)}`, // Last 4 only
},
},
BILLING: {
allowedFields: ['patient_id', 'full_name', 'date_of_birth', 'mrn'],
maskedFields: {
ssn: (value) => `***-**-${value.slice(-4)}`,
phone_number: () => '***PHI***',
email: () => '***PHI***',
},
},
ADMIN: {
allowedFields: [], // No PHI access
maskedFields: {
'*': () => '***PHI***', // Mask all fields
},
},
};
export function filterPHI(role: string) {
return (req: Request, res: Response, next: NextFunction) => {
const originalJson = res.json.bind(res);
res.json = (data: any) => {
const config = PHI_ACCESS_CONFIG[role];
if (!config) {
// No config for role → deny all PHI
data = maskAllPHI(data);
} else {
data = applyFieldFiltering(data, config);
}
// Audit log: PHI fields accessed
auditPHIAccess(req.user.id, role, extractPHIFields(data), req.path);
return originalJson(data);
};
next();
};
}
function applyFieldFiltering(data: any, config: PHIFieldConfig[string]): any {
if (Array.isArray(data)) {
return data.map(item => applyFieldFiltering(item, config));
}
if (typeof data === 'object' && data !== null) {
const filtered: any = {};
for (const [key, value] of Object.entries(data)) {
if (config.allowedFields.includes(key)) {
// Field allowed, check if masking required
if (config.maskedFields[key]) {
filtered[key] = config.maskedFields[key](value);
} else {
filtered[key] = value;
}
} else if (config.maskedFields['*']) {
// Wildcard mask (e.g., ADMIN role masks all)
filtered[key] = config.maskedFields['*'](value);
}
// Omit fields not in allowedFields (minimum necessary)
}
return filtered;
}
return data;
}
4.4 Purpose-Based Access Control
Access Purpose Categories:
| Purpose Code | Description | Requires Patient Consent | Audit Retention |
|---|---|---|---|
| TREATMENT | Direct patient care, diagnosis, treatment planning | No (TPO exception) | 7 years |
| PAYMENT | Billing, insurance claims, payment processing | No (TPO exception) | 7 years |
| OPERATIONS | Quality improvement, compliance audits, training | No (TPO exception) | 7 years |
| RESEARCH | Clinical research, de-identified analytics | Yes (IRB approval + patient consent) | 10 years |
| MARKETING | Promotional communications, fundraising | Yes (opt-in consent required) | 7 years |
| DISCLOSURE | Third-party disclosure (e.g., legal, public health) | Yes (unless required by law) | 10 years |
| EMERGENCY | Break-glass emergency access | No (emergency exception) | 10 years + enhanced review |
Purpose Enforcement Workflow:
User Requests PHI Access
↓
System Prompts: "What is the purpose of accessing this patient's PHI?"
↓
User Selects Purpose (dropdown): [TREATMENT | PAYMENT | OPERATIONS | RESEARCH | OTHER]
↓
If purpose = RESEARCH or MARKETING or DISCLOSURE:
→ System checks for patient consent record
→ If no consent: DENY access + prompt to obtain consent
↓
If purpose = EMERGENCY (break-glass):
→ Trigger break-glass workflow (Section 6)
↓
System logs purpose in audit trail:
- user_id, patient_id, purpose_code, timestamp, access_justification (free text)
↓
Access granted with minimum necessary fields for stated purpose
Purpose-Based Field Filtering:
| Purpose | Accessible PHI Fields | Rationale |
|---|---|---|
| TREATMENT | All clinical fields (names, DOB, MRN, clinical notes, lab results) | Comprehensive care requires full clinical context |
| PAYMENT | Name, DOB, MRN, insurance numbers, service dates, billing codes | Minimum needed for insurance claims |
| OPERATIONS | De-identified data preferred; if identified, limited to MRN + service dates | Quality metrics rarely require direct identifiers |
| RESEARCH | De-identified data REQUIRED (unless IRB waiver + patient consent) | HIPAA de-identification safe harbor or expert determination |
| EMERGENCY | All PHI fields (no restrictions during active emergency) | Life-threatening situations override minimum necessary |
4.5 PHI Access Request Workflow (Non-Routine Access)
When Required: User needs PHI access outside normal role scope (e.g., admin troubleshooting, cross-department consult)
Request Process:
# PHI Access Request Form
**Request ID:** PHI-REQ-YYYY-NNNN
**Requestor:** [Name, User ID, Current Role]
**Date:** [YYYY-MM-DD]
## Access Details
- **Patient(s):** [Patient MRN or "All patients in [department]"]
- **PHI Fields Requested:** [List specific fields, e.g., "name, DOB, MRN"]
- **Access Level:** [ ] Read-only [ ] Read-write
- **Duration:** [Start date] to [End date] (max 90 days)
## Justification
- **Purpose:** [ ] Treatment [ ] Payment [ ] Operations [ ] Research [ ] Other
- **Business Justification:** [Explain why access is necessary]
- **Minimum Necessary Analysis:** [Why broader access cannot meet need]
## Approval Chain
| Approver | Role | Signature | Date |
|----------|------|-----------|------|
| Direct Manager | | | |
| Privacy Officer | | | (Required for non-TPO purposes) |
| CISO | | | (Required for admin PHI access) |
## System Action
- [ ] Temporary role elevation granted (duration: [X] days)
- [ ] Enhanced audit logging enabled (all access logged at L3)
- [ ] Access automatically revoked on [expiration date]
- [ ] Post-access review scheduled (30 days after expiration)
Automated Expiration:
- Access grants expire automatically on end date (no manual revocation needed)
- User receives email notification 7 days before expiration
- Access review triggered 30 days post-expiration (verify no unauthorized retention)
5. Session Management and Timeout Controls
5.1 Session Timeout Requirements
HIPAA §164.312(a)(2)(iii) — Automatic Logoff (Addressable, IMPLEMENTED):
| Parameter | Value | Rationale | Configurable Range |
|---|---|---|---|
| Idle Timeout | 15 minutes (default) | Balance security and workflow efficiency per HHS guidance | 10-30 minutes (per role) |
| Absolute Timeout | 8 hours (shift-based) | Prevent session hijacking for long-running sessions | 4-12 hours (per role) |
| Grace Warning | 2 minutes before timeout | User experience (allow save before disconnect) | 1-5 minutes |
| Concurrent Sessions | 1 active PHI session per user | Prevent credential sharing | 1-3 (per role) |
| Re-authentication for Sensitive Ops | 5 minutes (signatures, break-glass) | FDA Part 11 §11.100(b) requirement | Non-configurable |
Role-Specific Timeout Configuration:
| Role | Idle Timeout | Absolute Timeout | Concurrent Sessions | Rationale |
|---|---|---|---|---|
| CLINICAL | 15 minutes | 12 hours (long shifts) | 2 (desktop + mobile) | Clinical workflow flexibility |
| BILLING | 20 minutes | 8 hours | 1 | Less sensitive than clinical, standard office hours |
| QA | 10 minutes | 8 hours | 1 | Compliance role, shorter timeout appropriate |
| ADMIN | 10 minutes | 4 hours | 1 | High privilege, shortest timeout |
| AUDITOR | 30 minutes | 8 hours | 1 | Read-only access, longer timeout acceptable |
| VENDOR | 15 minutes | 4 hours | 1 | External user, shorter timeout appropriate |
5.2 Session Token Security
JWT Session Token Configuration:
{
"token_type": "JWT",
"algorithm": "RS256",
"signing_key": "KMS-managed RSA-2048 key",
"claims": {
"iss": "https://auth.coditect-bio-qms.com",
"sub": "user-uuid",
"aud": "https://api.coditect-bio-qms.com",
"exp": 3600, // 1 hour expiration
"iat": 1708095120,
"nbf": 1708095120,
"jti": "unique-token-id",
"custom_claims": {
"role": "CLINICAL",
"tenant_id": "tenant-uuid",
"mfa_verified": true,
"mfa_timestamp": 1708095120,
"session_id": "session-uuid",
"device_fingerprint": "hash-of-device"
}
},
"http_security": {
"cookie_flags": ["HttpOnly", "Secure", "SameSite=Strict"],
"csrf_token": "separate-csrf-token-in-header",
"token_binding": "TLS-channel-id" // Bind token to TLS session
}
}
Token Rotation Strategy:
| Token Type | Lifetime | Rotation Trigger | Refresh Mechanism |
|---|---|---|---|
| Access Token (JWT) | 1 hour | Expiration | Refresh token exchange (no re-authentication required) |
| Refresh Token | 7 days | Each use | New refresh token issued on each refresh (single-use, rotating) |
| Session Cookie | Session (browser close) | Logout | New session on next login |
| CSRF Token | Session | Session change | Regenerated on privilege escalation |
Refresh Token Flow:
User Login (MFA verified)
→ System issues Access Token (1hr) + Refresh Token (7d)
↓
Access Token expires after 1 hour
→ Client sends Refresh Token to /auth/refresh endpoint
↓
System validates Refresh Token:
- Token signature valid (KMS verification)
- Token not revoked (check revocation list in Redis)
- Token not expired (7-day lifetime)
- Device fingerprint matches (prevent token theft)
↓
If valid:
→ Issue new Access Token (1hr) + new Refresh Token (7d, single-use)
→ Revoke old Refresh Token (add to Redis revocation list)
↓
If invalid:
→ Require full re-authentication (username + password + MFA)
5.3 Idle Detection and Grace Warning
Client-Side Idle Detection (JavaScript):
// Session idle timeout manager
class SessionIdleManager {
private idleTimeoutMs: number;
private graceWarningMs: number;
private idleTimer: number | null = null;
private warningTimer: number | null = null;
private lastActivityTime: number = Date.now();
constructor(idleTimeoutMinutes: number, graceWarningMinutes: number) {
this.idleTimeoutMs = idleTimeoutMinutes * 60 * 1000;
this.graceWarningMs = graceWarningMinutes * 60 * 1000;
// Monitor user activity
['mousedown', 'keypress', 'scroll', 'touchstart'].forEach(event => {
document.addEventListener(event, () => this.resetIdleTimer(), true);
});
this.resetIdleTimer();
}
private resetIdleTimer() {
this.lastActivityTime = Date.now();
// Clear existing timers
if (this.idleTimer) clearTimeout(this.idleTimer);
if (this.warningTimer) clearTimeout(this.warningTimer);
// Set warning timer (e.g., 13 minutes for 15-minute timeout)
this.warningTimer = window.setTimeout(() => {
this.showGraceWarning();
}, this.idleTimeoutMs - this.graceWarningMs);
// Set idle timeout timer (e.g., 15 minutes)
this.idleTimer = window.setTimeout(() => {
this.handleIdleTimeout();
}, this.idleTimeoutMs);
}
private showGraceWarning() {
const remainingSeconds = Math.floor(this.graceWarningMs / 1000);
// Display modal warning
const modal = document.createElement('div');
modal.className = 'session-timeout-warning';
modal.innerHTML = `
<div class="modal-content">
<h3>Session Expiring Soon</h3>
<p>Your session will expire in <strong id="countdown">${remainingSeconds}</strong> seconds due to inactivity.</p>
<p>Any unsaved work will be lost.</p>
<button id="stay-logged-in">Stay Logged In</button>
<button id="logout-now">Logout Now</button>
</div>
`;
document.body.appendChild(modal);
// Countdown timer
let countdown = remainingSeconds;
const interval = setInterval(() => {
countdown--;
document.getElementById('countdown')!.textContent = countdown.toString();
if (countdown <= 0) clearInterval(interval);
}, 1000);
// Button handlers
document.getElementById('stay-logged-in')!.addEventListener('click', () => {
this.resetIdleTimer();
document.body.removeChild(modal);
clearInterval(interval);
// Extend server-side session
fetch('/auth/extend-session', { method: 'POST' });
});
document.getElementById('logout-now')!.addEventListener('click', () => {
this.logout();
});
}
private handleIdleTimeout() {
// Log user out
this.logout();
// Redirect to login page with timeout message
window.location.href = '/login?reason=idle_timeout';
}
private logout() {
// Call logout API
fetch('/auth/logout', { method: 'POST' });
// Clear local storage
localStorage.clear();
sessionStorage.clear();
// Audit log
console.log('Session terminated due to idle timeout');
}
}
// Initialize on page load with role-specific timeout
const userRole = getCurrentUserRole(); // From API
const idleTimeout = getRoleIdleTimeout(userRole); // e.g., 15 minutes for CLINICAL
const sessionManager = new SessionIdleManager(idleTimeout, 2); // 2-minute grace warning
Server-Side Session Validation:
# Example: Server-side session timeout enforcement (Python/Django)
from datetime import datetime, timedelta
from django.contrib.sessions.models import Session
from django.core.cache import cache
def check_session_timeout(request):
"""Enforce idle and absolute timeout on server side"""
session_id = request.session.session_key
user = request.user
# Get session metadata from cache (Redis)
session_meta = cache.get(f'session:{session_id}')
if not session_meta:
# Session not found or expired
return logout_user(request, reason='session_expired')
# Check idle timeout
last_activity = datetime.fromisoformat(session_meta['last_activity'])
idle_timeout_minutes = get_role_idle_timeout(user.role)
if datetime.utcnow() - last_activity > timedelta(minutes=idle_timeout_minutes):
audit_log('SESSION_IDLE_TIMEOUT', user.id, session_id, f'{idle_timeout_minutes} minutes')
return logout_user(request, reason='idle_timeout')
# Check absolute timeout
session_start = datetime.fromisoformat(session_meta['session_start'])
absolute_timeout_hours = get_role_absolute_timeout(user.role)
if datetime.utcnow() - session_start > timedelta(hours=absolute_timeout_hours):
audit_log('SESSION_ABSOLUTE_TIMEOUT', user.id, session_id, f'{absolute_timeout_hours} hours')
return logout_user(request, reason='absolute_timeout')
# Update last activity timestamp
session_meta['last_activity'] = datetime.utcnow().isoformat()
cache.set(f'session:{session_id}', session_meta, timeout=86400) # 24 hour cache
return True # Session valid
def logout_user(request, reason):
"""Logout user and audit log"""
user_id = request.user.id
session_id = request.session.session_key
# Audit log
audit_log('SESSION_TERMINATED', user_id, session_id, reason)
# Revoke refresh tokens
RefreshToken.objects.filter(user=request.user).delete()
# Clear session
request.session.flush()
# Return 401 Unauthorized (client will redirect to login)
from django.http import JsonResponse
return JsonResponse({'error': 'Session expired', 'reason': reason}, status=401)
5.4 Concurrent Session Limits
Enforcement Strategy:
| Scenario | Action | User Experience |
|---|---|---|
| User logs in from 2nd device (limit=1) | Terminate oldest session, send notification to old device | Old device: "You have been logged out because you logged in from another device" |
| User logs in from 3rd device (limit=2) | Terminate oldest session, keep 2 most recent | Users manage their own active sessions via "Active Sessions" page |
| Suspicious concurrent sessions (different geographies) | Require MFA re-authentication on all sessions, alert security team | Modal: "Unusual activity detected. Please verify your identity." |
Active Sessions Management UI:
User Profile → Active Sessions
┌─────────────────────────────────────────────────────────────────────┐
│ Active Sessions (2/2) │
├─────────────────────────────────────────────────────────────────────┤
│ 1. Current Session (this device) │
│ • Device: Chrome 121.0 on macOS │
│ • Location: New York, NY, USA │
│ • IP: 192.168.1.100 │
│ • Started: 2026-02-16 08:30 AM │
│ • Last Activity: 2026-02-16 10:15 AM │
│ [THIS DEVICE] │
│ │
│ 2. Other Session │
│ • Device: Safari 17.2 on iOS 17.3 │
│ • Location: New York, NY, USA │
│ • IP: 192.168.1.101 │
│ • Started: 2026-02-16 07:00 AM │
│ • Last Activity: 2026-02-16 09:45 AM │
│ [TERMINATE SESSION] ← User can manually terminate │
└─────────────────────────────────────────────────────────────────────┘
6. Break-Glass Emergency Access
6.1 Emergency Access Policy
Purpose: Provide temporary elevated PHI access during life-threatening emergencies or disaster scenarios when normal authorization channels are unavailable.
Regulatory Basis:
- HIPAA §164.312(a)(2)(ii) — Emergency Access Procedure (Required)
- HIPAA §164.308(a)(4) — Access Authorization
Scope:
- Individual Patient Emergency: Clinician needs immediate access to specific patient's PHI (e.g., unconscious patient, no normal access authorization)
- Mass Casualty/Disaster: System-wide PHI access needed during disaster response (e.g., hospital evacuation, mass shooting)
Prohibited Uses:
- Convenience access (forgot password, normal workflow delays)
- Non-emergency troubleshooting
- Routine after-hours access
6.2 Break-Glass Workflow
Individual Patient Emergency Access:
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 1: Emergency Declaration │
│ • User clicks "Emergency Access" button (prominent red UI) │
│ • System displays warning: "EMERGENCY ACCESS — AUDITED AND REVIEWED"│
│ • User must declare emergency reason (dropdown + free text): │
│ [ ] Life-threatening patient condition │
│ [ ] Unconscious patient (no consent possible) │
│ [ ] Disaster/mass casualty event │
│ [ ] Other (requires detailed justification) │
│ • User acknowledges: "I understand this access will be reviewed by │
│ the Privacy Officer and inappropriate use may result in sanctions"│
└──────────────────┬──────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 2: MFA Re-Authentication │
│ • User MUST re-authenticate with MFA (no bypass) │
│ • TOTP code or FIDO2 challenge required │
│ • Failed MFA → deny break-glass access + P1 alert │
└──────────────────┬──────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 3: Temporary Access Grant │
│ • System grants PHI access for specified patient (individual) or │
│ all patients in facility (disaster) │
│ • Access level: Read + Write (clinical context required) │
│ • Duration: 4 hours (auto-expire, non-renewable) │
│ • Enhanced audit logging: Separate "break_glass_audit" stream │
└──────────────────┬──────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 4: Immediate Notifications │
│ • Privacy Officer: Email + SMS alert (immediate) │
│ • Security Team: PagerDuty alert (P2 severity) │
│ • User's Manager: Email notification (informational) │
│ • Notification includes: User, patient, reason, timestamp │
└──────────────────┬──────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 5: Access Expires Automatically │
│ • After 4 hours: Access automatically revoked │
│ • User receives 15-minute warning before expiration │
│ • No extension possible (must re-declare emergency if ongoing) │
└──────────────────┬──────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 6: Post-Access Review (within 24 hours) │
│ • Privacy Officer reviews access justification │
│ • Audit trail includes: All PHI fields accessed, timestamps, actions│
│ • Review outcome: │
│ → JUSTIFIED: Document as appropriate emergency use │
│ → UNJUSTIFIED: Security incident + disciplinary action │
│ • Review documented in compliance system │
└─────────────────────────────────────────────────────────────────────┘
Mass Casualty/Disaster Access (System-Wide):
| Trigger | Authorization | Access Scope | Duration | Review |
|---|---|---|---|---|
| Facility Evacuation | Incident Commander declaration | All patients in affected facility | 12 hours | Post-incident review (72 hours) |
| Mass Shooting/Terrorism | Incident Commander + CISO approval | All patients system-wide | 24 hours | Post-incident review (7 days) |
| Natural Disaster (Hurricane, Earthquake) | Incident Commander + Executive approval | All patients in affected region | 48 hours (renewable with approval) | Post-incident review (30 days) |
| Cyberattack/System Outage | CISO + CTO approval | Minimum necessary for continuity of care | Until system restored | Post-incident review (72 hours) |
6.3 Break-Glass Access Types
| Access Type | Scenario | Granted Permissions | Typical Use Cases |
|---|---|---|---|
| Individual Patient Read-Only | Provider needs to review patient history (emergency consult) | View all PHI fields for 1 patient, no write access | ER physician consulting on unfamiliar patient |
| Individual Patient Read-Write | Provider treating emergency patient | View + Edit all PHI fields for 1 patient | Unconscious patient, no advance directive accessible |
| Department-Wide Read-Only | Disaster drill, quality review | View PHI for all patients in department | Mass casualty triage, evacuation preparation |
| System-Wide Read-Write | Active disaster response | View + Edit PHI for all patients system-wide | Hospital evacuation, EHR system failure |
6.4 Break-Glass Audit Trail
Enhanced Logging Requirements:
| Audit Field | Required | Example | Purpose |
|---|---|---|---|
event_type | Yes | BREAK_GLASS_ACTIVATED | Differentiate from normal access |
user_id | Yes | U-8472 | Who activated break-glass |
patient_id | Yes (individual) | P-12345 or * (system-wide) | Which patient(s) accessed |
emergency_reason_code | Yes | LIFE_THREATENING | Categorized reason |
emergency_justification | Yes | "Patient unconscious, no family contact, severe allergic reaction" | Free-text explanation |
access_scope | Yes | INDIVIDUAL_READ_WRITE | Type of access granted |
access_duration | Yes | 4 hours | How long access valid |
mfa_verified | Yes | true | Confirm MFA completed |
notification_sent | Yes | privacy_officer@coditect.ai, security_team@coditect.ai | Who was notified |
phi_fields_accessed | Yes | ["full_name", "date_of_birth", "clinical_notes", "medications"] | Specific PHI accessed |
access_timestamp | Yes | 2026-02-16T14:32:18.123456Z | When access occurred |
expiration_timestamp | Yes | 2026-02-16T18:32:18.123456Z | When access expires |
review_status | Yes | PENDING | Tracking review completion |
review_outcome | No (added post-review) | JUSTIFIED or UNJUSTIFIED | Privacy Officer determination |
reviewer_id | No (added post-review) | U-9999 | Who reviewed |
review_timestamp | No (added post-review) | 2026-02-17T08:00:00.000000Z | When reviewed |
Separate Audit Stream:
// Example break-glass audit log entry
{
"entry_id": "BG-2026-02-16-00042",
"event_type": "BREAK_GLASS_ACTIVATED",
"timestamp": "2026-02-16T14:32:18.123456Z",
"user_id": "U-8472",
"user_role": "CLINICAL",
"patient_id": "P-12345",
"emergency_reason_code": "LIFE_THREATENING",
"emergency_justification": "Patient unconscious after severe allergic reaction. No family contact available. Need to review medication history to avoid drug interactions.",
"access_scope": "INDIVIDUAL_READ_WRITE",
"access_duration_hours": 4,
"expiration_timestamp": "2026-02-16T18:32:18.123456Z",
"mfa_verified": true,
"mfa_method": "TOTP",
"notification_sent": [
"privacy_officer@coditect.ai",
"security_team@coditect.ai",
"manager_john_doe@coditect.ai"
],
"phi_fields_accessed": [
"full_name",
"date_of_birth",
"mrn",
"clinical_notes",
"medications",
"allergies",
"lab_results"
],
"review_status": "PENDING",
"review_deadline": "2026-02-17T14:32:18.123456Z" // 24 hours
}
6.5 Break-Glass Metrics Dashboard
Privacy Officer Dashboard (Real-Time Monitoring):
Break-Glass Access Monitoring — Last 30 Days
┌─────────────────────────────────────────────────────────────────────┐
│ Summary Metrics │
├─────────────────────────────────────────────────────────────────────┤
│ • Total Break-Glass Events: 12 │
│ • Pending Review: 2 (within 24-hour window) │
│ • Justified: 9 (75%) │
│ • Unjustified: 1 (8.3%) → 1 disciplinary action initiated │
│ • Average Review Time: 6.2 hours │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Recent Events (Last 7 Days) │
├─────────────────────────────────────────────────────────────────────┤
│ 1. 2026-02-16 14:32 | Dr. Jane Smith | Patient P-12345 │
│ Reason: Life-threatening allergic reaction │
│ Status: ✅ JUSTIFIED (reviewed 2026-02-17 08:15) │
│ │
│ 2. 2026-02-15 22:10 | Nurse Bob Johnson | Patient P-67890 │
│ Reason: Unconscious patient, no consent │
│ Status: ⏳ PENDING REVIEW (due: 2026-02-16 22:10) │
│ │
│ 3. 2026-02-14 16:45 | Dr. Alice Williams | Patient P-11111 │
│ Reason: "Need to check something" │
│ Status: ❌ UNJUSTIFIED (reviewed 2026-02-15 09:00) │
│ Action: Disciplinary notice issued, training required │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Usage Patterns │
├─────────────────────────────────────────────────────────────────────┤
│ • Most Common Reason: Life-threatening condition (58%) │
│ • Most Common Department: Emergency Department (75%) │
│ • Peak Usage Time: 10pm - 6am (night shift) │
│ • Unjustified Rate Trend: 8% → 5% (improving with training) │
└─────────────────────────────────────────────────────────────────────┘
6.6 Break-Glass Review Procedures
Post-Access Review Checklist:
# Break-Glass Access Review
**Event ID:** BG-2026-02-16-00042
**Reviewer:** [Privacy Officer Name]
**Review Date:** [YYYY-MM-DD]
## Access Details
- **User:** Dr. Jane Smith (U-8472)
- **Patient:** John Doe (P-12345)
- **Date/Time:** 2026-02-16 14:32:18 UTC
- **Reason Stated:** "Patient unconscious after severe allergic reaction..."
- **Access Duration:** 4 hours (expired 2026-02-16 18:32:18)
## Review Questions
1. Was the emergency access justified based on the stated reason?
- [ ] Yes — Life-threatening situation, normal access not feasible
- [ ] No — Reason does not meet emergency criteria
- [ ] Unclear — Requires additional investigation
2. Did the user access only the minimum necessary PHI?
- [ ] Yes — Access appropriate for emergency treatment
- [ ] No — Excessive access beyond stated reason
- Review: PHI fields accessed were [list], appropriate for [stated reason]
3. Was there an alternative authorization method available?
- [ ] No — Emergency access was only option
- [ ] Yes — User could have obtained normal authorization via [method]
4. Did the user document the emergency access in clinical notes?
- [ ] Yes — Clinical notes include reference to break-glass access
- [ ] No — No documentation found (follow-up required)
5. Were there any policy violations or suspicious patterns?
- [ ] No violations detected
- [ ] Potential violation: [describe]
- Pattern check: User has [X] break-glass events in last 90 days
## Review Determination
- [ ] **JUSTIFIED** — Access appropriate, no further action required
- [ ] **UNJUSTIFIED** — Access inappropriate, initiate disciplinary process
- [ ] **REQUIRES INVESTIGATION** — Escalate to Security Team + HR
## Follow-Up Actions
- [ ] Document outcome in compliance system
- [ ] If unjustified: Notify manager and HR for disciplinary action
- [ ] If pattern detected: Recommend additional training
- [ ] Update break-glass metrics dashboard
## Privacy Officer Signature
**Signature:** [Digital Signature]
**Date:** [YYYY-MM-DD]
**Review Completion Time:** [X] hours after access
7. Access Review and Recertification
7.1 Quarterly Access Reviews
Objective: Ensure PHI access authorizations remain appropriate for job function (minimum necessary principle enforcement).
Review Frequency:
- Quarterly: All active users with PHI access
- Semi-Annual: Full recertification with role attestation
- Annual: Comprehensive audit with external auditor involvement
Review Process:
QUARTER END (Q1: March 31, Q2: June 30, Q3: Sept 30, Q4: Dec 31)
↓
System generates access review reports for each manager
↓
Manager receives email with user access list (PDF + CSV export)
↓
Manager reviews each user:
- Role still appropriate?
- PHI access still required for current job function?
- Any changes needed (role change, access revocation)?
↓
Manager submits review form (due: 15 days after quarter end)
↓
Privacy Officer audits manager reviews:
- Incomplete reviews flagged (escalate to manager's manager)
- High-risk roles (ADMIN, QA) reviewed directly by Privacy Officer
↓
System implements access changes:
- Revoked access → user notified, access removed within 24 hours
- Role changes → new role applied, audit log entry created
↓
Quarterly access review report published (Board of Directors)
Access Review Report Template:
# Quarterly Access Review — Q1 2026
**Review Period:** January 1, 2026 — March 31, 2026
**Report Date:** April 15, 2026
**Prepared By:** Privacy Officer
## Executive Summary
- Total Active Users with PHI Access: 247
- Users Reviewed: 247 (100%)
- Managers Completed Reviews: 18/18 (100%)
- Access Revoked: 12 users (departed employees)
- Role Changes: 8 users (department transfers)
- Stale Access Detected: 3 users (>90 days no login, access disabled)
- Exceptions Identified: 1 user (admin with PHI access, requires CISO approval)
## Review Compliance
| Department | Manager | Users | Review Complete | On Time |
|------------|---------|-------|-----------------|---------|
| Clinical Operations | Dr. Jane Smith | 45 | ✅ Yes | ✅ Yes |
| Billing | John Doe | 18 | ✅ Yes | ✅ Yes |
| Quality Assurance | Alice Johnson | 12 | ✅ Yes | ⚠️ Late (2 days) |
| IT/Admin | Bob Williams | 8 | ✅ Yes | ✅ Yes |
| ... | ... | ... | ... | ... |
## Access Changes Summary
| User | Previous Role | New Role | Reason | Effective Date |
|------|--------------|----------|--------|----------------|
| Sarah Lee | CLINICAL | BILLING | Department transfer | 2026-04-01 |
| Mike Chen | BILLING | (Revoked) | Termination | 2026-03-15 |
| ... | ... | ... | ... | ... |
## Stale Access Detection
| User | Last Login | Days Inactive | Action |
|------|-----------|---------------|--------|
| Tom Brown | 2025-12-20 | 102 days | PHI access disabled, notified manager |
| ... | ... | ... | ... |
## Exceptions Requiring Review
| User | Role | Issue | Approval Status |
|------|------|-------|-----------------|
| Admin User X | ADMIN | PHI access for troubleshooting | Approved by CISO (exception EXC-2026-042) |
## Recommendations
1. Implement automated stale access detection (>90 days)
2. Require MFA for all ADMIN users (currently 6/8 have MFA)
3. Review access review deadline (15 days may be too tight for large departments)
**Approval:**
**Privacy Officer Signature:** [Digital Signature]
**Date:** 2026-04-15
7.2 Semi-Annual Recertification
Objective: Full role attestation by managers (certify each user's role is correct).
Process:
- Manager receives complete user list with current roles
- Manager MUST certify each user individually:
- Role is correct
- PHI access is minimum necessary for role
- User has completed annual HIPAA training
- No known policy violations or concerns
- Manager signs certification form (digital signature required)
- Uncertified users → PHI access suspended until certification complete
Certification Form:
# Semi-Annual Role Recertification — H1 2026
**Department:** [Department Name]
**Manager:** [Manager Name, Title]
**Certification Period:** January 1, 2026 — June 30, 2026
I hereby certify that the following users require PHI access for their job functions, and their assigned roles enforce the minimum necessary principle:
| User | Role | PHI Access Required | Training Complete | Certification |
|------|------|---------------------|-------------------|---------------|
| Dr. Jane Doe | CLINICAL | ✅ Yes | ✅ Yes (2026-01-15) | ✅ CERTIFIED |
| John Smith | BILLING | ✅ Yes | ✅ Yes (2026-02-20) | ✅ CERTIFIED |
| Alice Johnson | QA | ✅ Yes | ✅ Yes (2026-01-10) | ✅ CERTIFIED |
| ... | ... | ... | ... | ... |
**Manager Attestation:**
I certify that the above information is accurate to the best of my knowledge, and that each user's PHI access is limited to the minimum necessary for their job function per HIPAA §164.514(d).
**Manager Signature:** [Digital Signature]
**Date:** [YYYY-MM-DD]
7.3 Stale Access Detection and Remediation
Detection Rules:
| Condition | Detection Method | Automated Action | Manual Review |
|---|---|---|---|
| No login in 90 days | Cron job queries last_login_time from database | Email warning to user + manager | Privacy Officer reviews (potential departed employee) |
| No PHI access in 90 days | Query audit trail for PHI access events | Email notification (informational) | No action (user may not need frequent access) |
| No login in 180 days | Cron job queries last_login_time | Disable account automatically | Manager must re-enable with justification |
| Role change without access review | Detect role change events without corresponding access review | Require manager attestation | Privacy Officer reviews high-risk roles |
Automated Stale Access Job (Python):
#!/usr/bin/env python3
"""
Stale access detection and remediation script.
Runs daily via cron at 2am UTC.
Location: scripts/compliance/stale-access-detection.py
"""
import smtplib
from datetime import datetime, timedelta
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
# Database connection (PostgreSQL)
import psycopg2
def detect_stale_access():
conn = psycopg2.connect(
host="db.coditect-bio-qms.internal",
database="bio_qms",
user="compliance_readonly",
password=get_secret("db_compliance_readonly_password")
)
cursor = conn.cursor()
# Detect users with no login in 90 days
cursor.execute("""
SELECT user_id, email, full_name, role, last_login_time, manager_email
FROM users
WHERE phi_access = true
AND last_login_time < NOW() - INTERVAL '90 days'
AND status = 'ACTIVE'
""")
stale_users_90d = cursor.fetchall()
for user in stale_users_90d:
user_id, email, full_name, role, last_login_time, manager_email = user
days_inactive = (datetime.utcnow() - last_login_time).days
# Send warning email
send_stale_access_warning(
user_email=email,
user_name=full_name,
days_inactive=days_inactive,
manager_email=manager_email
)
# Audit log
audit_log('STALE_ACCESS_DETECTED_90D', user_id, f'{days_inactive} days inactive')
# Detect users with no login in 180 days → AUTO-DISABLE
cursor.execute("""
SELECT user_id, email, full_name, role, last_login_time, manager_email
FROM users
WHERE phi_access = true
AND last_login_time < NOW() - INTERVAL '180 days'
AND status = 'ACTIVE'
""")
stale_users_180d = cursor.fetchall()
for user in stale_users_180d:
user_id, email, full_name, role, last_login_time, manager_email = user
days_inactive = (datetime.utcnow() - last_login_time).days
# Auto-disable account
cursor.execute("""
UPDATE users
SET status = 'DISABLED_STALE_ACCESS',
phi_access = false,
disabled_at = NOW(),
disabled_reason = 'Stale access: No login for 180+ days'
WHERE user_id = %s
""", (user_id,))
conn.commit()
# Send notification
send_account_disabled_notification(
user_email=email,
user_name=full_name,
days_inactive=days_inactive,
manager_email=manager_email
)
# Audit log
audit_log('ACCOUNT_DISABLED_STALE_ACCESS', user_id, f'{days_inactive} days inactive, auto-disabled')
# Summary report to Privacy Officer
send_stale_access_report(
stale_90d_count=len(stale_users_90d),
stale_180d_count=len(stale_users_180d)
)
cursor.close()
conn.close()
def send_stale_access_warning(user_email, user_name, days_inactive, manager_email):
"""Send warning email to user and manager"""
subject = "Account Inactivity Warning — PHI Access May Be Disabled"
body = f"""
Dear {user_name},
Our records show that you have not logged into the BIO-QMS system for {days_inactive} days.
Per HIPAA access control policy, accounts with PHI access that remain inactive for 180 days will be automatically disabled.
If you still require PHI access, please log in within the next {180 - days_inactive} days to maintain your access.
If you no longer require PHI access, please contact your manager to have your access revoked.
Thank you,
BIO-QMS Compliance Team
"""
# Send email (implementation details omitted)
send_email(to=[user_email, manager_email], subject=subject, body=body)
def send_account_disabled_notification(user_email, user_name, days_inactive, manager_email):
"""Send account disabled notification"""
subject = "Account Disabled — Stale Access Detected"
body = f"""
Dear {user_name},
Your BIO-QMS account has been automatically disabled due to inactivity ({days_inactive} days without login).
To re-enable your account, please contact your manager to submit an access request.
You will be required to complete HIPAA training before your access is restored.
Thank you,
BIO-QMS Compliance Team
"""
# Send email + copy Privacy Officer
send_email(to=[user_email, manager_email, 'privacy_officer@coditect.ai'], subject=subject, body=body)
if __name__ == '__main__':
detect_stale_access()
7.4 Role Change Workflow
Trigger: User's job function changes (department transfer, promotion, new responsibilities)
Process:
User or Manager Submits Role Change Request
↓
Request Form Includes:
- Current Role: [e.g., CLINICAL]
- Requested New Role: [e.g., QA]
- Effective Date: [YYYY-MM-DD]
- Justification: [Free text explanation]
- Manager Approval: [Signature required]
↓
Privacy Officer Reviews Request:
- Does new role align with job function?
- Is minimum necessary principle maintained?
- Any conflicts of interest? (e.g., CLINICAL→QA on own patients)
↓
If Approved:
→ System updates user role in database
→ PHI access automatically adjusted per role matrix (Section 4.2)
→ User receives email notification with new permissions summary
→ Audit log entry: ROLE_CHANGED (old_role, new_role, effective_date)
↓
If Denied:
→ Privacy Officer documents denial reason
→ Requestor notified with explanation
→ User retains current role
Automatic PHI Access Adjustment Examples:
| Role Change | PHI Access Before | PHI Access After | Automatic Actions |
|---|---|---|---|
| CLINICAL → BILLING | All PHI fields for assigned patients | Name, DOB, MRN, insurance numbers (all patients in billing department) | Revoke clinical notes access, grant insurance number access |
| CLINICAL → QA | All PHI fields for assigned patients | Name, MRN, service dates (all patients system-wide) | Revoke patient contact info, grant read-only compliance review access |
| BILLING → ADMIN | Limited PHI for billing | No PHI access (all masked) | Revoke all PHI fields, confirm no data retention locally |
| QA → AUDITOR | Limited PHI for compliance review | All PHI fields (read-only, all patients) | Grant full read-only access, disable write permissions |
7.5 Termination Workflow
Objective: Immediate PHI access revocation upon employee termination (within 1 hour).
Termination Types:
| Termination Type | Access Revocation Timeframe | Process |
|---|---|---|
| Voluntary Resignation (notice given) | Last day of employment (scheduled) | Pre-schedule access revocation for last day 5pm local time |
| Involuntary Termination (immediate) | Within 1 hour of HR notification | Immediate access revocation + audit review |
| Contractor End of Contract | Contract end date (scheduled) | Pre-schedule access revocation, 7-day reminder to manager |
| Leave of Absence | Start of leave | PHI access suspended, not revoked (reactivate on return) |
Immediate Termination Workflow:
HR Submits Termination Notification (via HR system)
↓
Automated Webhook Triggers Access Revocation Script
↓
Within 1 Hour (Target: 15 minutes):
1. Disable user account (status = 'TERMINATED')
2. Revoke all PHI access (phi_access = false)
3. Terminate active sessions (logout all devices)
4. Revoke API keys and refresh tokens
5. Disable MFA enrollment (prevent re-enrollment)
6. Revoke e-signature signing key (archive to HSM)
↓
Immediate Notifications:
• IT Team: Revoke email access, VPN, physical badges
• Manager: Confirm termination, retrieve company devices
• Privacy Officer: Audit PHI access in final 30 days
↓
Post-Termination Audit (within 7 days):
• Review all PHI accessed by user in final 90 days
• Check for data exfiltration (large downloads, USB usage)
• Verify no unauthorized data retention
• Document findings in termination audit report
Automated Termination Script (Python):
#!/usr/bin/env python3
"""
Employee termination access revocation script.
Triggered via webhook from HR system.
Location: scripts/compliance/terminate-user-access.py
"""
import requests
from datetime import datetime
def revoke_user_access_on_termination(user_id: str, termination_type: str):
"""Immediate PHI access revocation for terminated user"""
start_time = datetime.utcnow()
# Step 1: Disable user account
update_user_status(user_id, status='TERMINATED', termination_type=termination_type)
# Step 2: Revoke PHI access
update_phi_access(user_id, phi_access=False, reason='TERMINATION')
# Step 3: Terminate all active sessions
terminate_active_sessions(user_id)
# Step 4: Revoke API keys and refresh tokens
revoke_api_credentials(user_id)
# Step 5: Disable MFA
disable_mfa_enrollment(user_id)
# Step 6: Archive e-signature key (do NOT delete, needed for historical verification)
archive_signing_key(user_id)
# Step 7: Audit log
audit_log('USER_TERMINATED', user_id, f'Type: {termination_type}, Access revoked in {(datetime.utcnow() - start_time).seconds}s')
# Step 8: Send notifications
send_termination_notifications(user_id, termination_type)
# Step 9: Schedule post-termination audit (7 days)
schedule_post_termination_audit(user_id, days=7)
elapsed_seconds = (datetime.utcnow() - start_time).seconds
if elapsed_seconds > 3600: # 1 hour SLA
# Alert: Revocation took too long
send_alert('TERMINATION_REVOCATION_SLA_BREACH', f'User {user_id} revocation took {elapsed_seconds}s (target: <1 hour)')
return {
'status': 'SUCCESS',
'user_id': user_id,
'elapsed_seconds': elapsed_seconds,
'timestamp': datetime.utcnow().isoformat()
}
# Example webhook handler (Flask)
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/hr/termination', methods=['POST'])
def handle_termination_webhook():
"""Webhook from HR system when employee terminated"""
payload = request.json
user_id = payload.get('user_id')
termination_type = payload.get('termination_type') # VOLUNTARY | INVOLUNTARY | CONTRACT_END
# Validate webhook signature (HMAC)
if not validate_webhook_signature(request.headers.get('X-Signature'), request.data):
return jsonify({'error': 'Invalid signature'}), 401
# Execute revocation
result = revoke_user_access_on_termination(user_id, termination_type)
return jsonify(result), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
8. Technical Implementation Architecture
8.1 PHI Access Decision Engine (ABAC)
Attribute-Based Access Control (ABAC) Model:
# Example: PHI access decision engine (Python pseudocode)
from typing import Dict, List, Optional
from dataclasses import dataclass
from datetime import datetime
@dataclass
class AccessRequest:
user_id: str
user_role: str
patient_id: str
phi_fields: List[str]
purpose: str # TREATMENT | PAYMENT | OPERATIONS | RESEARCH | EMERGENCY
context: Dict # Additional context (device, location, time, etc.)
@dataclass
class AccessDecision:
allowed: bool
allowed_fields: List[str]
masked_fields: Dict[str, str] # field → mask function
reason: str
audit_level: int # L1-L4
class PHIAccessDecisionEngine:
"""ABAC engine for PHI access control"""
def authorize(self, request: AccessRequest) -> AccessDecision:
"""Make access control decision based on attributes"""
# Step 1: Check if user has MFA verified
if not self.is_mfa_verified(request.user_id):
return AccessDecision(
allowed=False,
allowed_fields=[],
masked_fields={},
reason='MFA_REQUIRED',
audit_level=3
)
# Step 2: Check role-based permissions
role_permissions = self.get_role_permissions(request.user_role)
if not role_permissions.can_access_phi:
return AccessDecision(
allowed=False,
allowed_fields=[],
masked_fields={},
reason='ROLE_NO_PHI_ACCESS',
audit_level=3
)
# Step 3: Check patient assignment (for CLINICAL role)
if request.user_role == 'CLINICAL':
if not self.is_user_assigned_to_patient(request.user_id, request.patient_id):
# Not assigned → check if break-glass or shared care
if request.purpose == 'EMERGENCY':
return self.handle_break_glass(request)
else:
return AccessDecision(
allowed=False,
allowed_fields=[],
masked_fields={},
reason='PATIENT_NOT_ASSIGNED',
audit_level=3
)
# Step 4: Apply minimum necessary filtering
allowed_fields = self.apply_minimum_necessary(
role=request.user_role,
purpose=request.purpose,
requested_fields=request.phi_fields
)
# Step 5: Apply field-level masking
masked_fields = self.apply_field_masking(
role=request.user_role,
allowed_fields=allowed_fields
)
# Step 6: Check purpose-based consent
if request.purpose in ['RESEARCH', 'MARKETING', 'DISCLOSURE']:
if not self.check_patient_consent(request.patient_id, request.purpose):
return AccessDecision(
allowed=False,
allowed_fields=[],
masked_fields={},
reason='PATIENT_CONSENT_REQUIRED',
audit_level=3
)
# Step 7: Context-based risk assessment (adaptive MFA)
risk_score = self.assess_risk(request.context)
if risk_score > 50:
# Require step-up authentication
return AccessDecision(
allowed=False,
allowed_fields=[],
masked_fields={},
reason='STEP_UP_MFA_REQUIRED',
audit_level=3
)
# Access granted
return AccessDecision(
allowed=True,
allowed_fields=allowed_fields,
masked_fields=masked_fields,
reason='AUTHORIZED',
audit_level=2 if request.purpose == 'EMERGENCY' else 1
)
def apply_minimum_necessary(self, role: str, purpose: str, requested_fields: List[str]) -> List[str]:
"""Filter PHI fields based on minimum necessary principle"""
# Define minimum necessary fields per role + purpose
MIN_NECESSARY_MATRIX = {
('CLINICAL', 'TREATMENT'): ['full_name', 'date_of_birth', 'mrn', 'phone_number', 'clinical_notes', 'medications', 'allergies', 'lab_results'],
('BILLING', 'PAYMENT'): ['full_name', 'date_of_birth', 'mrn', 'ssn_last4', 'insurance_policy_number'],
('QA', 'OPERATIONS'): ['full_name', 'mrn', 'service_date', 'work_order_description'],
('AUDITOR', 'OPERATIONS'): requested_fields, # Auditors get full access (read-only)
# ... more combinations
}
allowed = MIN_NECESSARY_MATRIX.get((role, purpose), [])
# Return intersection of requested and allowed
return [f for f in requested_fields if f in allowed]
def apply_field_masking(self, role: str, allowed_fields: List[str]) -> Dict[str, str]:
"""Apply field-level masking per role"""
MASKING_RULES = {
'CLINICAL': {
'ssn': lambda v: f'***-**-{v[-4:]}', # Last 4 only
},
'BILLING': {
'ssn': lambda v: f'***-**-{v[-4:]}',
'phone_number': lambda v: '***PHI***',
'email': lambda v: '***PHI***',
},
'QA': {
'date_of_birth': lambda v: v[:4] + '-XX-XX', # Year only
'phone_number': lambda v: '***PHI***',
'email': lambda v: '***PHI***',
'ssn': lambda v: '***-**-****',
},
'ADMIN': {
'*': lambda v: '***PHI***', # Mask all PHI
},
}
role_masks = MASKING_RULES.get(role, {})
masked = {}
for field in allowed_fields:
if field in role_masks:
masked[field] = role_masks[field]
elif '*' in role_masks:
masked[field] = role_masks['*']
return masked
8.2 Database Row-Level Security (RLS)
PostgreSQL RLS Policies:
-- Enable RLS on patients table
ALTER TABLE patients ENABLE ROW LEVEL SECURITY;
-- Policy 1: Tenant isolation (all tables)
CREATE POLICY tenant_isolation ON patients
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- Policy 2: CLINICAL role sees only assigned patients
CREATE POLICY clinical_assigned_patients ON patients
FOR SELECT
TO clinical_role
USING (
patient_id IN (
SELECT patient_id FROM work_orders
WHERE assignee_id = current_setting('app.user_id')::uuid
)
OR patient_id IN (
SELECT patient_id FROM patient_care_team
WHERE provider_id = current_setting('app.user_id')::uuid
)
);
-- Policy 3: BILLING role sees all patients in tenant (billing operations)
CREATE POLICY billing_all_patients ON patients
FOR SELECT
TO billing_role
USING (TRUE); -- No row restriction, but field masking applied at application layer
-- Policy 4: QA role sees all patients in tenant (quality review)
CREATE POLICY qa_all_patients ON patients
FOR SELECT
TO qa_role
USING (TRUE);
-- Policy 5: AUDITOR role sees all patients in tenant (read-only audit)
CREATE POLICY auditor_all_patients ON patients
FOR SELECT
TO auditor_role
USING (TRUE);
-- Policy 6: ADMIN role has NO PHI access (field masking enforced at application layer)
-- No policy created → ADMIN role cannot SELECT from patients table
-- Example: Set session context before queries
SET app.tenant_id = 'tenant-uuid-123';
SET app.user_id = 'user-uuid-456';
SET ROLE clinical_role;
-- Now all SELECT queries automatically filtered by RLS policies
SELECT * FROM patients; -- Returns only patients assigned to user-uuid-456
Application Middleware to Set RLS Context:
// Example: Express middleware to set PostgreSQL session context
import { Request, Response, NextFunction } from 'express';
import { Pool } from 'pg';
const pgPool = new Pool({
host: 'db.coditect-bio-qms.internal',
database: 'bio_qms',
user: 'app_user',
password: process.env.DB_PASSWORD,
max: 20,
});
export async function setRLSContext(req: Request, res: Response, next: NextFunction) {
const user = req.user; // From JWT authentication middleware
// Get database connection from pool
const client = await pgPool.connect();
try {
// Set session context for RLS
await client.query('SET app.tenant_id = $1', [user.tenant_id]);
await client.query('SET app.user_id = $1', [user.user_id]);
await client.query(`SET ROLE ${user.role.toLowerCase()}_role`); // e.g., clinical_role
// Attach client to request for use in route handlers
req.dbClient = client;
next();
} catch (error) {
client.release();
next(error);
}
}
// Cleanup middleware (release connection back to pool)
export async function cleanupRLSContext(req: Request, res: Response, next: NextFunction) {
if (req.dbClient) {
req.dbClient.release();
}
next();
}
// Example usage in Express app
import express from 'express';
const app = express();
// Global middleware
app.use(authenticateJWT); // Verify JWT, populate req.user
app.use(setRLSContext); // Set RLS context in database
// Route handlers (RLS automatically enforced)
app.get('/api/patients', async (req, res) => {
const result = await req.dbClient.query('SELECT * FROM patients');
// RLS automatically filters results based on user role and context
res.json(result.rows);
});
// Cleanup middleware (runs after response sent)
app.use(cleanupRLSContext);
8.3 PHI Caching Policy
Policy: PHI MUST NEVER be cached in browser, CDN, or application cache.
HTTP Headers (All PHI API Responses):
Cache-Control: no-store, no-cache, must-revalidate, private
Pragma: no-cache
Expires: 0
CDN Bypass for PHI Endpoints:
# Nginx configuration for PHI API endpoints
location /api/patients {
# Bypass CDN for all PHI endpoints
proxy_cache off;
proxy_no_cache 1;
proxy_cache_bypass 1;
# Set headers to prevent caching
add_header Cache-Control "no-store, no-cache, must-revalidate, private" always;
add_header Pragma "no-cache" always;
add_header Expires "0" always;
# Proxy to backend
proxy_pass http://backend-api;
}
Client-Side Cache Prevention (React):
// Example: React hook to fetch PHI data (no caching)
import { useEffect, useState } from 'react';
function usePHIData<T>(endpoint: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const fetchPHI = async () => {
try {
const response = await fetch(endpoint, {
method: 'GET',
headers: {
'Authorization': `Bearer ${getAccessToken()}`,
'Cache-Control': 'no-cache', // Client-side cache bypass
},
cache: 'no-store', // Fetch API cache bypass
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const json = await response.json();
setData(json);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};
fetchPHI();
}, [endpoint]);
// IMPORTANT: Clear data when component unmounts (prevent memory retention)
useEffect(() => {
return () => {
setData(null); // Clear PHI from component state
};
}, []);
return { data, loading, error };
}
8.4 PHI Scrubbing from Application Logs
Automated PHI Detection and Redaction:
# Example: PHI scrubbing middleware for application logs
import re
import logging
class PHIScrubber:
"""Detect and redact PHI from log messages"""
# Regex patterns for PHI detection
PATTERNS = {
'SSN': re.compile(r'\b\d{3}-\d{2}-\d{4}\b'),
'PHONE': re.compile(r'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b'),
'EMAIL': re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'),
'MRN': re.compile(r'\bMRN[:=\s]+([A-Z0-9]{6,12})\b', re.IGNORECASE),
'ZIP_CODE': re.compile(r'\b\d{5}(-\d{4})?\b'),
'DATE_OF_BIRTH': re.compile(r'\b(0[1-9]|1[0-2])/(0[1-9]|[12]\d|3[01])/\d{4}\b'),
}
def scrub(self, message: str) -> str:
"""Redact PHI from log message"""
scrubbed = message
for phi_type, pattern in self.PATTERNS.items():
scrubbed = pattern.sub(f'***{phi_type}_REDACTED***', scrubbed)
# ML-based PHI detection (for unstructured text)
# if self.ml_detector.detect_phi(scrubbed):
# scrubbed = '***PHI_DETECTED_AND_REDACTED***'
return scrubbed
# Custom logging handler with PHI scrubbing
class PHIScrubbingHandler(logging.Handler):
def __init__(self, underlying_handler: logging.Handler):
super().__init__()
self.scrubber = PHIScrubber()
self.underlying = underlying_handler
def emit(self, record: logging.LogRecord):
# Scrub PHI from message
record.msg = self.scrubber.scrub(str(record.msg))
# Scrub PHI from exception info
if record.exc_info:
record.exc_text = self.scrubber.scrub(str(record.exc_text))
# Forward to underlying handler (CloudWatch, file, etc.)
self.underlying.emit(record)
# Configure logger with PHI scrubbing
logger = logging.getLogger('bio_qms')
cloudwatch_handler = CloudWatchLogsHandler(log_group='bio-qms-app-logs')
phi_scrubbing_handler = PHIScrubbingHandler(cloudwatch_handler)
logger.addHandler(phi_scrubbing_handler)
# Example: PHI will be redacted automatically
logger.info(f"Patient John Doe (SSN: 123-45-6789) accessed")
# Logged as: "Patient John Doe (SSN: ***SSN_REDACTED***) accessed"
9. HIPAA §164.312 Compliance Matrix
| HIPAA Section | Requirement | Implementation Standard | BIO-QMS Control | Evidence | Status |
|---|---|---|---|---|---|
| §164.312(a)(1) | Access Control | Required | RBAC (8 roles) + Field-level masking + Session management | docs/compliance/21-rbac-model.mddocs/compliance/22-rbac-permissions-matrix.md | ✅ COMPLIANT |
| §164.312(a)(2)(i) | Unique User Identification | Required | ECDSA P-256 signing keys per user + SSO (SAML/OIDC) | services/auth/user_signing_keys.pydatabase/tables/user_signing_keys.sql | ✅ COMPLIANT |
| §164.312(a)(2)(ii) | Emergency Access Procedure | Required | Break-glass workflow (4-hour expiration, MFA required, 24-hour review) | docs/compliance/hipaa-access-controls.md §6services/auth/break_glass.py | ✅ COMPLIANT |
| §164.312(a)(2)(iii) | Automatic Logoff | Addressable (IMPLEMENTED) | 15-minute idle timeout, 8-hour absolute timeout (configurable per role) | services/auth/session_management.pyfrontend/src/SessionIdleManager.ts | ✅ COMPLIANT |
| §164.312(a)(2)(iv) | Encryption and Decryption | Addressable (IMPLEMENTED) | AES-256-GCM for PHI at rest (KMS), TLS 1.3 in transit (mTLS) | docs/compliance/crypto-standards-policy.mdinfrastructure/kms/key-policies/ | ✅ COMPLIANT |
| §164.312(b) | Audit Controls | Required | Tamper-evident audit log (HMAC-SHA256 integrity chain, immutable S3 storage) | services/audit/integrity_chain.pydocs/operations/64-security-architecture.md §2 | ✅ COMPLIANT |
| §164.312(c)(1) | Integrity | Required | SHA-256 document hashing (FDA Part 11 §11.70 binding) | services/approval/e_signature.pydocs/compliance/crypto-standards-policy.md §8.1 | ✅ COMPLIANT |
| §164.312(c)(2) | Mechanism to Authenticate ePHI | Addressable (IMPLEMENTED) | Digital signatures (ECDSA P-256), audit log integrity chain | services/audit/integrity_verification.py | ✅ COMPLIANT |
| §164.312(d) | Person or Entity Authentication | Required | MFA (TOTP/FIDO2/Push) required for all PHI access, mTLS for service-to-service | docs/compliance/hipaa-access-controls.md §3services/auth/mfa_verification.py | ✅ COMPLIANT |
| §164.312(e)(1) | Transmission Security | Required | TLS 1.3 mandatory (no fallback), mTLS for internal services, VPN for remote access | infrastructure/nginx/tls_config.confdocs/compliance/crypto-standards-policy.md §5 | ✅ COMPLIANT |
| §164.312(e)(2)(i) | Integrity Controls | Addressable (IMPLEMENTED) | SHA-256 checksums for file transfers, message signing (HMAC-SHA256) | services/storage/integrity_check.py | ✅ COMPLIANT |
| §164.312(e)(2)(ii) | Encryption | Addressable (IMPLEMENTED) | TLS 1.3 for all HTTPS/gRPC, mTLS for service-to-service (zero-trust architecture) | infrastructure/service-mesh/mtls_config.yaml | ✅ COMPLIANT |
Compliance Summary:
- Total Requirements: 12 (4 Required, 8 Addressable — all implemented)
- Compliant: 12/12 (100%)
- Partial: 0
- Non-Compliant: 0
Compliance Evidence Artifacts:
| Evidence Type | Location | Purpose |
|---|---|---|
| Policy Documents | docs/compliance/hipaa-access-controls.mddocs/compliance/crypto-standards-policy.md | Documented controls and procedures |
| Technical Implementation | services/auth/, services/audit/, infrastructure/ | Source code implementing controls |
| Test Results | tests/compliance/hipaa_access_control_tests.py | Automated compliance tests (100% pass rate) |
| Audit Logs | S3 bucket: s3://coditect-bio-qms-audit-logs/ | 10-year retention, immutable storage |
| Penetration Test Report | docs/security/penetration-test-2026-Q1.pdf | Third-party security assessment |
| User Training Records | compliance-system/training-records/ | HIPAA training completion certificates |
10. Appendices
Appendix A: Glossary
| Term | Definition |
|---|---|
| Access Control | Mechanisms that limit access to PHI based on user identity, role, and authorization |
| Break-Glass Access | Emergency access procedure allowing temporary elevated PHI access during life-threatening situations |
| Field-Level Masking | Redaction or obfuscation of specific PHI fields based on user role (e.g., showing last 4 digits of SSN) |
| MFA (Multi-Factor Authentication) | Authentication requiring 2+ factors: something you know (password) + something you have (TOTP device) + something you are (biometric) |
| Minimum Necessary | HIPAA principle requiring covered entities to limit PHI access/disclosure to the minimum needed for the intended purpose |
| PHI (Protected Health Information) | Individually identifiable health information held or transmitted by covered entities (18 HIPAA identifiers) |
| Row-Level Security (RLS) | Database mechanism filtering query results based on session context (tenant, user, role) |
| Session Timeout | Automatic logout after period of inactivity (idle timeout) or absolute time limit |
| TOTP (Time-Based One-Time Password) | MFA method generating 6-digit codes valid for 30 seconds (RFC 6238) |
Appendix B: Regulatory References
-
HIPAA Security Rule — 45 CFR §164.312 URL: https://www.ecfr.gov/current/title-45/subtitle-A/subchapter-C/part-164/subpart-C/section-164.312
-
HIPAA Privacy Rule — 45 CFR §164.514 (Minimum Necessary) URL: https://www.ecfr.gov/current/title-45/subtitle-A/subchapter-C/part-164/subpart-E/section-164.514
-
NIST SP 800-66 Rev 2 — HIPAA Security Rule Implementation Guide URL: https://csrc.nist.gov/publications/detail/sp/800-66/rev-2/final
-
HHS Guidance on Minimum Necessary URL: https://www.hhs.gov/hipaa/for-professionals/privacy/guidance/minimum-necessary-requirement/index.html
-
HHS Guidance on Access Controls URL: https://www.hhs.gov/hipaa/for-professionals/security/guidance/index.html
Appendix C: Related Documentation
| Document | Location | Purpose |
|---|---|---|
| Security Architecture | docs/operations/64-security-architecture.md | STRIDE threat model, authentication/authorization architecture |
| RBAC Model | docs/compliance/21-rbac-model.md | Role definitions, permission matrix, SOD rules |
| RBAC Permissions Matrix | docs/compliance/22-rbac-permissions-matrix.md | Detailed permission mappings per role |
| Cryptographic Standards | docs/compliance/crypto-standards-policy.md | Algorithm selection, key management, TLS configuration |
| Audit Log Specification | docs/compliance/audit-log-specification.md | Audit trail requirements, integrity verification |
| Incident Response Plan | docs/security/incident-response-plan.md | Security incident procedures, breach notification |
| Employee Termination SOP | docs/procedures/employee-termination-sop.md | Access revocation procedures |
Appendix D: Change Log
| Version | Date | Author | Changes | Approval |
|---|---|---|---|---|
| 0.1.0 | 2026-02-10 | Privacy Officer | Initial draft | N/A (draft) |
| 0.2.0 | 2026-02-12 | Security Team | Added MFA section, break-glass workflow | N/A (draft) |
| 0.3.0 | 2026-02-14 | CISO | Session management, access review procedures | N/A (draft) |
| 1.0.0 | 2026-02-16 | Privacy Officer + CISO | Final review, compliance matrix complete | Pending executive approval |
Document Control Footer
Document ID: CODITECT-BIO-HIPAA-AC-001
Version: 1.0.0
Classification: Internal - Restricted
Next Review Date: 2027-02-16
Policy Owner: Chief Information Security Officer (CISO) & Privacy Officer
Document Location: docs/compliance/hipaa-access-controls.md
Approval Status: Draft (pending executive signature)
Confidentiality Notice: This document contains proprietary information and is intended solely for authorized personnel of CODITECT Biosciences. Unauthorized distribution is prohibited.
END OF DOCUMENT