Implement HIPAA-compliant technical safeguards in your healthcare applications. Covers PHI handling, encryption requirements, access controls, audit logging, and breach response for engineering teams.
HIPAA Technical Controls for Engineers is a reference for the specific, actionable engineering work required to meet the technical requirements of the Health Insurance Portability and Accountability Act (HIPAA), particularly as they apply to data systems handling protected health information (PHI). This page details the day-to-day implementation, validation, and maintenance of HIPAA’s technical safeguards: access controls, audit controls, integrity controls, encryption, and incident response. It is used when designing, deploying, or verifying systems that must comply with HIPAA’s Security Rule, especially in regulated environments such as electronic health record (EHR) platforms, patient portals, and clinical data pipelines.
Access controls ensure that only authorized users can access PHI, and that their access is auditable and time-bound. The core pattern is a role-based access control (RBAC) system where users are assigned roles, and roles are mapped to permissions over PHI data.
Each user’s access must be reviewed at least annually. A standard implementation uses an identity provider (IdP) that emits SAML or OIDC tokens, with claims for user roles and department affiliations.
To enforce access, systems must validate the user’s role at every request. For example, a web service in Go using the github.com/golang-jwt/jwt/v5 library might verify a JWT token with the following middleware:
func requireRole(role string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Get("user").(*jwt.Token)
claims := token.Claims.(jwt.MapClaims)
userRoles, ok := claims["roles"].([]interface{})
if !ok {
return echo.NewHTTPError(http.StatusForbidden, "missing roles claim")
}
rolePresent := false
for _, r := range userRoles {
if r == role {
rolePresent = true
break
}
}
if !rolePresent {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("user lacks required role: %s", role))
}
return next(c)
}
}
}
The system must also enforce time-limited sessions. A session is considered active if the user has made a request within the last 15 minutes. After 30 minutes of inactivity, the session expires and must be revalidated.
When a user accesses a patient’s record, the system logs the session ID, timestamp, IP address, and requested resource. This log is stored in a table with the schema:
CREATE TABLE access_log (
id BIGSERIAL PRIMARY KEY,
session_id UUID NOT NULL,
user_id UUID NOT NULL,
patient_id UUID NOT NULL,
resource_path TEXT NOT NULL,
action TEXT NOT NULL, -- "view", "edit", "download"
ip_address INET NOT NULL,
timestamp TIMESTAMPTZ NOT NULL,
user_agent TEXT,
duration_ms INTEGER
);
Failure modes:
user_roles claim is not an array, the middleware returns a 403 with message missing roles claim.roles claim is a string instead of an array, the system might accept ["nurse"] but fail silently when given {"roles": "nurse"}.Audit trails capture all access and modification events on PHI. The audit trail must include at least the following fields: event type, timestamp, user, action, object, and result.
Each audit event is written to a durable, immutable log. The system uses a message queue (e.g., Apache Kafka or AWS Kinesis) where each event is a JSON record:
{
"eventId": "a9b4c3d2-e1f0-11ed-861d-0242ac120002",
"eventType": "PHI_UPDATE",
"timestamp": "2023-11-14T10:32:15.123Z",
"userId": "e8f9a0b1-c2d3-4e5f-6a7b-8c9d0e1f2a3b",
"userName": "[email protected]",
"patientId": "f1e2d3c4-b5a6-7b8c-9d0e-1f2a3b4c5d6e",
"resourceType": "Encounter",
"resourceId": "c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f",
"oldValue": {
"status": "scheduled",
"notes": "Patient arrived at clinic."
},
"newValue": {
"status": "completed",
"notes": "Patient seen by Dr. Lee. Prescribed antibiotics."
},
"result": "SUCCESS",
"sourceIp": "192.168.1.100",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
}
The audit trail is written synchronously during a write operation. For example, when a clinician saves a patient’s lab results:
func saveLabResults(ctx context.Context, patientID uuid.UUID, results []LabResult) error {
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
return err
}
defer tx.Rollback()
// Update lab results in database
_, err = tx.Exec(`
INSERT INTO lab_results (patient_id, test_type, value, recorded_at)
VALUES ($1, $2, $3, $4)
`, patientID, "CBC", "12.4", time.Now())
if err != nil {
return err
}
// Log audit event
auditEvent := map[string]interface{}{
"eventId": uuid.New().String(),
"eventType": "PHI_UPDATE",
"timestamp": time.Now().UTC().Format(time.RFC3339Nano),
"userId": ctx.Value("userID").(uuid.UUID).String(),
"userName": ctx.Value("userName").(string),
"patientId": patientID.String(),
"resourceType": "LabResults",
"resourceId": "c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f",
"oldValue": nil,
"newValue": results,
"result": "SUCCESS",
"sourceIp": ctx.Value("clientIP").(string),
"userAgent": ctx.Value("userAgent").(string),
}
err = sendMessageToAuditTopic(ctx, "audit-log", auditEvent)
if err != nil {
return err
}
return tx.Commit()
}
The audit system must also support event retention for at least six years. The retention policy is driven by a scheduled job that runs daily:
0 2 * * * /usr/local/bin/audit-retention --max-age 2190 --source kafka --topic audit-log --storage s3
The job reads from the message queue, groups events by patientId, and archives them to a compressed, versioned archive in S3.
Common failure modes:
eventId is not unique, duplicate events are created, leading to multiple entries for the same action.sourceIp field is often missing or malformed, especially when requests pass through reverse proxies.Integrity controls ensure that PHI is not altered without detection. Systems must validate data integrity before and after transmission or storage.
Each PHI record must have a cryptographic digest (e.g., SHA-256) computed and stored. The digest is recalculated on every read, and the system compares the stored digest against the current value. If they differ, the record is considered corrupted.
For example, a database trigger in PostgreSQL ensures integrity on a patient table:
CREATE OR REPLACE FUNCTION check_patient_integrity()
RETURNS TRIGGER AS $$
DECLARE
current_digest BYTEA;
expected_digest BYTEA;
BEGIN
-- Compute SHA-256 of all non-null fields
SELECT encode(
digest(
coalesce(NEW.first_name, '') ||
coalesce(NEW.last_name, '') ||
coalesce(NEW.dob, '') ||
coalesce(NEW.ssn, '') ||
coalesce(NEW.address, '') ||
coalesce(NEW.phone, ''),
'sha256'
), 'hex'
) INTO current_digest;
-- Retrieve expected digest
SELECT digest FROM patient_digests WHERE patient_id = NEW.id INTO expected_digest;
IF expected_digest IS NULL THEN
-- First time: insert new digest
INSERT INTO patient_digests (patient_id, digest) VALUES (NEW.id, current_digest);
ELSIF expected_digest != current_digest THEN
-- Integrity check failed
INSERT INTO integrity_alerts (patient_id, old_digest, new_digest, timestamp)
VALUES (NEW.id, expected_digest, current_digest, now());
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER patient_integrity_trigger
AFTER INSERT OR UPDATE ON patient
FOR EACH ROW EXECUTE FUNCTION check_patient_integrity();
The system must also encrypt PHI at rest and in transit. For encryption, the following configuration is standard:
A service using nginx as a reverse proxy configures TLS with the following directives:
server {
listen 443 ssl http2;
server_name ehr.example.com;
ssl_certificate_file /etc/ssl/certs/ehr.pem;
ssl_certificate_key_file /etc/ssl/private/ehr.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA256;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Certificate pinning (HPKP)
add_header Public-Key-Pins '
pin-sha256="base64==456789abcde123456789abcdef123456789abcdef123456789abcdef123456789==";
max-age=5184000;
includeSubDomains;
report-uri="https://report.example.com/hpkp-report";
' always;
location / {
proxy_pass http://app-server:8080;
proxy_set_header Host $host;
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 $scheme;
}
}
The system must also validate that all PHI data is encrypted before being stored. A periodic job checks that all patient records have a encrypted_at timestamp and a key_id field.
When a record is marked as encrypted, the system must ensure that the ssn, address, and medical_history fields are stored as ciphertext in the database.
Failure modes:
digest column in patient_digests is not indexed on patient_id, causing integrity checks to take seconds for large datasets.ssl_ciphers directive includes a cipher suite that is not supported by the server, leading to TLS handshake failures.When a security incident is detected (e.g., a data breach, unauthorized access, or system failure), the system must follow a defined incident response process.
Incidents are logged in a centralized event table with the following schema:
CREATE TABLE security_incident (
id BIGSERIAL PRIMARY KEY,
incident_id UUID UNIQUE NOT NULL,
type TEXT NOT NULL,
severity TEXT NOT NULL, -- "LOW", "MEDIUM", "HIGH", "CRITICAL"
title TEXT NOT NULL,
description TEXT,
detected_at TIMESTAMPTZ NOT NULL,
reported_by UUID,
resolved_at TIMESTAMPTZ,
status TEXT NOT NULL, -- "NEW", "IN_PROGRESS", "RESOLVED", "CLOSED"
metadata JSONB
);
An incident is triggered by a monitoring system such as Prometheus. A rule in prometheus.yml detects a spike in failed login attempts:
groups:
- name: authentication
rules:
- alert: HighLoginFailures
expr: |
rate(auth_login_failures_total{job="ehr-app"}[15m]) > 50
for: 10m
labels:
severity: "HIGH"
annotations:
summary: "High login failure rate detected"
description: |
More than 50 failed login attempts in the last 15 minutes.
Current rate: {{ $value }}.
Source: {{ $labels.job }}
When an incident is created, a Slack message is sent using curl:
curl -X POST \
-H "Content-Type: application/json" \
-d '{
"text": "🚨 Incident: HighLoginFailures",
"attachments": [
{
"title": "Details",
"fields": [
{
"title": "Incident ID",
"value": "INC-2023-11-14-001"
},
{
"title": "Severity",
"value": "HIGH"
},
{
"title": "Detected At",
"value": "2023-11-14T11:23:45Z"
},
{
"title": "Link",
"value": "https://incident.example.com/INC-2023-11-14-001"
}
]
}
]
}' \
https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
The incident response team must resolve the incident within SLA windows:
Failure modes:
incident_id is not unique, leading to duplicate entries when multiple alerts fire simultaneously.metadata column is not indexed, causing slow queries when filtering incidents by tags or user IDs.Compliance is not a one-time setup. It must be maintained through continuous monitoring, automated checks, and documentation.
A compliance report is generated monthly. It includes:
This report is generated by a script that runs once a month:
0 0 1 * * /usr/local/bin/compliance-report \
--output /reports/hipaa-2023-11.pdf \
--from "2023-10-01T00:00:00Z" \
--to "2023-11-01T00:00:00Z" \
--include audit,encryption,access,incident
The report is stored in a versioned repository using git:
cd /reports
git add .
git commit -m "HIPAA compliance report for November 2023"
git push origin main
The report is also published to a dashboard via curl:
curl -X POST \
-H "Authorization: Bearer $DASHBOARD_TOKEN" \
-H "Content-Type: application/json" \
-d '{"reportId": "hipaa-2023-11", "title": "HIPAA Compliance Report", "url": "https://reports.example.com/hipaa-2023-11.pdf"}' \
https://dashboard.example.com/api/reports
Failure modes:
--from and --to timestamps are not in UTC, causing reports to be generated for incorrect time windows.git push fails, but the script does not check the exit code, so a failed report is silently skipped.This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy