HIPAA Security Requirements for Healthcare Apps
HIPAA Security Requirements for Healthcare Apps
Building healthcare applications that handle Protected Health Information (PHI) requires strict adherence to HIPAA security requirements. Having architected HIPAA-compliant systems processing millions of patient records, I’m sharing the technical implementation details you need.
Understanding HIPAA
HIPAA (Health Insurance Portability and Accountability Act) mandates how healthcare providers, payers, and their business associates must protect patient health information.
Key terms:
- PHI - Protected Health Information (any health data that can identify a patient)
- Covered Entity - Healthcare providers, health plans, healthcare clearinghouses
- Business Associate - Third parties handling PHI on behalf of covered entities
- BAA - Business Associate Agreement (required contract)
Penalties for violations:
- Tier 1 (Unaware): $100-$50,000 per violation
- Tier 2 (Reasonable cause): $1,000-$50,000 per violation
- Tier 3 (Willful neglect, corrected): $10,000-$50,000 per violation
- Tier 4 (Willful neglect, not corrected): $50,000 per violation
- Maximum annual penalty: $1.5 million per violation type
The HIPAA Security Rule
The Security Rule has three types of safeguards:
1. Administrative Safeguards
Security management, workforce training, contingency planning
2. Physical Safeguards
Facility access, workstation security, device controls
3. Technical Safeguards
Access control, audit controls, integrity, transmission security
Today I’m focusing on the technical safeguards - the code and infrastructure.
What Qualifies as PHI?
PHI includes any of the following when linked to health information:
Identifiers:
- Names
- Dates (birth, admission, discharge, death)
- Phone/fax numbers
- Email addresses
- SSN
- Medical record numbers
- Health plan numbers
- Account numbers
- License/certificate numbers
- Vehicle identifiers
- Device identifiers/serial numbers
- URLs
- IP addresses
- Biometric identifiers
- Photos
- Any other unique identifying number
Health information:
- Medical history
- Test results
- Diagnoses
- Treatment plans
- Prescriptions
- Insurance information
Architecture for HIPAA Compliance
High-Level Design
┌─────────────────────────────────────────┐
│ Internet (Public) │
└──────────────┬──────────────────────────┘
│
┌──────┴──────┐
│ CloudFlare │ WAF, DDoS protection
│ CDN │
└──────┬──────┘
│
┌──────────────┼──────────────────────────┐
│ VPC (HIPAA-compliant) │
│ │ │
│ ┌───────┴────────┐ │
│ │ Load Balancer │ │
│ │ (TLS 1.2+) │ │
│ └───────┬─────────┘ │
│ │ │
│ ┌──────────┼──────────────┐ │
│ │ Public Subnet (DMZ) │ │
│ │ ┌──────┴──────┐ │ │
│ │ │ Web Tier │ │ │
│ │ │ (Nginx) │ │ │
│ │ └──────┬──────┘ │ │
│ └─────────┼───────────────┘ │
│ │ │
│ ┌─────────┼────────────────┐ │
│ │ Private Subnet │ │
│ │ ┌──────┴──────┐ │ │
│ │ │ App Tier │ │ │
│ │ │ (Node.js) │ │ │
│ │ └──────┬──────┘ │ │
│ └─────────┼────────────────┘ │
│ │ │
│ ┌─────────┼────────────────┐ │
│ │ Database Subnet │ │
│ │ ┌──────┴──────┐ │ │
│ │ │ PostgreSQL │ │ │
│ │ │ (Encrypted)│ │ │
│ │ └─────────────┘ │ │
│ └──────────────────────────┘ │
│ │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Audit & Monitoring (Isolated) │
│ - CloudWatch Logs │
│ - S3 (Encrypted, versioned) │
│ - CloudTrail (All API calls) │
└─────────────────────────────────────────┘
Key principles:
- Network isolation (VPC)
- Multiple security layers
- Encryption everywhere
- Comprehensive logging
- No direct internet access to databases
1. Access Control (§164.312(a)(1))
Unique User Identification
// User model with unique identifiers
const userSchema = new mongoose.Schema({
// Unique identifier (required)
user_id: {
type: String,
required: true,
unique: true,
default: () => uuidv4(),
},
// Authentication
email: {
type: String,
required: true,
unique: true,
lowercase: true,
},
password_hash: {
type: String,
required: true,
},
// Role-based access
role: {
type: String,
enum: ['physician', 'nurse', 'admin', 'billing', 'patient'],
required: true,
},
// Access privileges
permissions: [{
type: String,
enum: ['read_phi', 'write_phi', 'delete_phi', 'export_phi', 'manage_users'],
}],
// MFA required for PHI access
mfa_enabled: {
type: Boolean,
required: true,
default: false,
},
mfa_secret: String,
// Audit trail
last_login: Date,
login_attempts: {
type: Number,
default: 0,
},
account_locked: {
type: Boolean,
default: false,
},
created_at: Date,
updated_at: Date,
});
Emergency Access Procedure
// Break-glass access for emergencies
class EmergencyAccess {
async grantEmergencyAccess(userId, reason, duration = 60) {
// Log emergency access request
await AuditLog.create({
event_type: 'emergency_access_granted',
user_id: userId,
reason,
duration_minutes: duration,
granted_by: 'system',
granted_at: new Date(),
});
// Grant temporary elevated access
const expiresAt = new Date(Date.now() + duration * 60000);
await EmergencyAccessGrant.create({
user_id: userId,
reason,
expires_at: expiresAt,
revoked: false,
});
// Alert security team
await this.alertSecurityTeam({
type: 'emergency_access',
user_id: userId,
reason,
expires_at: expiresAt,
});
return { granted: true, expires_at: expiresAt };
}
async checkEmergencyAccess(userId) {
const grant = await EmergencyAccessGrant.findOne({
user_id: userId,
expires_at: { $gt: new Date() },
revoked: false,
});
return grant !== null;
}
}
Automatic Logoff
// Session timeout middleware
const SESSION_TIMEOUT = 15 * 60 * 1000; // 15 minutes
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true,
maxAge: SESSION_TIMEOUT,
sameSite: 'strict',
},
store: new RedisStore({
client: redisClient,
ttl: SESSION_TIMEOUT / 1000,
}),
}));
// Activity tracking middleware
app.use((req, res, next) => {
if (req.session.user) {
const now = Date.now();
const lastActivity = req.session.lastActivity || now;
// Check for inactivity
if (now - lastActivity > SESSION_TIMEOUT) {
req.session.destroy();
return res.status(401).json({
error: 'Session expired due to inactivity',
});
}
// Update last activity
req.session.lastActivity = now;
}
next();
});
Encryption and Decryption
// Encryption of PHI at rest
const crypto = require('crypto');
class PHIEncryption {
constructor() {
// Use AES-256-GCM (authenticated encryption)
this.algorithm = 'aes-256-gcm';
this.keyDerivationIterations = 100000;
}
// Derive encryption key from master key
deriveKey(masterKey, salt) {
return crypto.pbkdf2Sync(
masterKey,
salt,
this.keyDerivationIterations,
32,
'sha256'
);
}
// Encrypt PHI
encrypt(plaintext, masterKey) {
// Generate random salt and IV
const salt = crypto.randomBytes(16);
const iv = crypto.randomBytes(12);
// Derive key
const key = this.deriveKey(masterKey, salt);
// Encrypt
const cipher = crypto.createCipheriv(this.algorithm, key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'base64');
encrypted += cipher.final('base64');
// Get auth tag
const authTag = cipher.getAuthTag();
// Return: salt + iv + authTag + encrypted
return {
encrypted: encrypted,
salt: salt.toString('base64'),
iv: iv.toString('base64'),
authTag: authTag.toString('base64'),
};
}
// Decrypt PHI
decrypt(encryptedData, masterKey) {
// Parse components
const salt = Buffer.from(encryptedData.salt, 'base64');
const iv = Buffer.from(encryptedData.iv, 'base64');
const authTag = Buffer.from(encryptedData.authTag, 'base64');
const encrypted = encryptedData.encrypted;
// Derive key
const key = this.deriveKey(masterKey, salt);
// Decrypt
const decipher = crypto.createDecipheriv(this.algorithm, key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, 'base64', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
// Usage in patient record model
const phiEncryption = new PHIEncryption();
const patientSchema = new mongoose.Schema({
patient_id: String,
// Encrypted PHI fields
first_name_encrypted: Object,
last_name_encrypted: Object,
ssn_encrypted: Object,
diagnosis_encrypted: Object,
// Searchable hash (for lookups)
ssn_hash: String,
// Non-PHI fields (not encrypted)
date_created: Date,
});
// Pre-save hook to encrypt PHI
patientSchema.pre('save', function(next) {
if (this.isModified('first_name')) {
this.first_name_encrypted = phiEncryption.encrypt(
this.first_name,
process.env.MASTER_ENCRYPTION_KEY
);
}
// ... encrypt other fields
next();
});
2. Audit Controls (§164.312(b))
Comprehensive Audit Logging
// Audit logging middleware
class AuditLogger {
async log(event) {
const auditEntry = {
timestamp: new Date().toISOString(),
event_type: event.type,
user_id: event.user_id,
user_role: event.user_role,
ip_address: event.ip_address,
action: event.action,
resource: event.resource,
resource_id: event.resource_id,
result: event.result, // success/failure
phi_accessed: event.phi_accessed || false,
patient_id: event.patient_id,
details: event.details,
};
// Log to multiple destinations
await Promise.all([
// Database (for querying)
AuditLog.create(auditEntry),
// S3 (for long-term retention)
this.writeToS3(auditEntry),
// CloudWatch (for monitoring/alerting)
this.sendToCloudWatch(auditEntry),
]);
// Alert on suspicious activity
if (this.isSuspicious(auditEntry)) {
await this.alertSecurityTeam(auditEntry);
}
}
isSuspicious(entry) {
return (
entry.result === 'unauthorized_access' ||
entry.action === 'bulk_export' ||
entry.event_type === 'emergency_access' ||
entry.action === 'delete_phi'
);
}
}
// Usage in routes
const auditLogger = new AuditLogger();
app.get('/api/patients/:id', authenticate, async (req, res) => {
try {
const patient = await Patient.findById(req.params.id);
// Log successful PHI access
await auditLogger.log({
type: 'phi_access',
user_id: req.user.id,
user_role: req.user.role,
ip_address: req.ip,
action: 'read',
resource: 'patient',
resource_id: req.params.id,
result: 'success',
phi_accessed: true,
patient_id: patient.patient_id,
details: {
fields_accessed: ['name', 'dob', 'diagnosis'],
},
});
res.json(patient);
} catch (error) {
// Log failed access attempt
await auditLogger.log({
type: 'phi_access',
user_id: req.user.id,
user_role: req.user.role,
ip_address: req.ip,
action: 'read',
resource: 'patient',
resource_id: req.params.id,
result: 'failure',
phi_accessed: false,
details: { error: error.message },
});
res.status(500).json({ error: 'Internal server error' });
}
});
Audit Log Retention
// Audit log retention policy (6 years minimum)
class AuditLogRetention {
async archiveOldLogs() {
const sixYearsAgo = new Date();
sixYearsAgo.setFullYear(sixYearsAgo.getFullYear() - 6);
// Move logs older than 6 years to glacier storage
const oldLogs = await AuditLog.find({
timestamp: { $lt: sixYearsAgo },
archived: false,
});
for (const log of oldLogs) {
// Upload to S3 Glacier
await this.uploadToGlacier(log);
// Mark as archived
await AuditLog.updateOne(
{ _id: log._id },
{ archived: true, archived_at: new Date() }
);
}
console.log(`Archived ${oldLogs.length} audit logs to Glacier`);
}
}
// Run monthly
cron.schedule('0 0 1 * *', async () => {
const retention = new AuditLogRetention();
await retention.archiveOldLogs();
});
3. Integrity Controls (§164.312(c)(1))
Data Integrity Verification
// Ensure PHI has not been altered
class DataIntegrity {
// Calculate hash of PHI
calculateHash(data) {
return crypto
.createHash('sha256')
.update(JSON.stringify(data))
.digest('hex');
}
// Store hash with data
async saveWithIntegrity(model, data) {
const hash = this.calculateHash(data);
await model.create({
...data,
integrity_hash: hash,
integrity_verified_at: new Date(),
});
}
// Verify data integrity
async verifyIntegrity(record) {
const currentHash = this.calculateHash({
// Hash all PHI fields
first_name: record.first_name,
last_name: record.last_name,
ssn: record.ssn,
diagnosis: record.diagnosis,
// ... other PHI fields
});
if (currentHash !== record.integrity_hash) {
// Data has been tampered with
await this.alertSecurityTeam({
type: 'data_integrity_violation',
record_id: record._id,
expected_hash: record.integrity_hash,
actual_hash: currentHash,
});
throw new Error('Data integrity check failed');
}
return true;
}
}
Electronic Signature
// Digital signatures for critical actions
class ElectronicSignature {
async signRecord(userId, recordId, action) {
// Create signature payload
const payload = {
user_id: userId,
record_id: recordId,
action,
timestamp: new Date().toISOString(),
};
// Sign with user's private key
const signature = crypto
.createSign('RSA-SHA256')
.update(JSON.stringify(payload))
.sign(process.env.SIGNING_KEY, 'base64');
// Store signature
await Signature.create({
...payload,
signature,
verified: false,
});
return signature;
}
async verifySignature(signatureId) {
const sig = await Signature.findById(signatureId);
const payload = {
user_id: sig.user_id,
record_id: sig.record_id,
action: sig.action,
timestamp: sig.timestamp,
};
const verified = crypto
.createVerify('RSA-SHA256')
.update(JSON.stringify(payload))
.verify(process.env.PUBLIC_KEY, sig.signature, 'base64');
await Signature.updateOne(
{ _id: signatureId },
{ verified, verified_at: new Date() }
);
return verified;
}
}
4. Transmission Security (§164.312(e)(1))
TLS Configuration
# nginx.conf - HIPAA-compliant TLS
server {
listen 443 ssl http2;
server_name healthcare.example.com;
# TLS 1.2 minimum (TLS 1.3 preferred)
ssl_protocols TLSv1.2 TLSv1.3;
# Strong ciphers only
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
ssl_prefer_server_ciphers on;
# Certificates
ssl_certificate /etc/ssl/certs/healthcare.crt;
ssl_certificate_key /etc/ssl/private/healthcare.key;
# HSTS (force HTTPS)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Disable client-side caching of PHI
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
add_header Pragma "no-cache" always;
# Security headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer" always;
# CSP
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';" always;
location / {
proxy_pass http://app_servers;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
}
End-to-End Encryption
// Encrypt data in transit (API to mobile app)
class SecureTransmission {
// Generate ephemeral key pair for each session
async establishSecureChannel(clientPublicKey) {
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
});
// Perform key exchange (Diffie-Hellman)
const sharedSecret = crypto.diffieHellman({
privateKey,
publicKey: clientPublicKey,
});
// Derive session key
const sessionKey = crypto
.createHash('sha256')
.update(sharedSecret)
.digest();
// Store session key (encrypted)
await Session.create({
session_id: uuidv4(),
session_key_encrypted: this.encryptSessionKey(sessionKey),
expires_at: new Date(Date.now() + 15 * 60000), // 15 min
});
return { publicKey: publicKey.export({ type: 'spki', format: 'pem' }) };
}
// Encrypt PHI for transmission
async encryptForTransmission(data, sessionKey) {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', sessionKey, iv);
let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'base64');
encrypted += cipher.final('base64');
const authTag = cipher.getAuthTag();
return {
encrypted,
iv: iv.toString('base64'),
authTag: authTag.toString('base64'),
};
}
}
5. Database Encryption
PostgreSQL Encryption
-- Enable pgcrypto extension
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Create encrypted column type
CREATE TABLE patients (
patient_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Encrypted PHI columns
first_name_encrypted BYTEA,
last_name_encrypted BYTEA,
ssn_encrypted BYTEA,
diagnosis_encrypted BYTEA,
-- Searchable hash columns
ssn_hash VARCHAR(64),
-- Audit fields
created_at TIMESTAMP DEFAULT NOW(),
created_by UUID REFERENCES users(user_id),
updated_at TIMESTAMP DEFAULT NOW(),
updated_by UUID REFERENCES users(user_id)
);
-- Encryption function
CREATE OR REPLACE FUNCTION encrypt_phi(plaintext TEXT, key TEXT)
RETURNS BYTEA AS $$
BEGIN
RETURN pgp_sym_encrypt(plaintext, key, 'cipher-algo=aes256');
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Decryption function
CREATE OR REPLACE FUNCTION decrypt_phi(ciphertext BYTEA, key TEXT)
RETURNS TEXT AS $$
BEGIN
RETURN pgp_sym_decrypt(ciphertext, key);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Insert with encryption
INSERT INTO patients (first_name_encrypted, last_name_encrypted, ssn_encrypted)
VALUES (
encrypt_phi('John', 'encryption-key'),
encrypt_phi('Doe', 'encryption-key'),
encrypt_phi('123-45-6789', 'encryption-key')
);
-- Query with decryption (requires key)
SELECT
patient_id,
decrypt_phi(first_name_encrypted, 'encryption-key') AS first_name,
decrypt_phi(last_name_encrypted, 'encryption-key') AS last_name
FROM patients
WHERE patient_id = '...';
Transparent Data Encryption (AWS RDS)
// Enable encryption at rest for RDS
const rdsConfig = {
DBInstanceIdentifier: 'healthcare-db',
Engine: 'postgres',
EngineVersion: '14.7',
// Enable encryption
StorageEncrypted: true,
KmsKeyId: 'arn:aws:kms:us-east-1:123456789:key/...',
// Encrypted backups
BackupRetentionPeriod: 30,
PreferredBackupWindow: '03:00-04:00',
// Multi-AZ for high availability
MultiAZ: true,
// Enhanced monitoring
EnableCloudwatchLogsExports: ['postgresql'],
MonitoringInterval: 60,
};
6. Disaster Recovery & Backups
Backup Strategy
// Automated encrypted backups
class BackupManager {
async performBackup() {
const timestamp = new Date().toISOString();
const backupPath = `/backups/phi-backup-${timestamp}.sql.enc`;
// 1. Create database dump
await execAsync(`pg_dump ${DB_NAME} > /tmp/backup.sql`);
// 2. Encrypt backup
await execAsync(
`openssl enc -aes-256-cbc -salt -in /tmp/backup.sql -out ${backupPath} -pass pass:${BACKUP_PASSWORD}`
);
// 3. Upload to S3 with encryption
await s3.upload({
Bucket: 'hipaa-compliant-backups',
Key: `backups/${timestamp}.sql.enc`,
Body: fs.createReadStream(backupPath),
ServerSideEncryption: 'aws:kms',
SSEKMSKeyId: process.env.KMS_KEY_ID,
StorageClass: 'GLACIER', // Long-term retention
});
// 4. Verify backup integrity
const hash = await this.calculateFileHash(backupPath);
await BackupLog.create({
timestamp,
file_path: backupPath,
file_hash: hash,
verified: true,
});
// 5. Clean up temp files
await fs.unlink('/tmp/backup.sql');
// 6. Retain backups for 6 years (HIPAA requirement)
await this.cleanOldBackups(6);
console.log(`Backup completed: ${backupPath}`);
}
async restoreBackup(backupFile) {
// Audit the restore operation
await auditLogger.log({
type: 'backup_restore',
user_id: 'system',
action: 'restore_database',
details: { backup_file: backupFile },
});
// Decrypt and restore
await execAsync(
`openssl enc -aes-256-cbc -d -in ${backupFile} -pass pass:${BACKUP_PASSWORD} | psql ${DB_NAME}`
);
}
}
// Run daily backups
cron.schedule('0 2 * * *', async () => {
const backup = new BackupManager();
await backup.performBackup();
});
HIPAA Compliance Checklist
Technical Safeguards
-
Access Control
- Unique user IDs
- Emergency access procedure
- Automatic logoff (15 min inactivity)
- Encryption of PHI at rest (AES-256)
-
Audit Controls
- Log all PHI access
- Log authentication events
- Log system configuration changes
- Retain logs for 6 years
- Regular log review
-
Integrity Controls
- Data integrity verification (checksums)
- Electronic signature capability
- Detect unauthorized changes
-
Transmission Security
- TLS 1.2+ for all transmissions
- End-to-end encryption
- VPN for administrative access
Physical Safeguards
- Facility access controls
- Workstation security policies
- Device encryption
- Media disposal procedures
Administrative Safeguards
- Security risk assessment (annual)
- Workforce training
- Business Associate Agreements (BAAs)
- Incident response plan
- Contingency/disaster recovery plan
Common HIPAA Violations to Avoid
- Unencrypted devices - Encrypt all laptops, phones, tablets
- Weak passwords - Enforce complex passwords + MFA
- No access logs - Log everything
- Unencrypted backups - Always encrypt backups
- Missing BAAs - Sign BAAs with all vendors
- No staff training - Annual HIPAA training required
- Lack of encryption in transit - Use TLS everywhere
- Improper disposal - Securely wipe/destroy media
Tools for HIPAA Compliance
Infrastructure:
- AWS HIPAA-eligible services
- Azure Healthcare APIs
- Google Cloud Healthcare API
Security:
- Vaultree - Encryption
- HashiCorp Vault - Secrets management
- Cloudflare - WAF/DDoS
Compliance:
- Aptible - HIPAA-compliant platform
- Datica - Healthcare compliance
- HIPAA Vault - Compliant hosting
Monitoring:
- Datadog - Infrastructure monitoring
- New Relic - Application monitoring
- Splunk - Log analysis
Key Takeaways
- Encrypt everything - At rest, in transit, in backups
- Log everything - Comprehensive audit trails required
- Least privilege - Role-based access control
- Regular audits - Annual risk assessments mandatory
- Business Associate Agreements - Required for all vendors
- Incident response - Have a documented plan
- Training - Annual workforce training required
- Backups - Encrypted, tested, retained 6+ years
Resources
Conclusion
HIPAA compliance is complex, but achievable with the right architecture and controls. The key is building security and privacy into every layer - from network design to application code to operational procedures.
Remember that HIPAA compliance is not a one-time achievement - it requires ongoing monitoring, testing, training, and improvement. Treat patient data with the respect and protection it deserves.
When in doubt, encrypt. When certain, encrypt anyway.
Published: December 28, 2025