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.
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.
Related
- SOC 2 access review evidence in JSON — the general periodic-review projection.
- PCI DSS Requirement 7 least-privilege attestation report — the cardholder-data CSV attestation.
- Privilege Scope Mapping — the sensitivity classification that flags ePHI objects.