HIPAA §164.312 access control evidence export

This page specifies the evidence export that proves two HIPAA Security Rule technical safeguards at once over electronic protected health information: unique user identification under §164.312(a)(2)(i) and access control under §164.312(a)(1) — built from RBAC drift records so shared accounts and over-broad ePHI access surface automatically.

HIPAA §164.312 is distinctive among the frameworks this section covers because it demands that every principal touching ePHI be a uniquely identifiable user, not a shared service login. A SOC 2 reviewer accepts analytics_ro as a role; a HIPAA assessor asks whether the humans behind it can be individually attributed. The export below therefore carries a field no other report needs — a unique-user-identification verdict per principal — alongside the access-control facts, both projected from the canonical audit report artifact formats record and scoped to ePHI-tagged objects.

When to use this — and when not to

Use the §164.312 evidence export when:

  • Your databases hold ePHI (patient, claims, or clinical objects) and an assessor wants technical-safeguard evidence, not just a policy document.
  • You can classify database objects as ePHI and can distinguish uniquely-identified user accounts from shared or service accounts.
  • You need to prove both access control and unique user identification from the same access facts, in one file.

Do not reach for this when:

  • The data is cardholder data, not ePHI — the least-privilege framing a QSA wants lives in the PCI Requirement 7 attestation.
  • You have no way to tell a human account from a shared one. The unique-user-id verdict is the whole point; without an account-type signal the export cannot make the §164.312(a)(2)(i) claim.
  • You only need a general periodic review — the SOC 2 JSON document is lighter and sufficient.
Building the HIPAA §164.312 access-control evidence export Canonical evidence records enter on the left and pass an ePHI filter keeping only health-information objects. Each surviving principal is classified by an identity check as a uniquely-identified user or a shared account. An access-control check confirms the grant maps to an assigned role. The result is a JSON export carrying, per ePHI grant, both the unique-user-identification verdict for §164.312(a)(2)(i) and the access-control safeguard for §164.312(a)(1). canonical records all objects ePHI filter sensitivity == ephi identity check unique user vs shared access control grant → assigned role §164.312(a)(1) safeguard verdict JSON export unique_user_id + access_granted
Figure 1 — The export proves two safeguards from one record: unique user identification and role-based access control over ePHI.

Step-by-step implementation

Step 1 — Model the ePHI evidence entry with pydantic v2

The entry pairs the access fact with the two safeguard verdicts. Modeling unique_user_id as a required boolean forces the export to make an explicit claim about identifiability for every ePHI grant.

from datetime import datetime, timezone
from typing import Literal
from pydantic import BaseModel, field_validator

AccountType = Literal["individual", "shared", "service"]

class EphiAccessEntry(BaseModel):
    principal: str
    ephi_object: str                     # schema.table holding ePHI
    privilege: str
    account_type: AccountType
    unique_user_id: bool                 # §164.312(a)(2)(i) verdict
    access_granted_via: str              # "role:<name>" or "direct"
    access_control_ok: bool              # §164.312(a)(1) verdict
    observed_at: datetime
    evidence_hash: str

    @field_validator("observed_at")
    @classmethod
    def utc_only(cls, v: datetime) -> datetime:
        if v.tzinfo is None:
            raise ValueError("observed_at must be timezone-aware UTC")
        return v

class EphiEvidenceExport(BaseModel):
    framework: Literal["HIPAA-164.312"] = "HIPAA-164.312"
    covered_entity: str
    generated_at: datetime
    entries: list[EphiAccessEntry]

Step 2 — Classify identity and derive both verdicts

Unique user identification passes only for individual accounts; a shared or service account touching ePHI is a §164.312(a)(2)(i) finding. Access control passes when the grant arrives through an assigned role rather than a direct ad-hoc grant.

def build_entry(record: dict, *, account_type: str) -> EphiAccessEntry:
    unique_ok = account_type == "individual"
    via = record.get("access_granted_via", "direct")
    access_ok = via.startswith("role:")           # role-mediated, not ad-hoc direct grant
    return EphiAccessEntry(
        principal=record["principal"],
        ephi_object=record["object"],
        privilege=record["privilege"].upper(),
        account_type=account_type,                # type: ignore[arg-type]
        unique_user_id=unique_ok,
        access_granted_via=via,
        access_control_ok=access_ok,
        observed_at=record["observed_at"],
        evidence_hash=record["evidence_hash"],
    )

Verify a shared account fails identity while an individual role-based account passes both:

from datetime import datetime, timezone
now = datetime(2026, 7, 18, 12, 0, tzinfo=timezone.utc)

shared = build_entry({"principal": "clinic_shared", "object": "phi.encounters",
                      "privilege": "SELECT", "access_granted_via": "role:clinician_ro",
                      "observed_at": now, "evidence_hash": "d1"}, account_type="shared")
indiv  = build_entry({"principal": "e.rivera", "object": "phi.encounters",
                      "privilege": "SELECT", "access_granted_via": "role:clinician_ro",
                      "observed_at": now, "evidence_hash": "d2"}, account_type="individual")
print(shared.unique_user_id, shared.access_control_ok)   # -> False True
print(indiv.unique_user_id,  indiv.access_control_ok)    # -> True True

Step 3 — Serialize the ePHI export deterministically

Emit the whole export with sorted keys. Pydantic v2 model_dump(mode="json") renders the datetimes and the literal framework tag; json.dumps with sort_keys keeps period-over-period diffs meaningful.

import json

def to_json(export: EphiEvidenceExport) -> str:
    return json.dumps(export.model_dump(mode="json"), indent=2, sort_keys=True)

Expected output for one shared-account finding:

{
  "covered_entity": "Northside Health",
  "entries": [
    {
      "access_control_ok": true,
      "access_granted_via": "role:clinician_ro",
      "account_type": "shared",
      "ephi_object": "phi.encounters",
      "evidence_hash": "d1",
      "observed_at": "2026-07-18T12:00:00+00:00",
      "principal": "clinic_shared",
      "privilege": "SELECT",
      "unique_user_id": false
    }
  ],
  "framework": "HIPAA-164.312",
  "generated_at": "2026-07-18T12:00:00+00:00"
}

Worked example: a shared clinic login on PostgreSQL 15

Scenario: PostgreSQL 15. The phi schema holds encounters. Access is granted through the clinician_ro role, which is correct role-based access control. But one grantee, clinic_shared, is a shared front-desk login used by several staff — a direct violation of unique user identification even though the access-control safeguard itself is satisfied.

records = [
    {"principal": "e.rivera", "object": "phi.encounters", "privilege": "SELECT",
     "access_granted_via": "role:clinician_ro", "observed_at": now, "evidence_hash": "d2"},
    {"principal": "clinic_shared", "object": "phi.encounters", "privilege": "SELECT",
     "access_granted_via": "role:clinician_ro", "observed_at": now, "evidence_hash": "d1"},
]
account_types = {"e.rivera": "individual", "clinic_shared": "shared"}

export = EphiEvidenceExport(
    covered_entity="Northside Health", generated_at=now,
    entries=[build_entry(r, account_type=account_types[r["principal"]]) for r in records],
)
findings = [e.principal for e in export.entries if not e.unique_user_id]
print(findings)   # -> ['clinic_shared']

The export shows both grantees with satisfied access control, but isolates clinic_shared as the §164.312(a)(2)(i) finding. That distinction — good access control, failed unique identification — is exactly what a HIPAA assessor needs and what a raw grant list cannot express.

Gotchas and engine-specific notes

Shared accounts hide as ordinary roles. Neither PostgreSQL nor MySQL marks a login as “shared” — the account-type signal must come from your identity source (HR system, IdP), joined in by principal. Defaulting an unknown account to individual silently passes the very finding this export exists to catch; default unknowns to shared so they fail closed.

Role membership must resolve to real humans. PostgreSQL pg_auth_members tells you which login roles belong to clinician_ro, but a login role can itself be a service account. Walk membership to the leaf login and classify that, not the group role, when deciding unique_user_id.

rolcanlogin distinguishes accounts from group roles. In PostgreSQL, only roles with rolcanlogin = true are actual accounts; a NOLOGIN group role is not a principal that identifies a user. Filter on pg_roles.rolcanlogin before classifying, or you will attribute ePHI access to a role no human logs in as. MySQL has no NOLOGIN equivalent, so treat every mysql.user row as a potential account and lean on the identity join.

ePHI classification, not table names, decides scope. A notes column in a scheduling table can be ePHI while a patients_archive table is de-identified. Source the ePHI flag from the sensitivity tier in privilege scope mapping, never from the object name.

Compliance note

This export evidences HIPAA §164.312(a)(1) (access control — allow access only to authorized persons or software) and §164.312(a)(2)(i) (unique user identification — assign a unique name or number for identity tracking) over ePHI objects. Each entry states, per grant, whether access was role-mediated and whether the accessing principal is uniquely identifiable, turning the drift record set into a technical-safeguard attestation. The evidence_hash binds each entry to its canonical record for the tamper-evidence guarantees of evidence integrity and signing, while the underlying deviations are ranked by rule-based drift scoring. The HIPAA Security Rule §164.312 defines both safeguards.

Frequently asked questions

How does the export prove unique user identification? It joins each ePHI grantee to an account-type signal from your identity source and sets unique_user_id true only for individual accounts. A shared or service account touching ePHI is flagged false, which is the §164.312(a)(2)(i) finding an assessor looks for.

Can access control pass while unique user identification fails? Yes, and that pairing is the point. A shared login reached through a correctly assigned role satisfies §164.312(a)(1) access control but fails §164.312(a)(2)(i) identity. Modeling the two verdicts separately lets the export show a well-controlled grant that still violates unique identification.

Where does the account-type classification come from? From your identity provider or HR system, joined by principal — the database itself does not record whether a login is shared. Default unknown accounts to shared so they fail closed rather than silently passing the identity check.

Do NOLOGIN group roles appear in the export? No. Filter on rolcanlogin in PostgreSQL so only real login accounts are classified; a NOLOGIN group role is an access path, not a user, and attributing ePHI access to it would misstate who can reach the data.

Up: Audit Report Artifact Formats