HIPAA Technical Controls for Engineers

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.

Implementing Access Controls for PHI

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:

Configuring Audit Trails for PHI Events

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:

Enforcing Data Integrity and Encryption

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:

Responding to Security Incidents

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:

Maintaining Compliance Over Time

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:

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.