The trust boundary

PR12.8 requires that this be stated in one place, as a table, because a boundary nobody can point at is not a boundary. This is that place, for all six ports.

The table#

Concern Provided here Provided by your deployment
Authentication — who is this
Authorization — may they see it
SMART on FHIR scopes, compartments
Consent evaluation
meta.security label enforcement
Rate limiting per identity
TLS termination for clients
Attribution — recording who acted M3.15, PR12.4 supplies the identity
Disclosure logging — recording who read PR12.5, PR12.6 retains and reviews the log
Tamper-evident history M3.16M3.16e holds the key, keeps the witness off-box
Append-only history in the database M3.17 applies the REVOKE grants
Erasure with a tombstone M3.18 decides when to erase
No PHI in logs at default level O10.2, T11.7 routes and retains logs
Encrypted database transport O10.7 configures and verifies certificates
Transactional writes, snapshot reads R4.4, R4.5
Structural validation V9.1
Terminology validation V9.4 a terminology service
Profile / IG conformance a validator

The seam, and why it is where it is#

These libraries do not authenticate. Identity is established at the deployment perimeter, and that is a deliberate design decision, not an omission to be fixed later.

But "authentication is the perimeter's job" cannot mean "the record of who did what is nobody's job". HIPAA §164.312(b) asks who accessed a record, and no perimeter can answer that: the perimeter knows the identity, and only the store knows which rows were touched. So the split is:

  • The perimeter verifies identity.
  • The store records it — on every write, and on every read.

That is why §12 survives the retirement of the REST layer intact. The service-side half (trusted headers, trusted proxies, require-principal) is marked [service] and describes something this monorepo does not contain (C0.17). The store-side half — PR12.3a, PR12.4, PR12.5, PR12.6, and the Audit value that carries the identity — is in force today.

What you must supply#

An identity, deliberately. There is no default attribution (PR12.3a):

Audit::principal("dr.jones@clinic.example", "header:X-Fhir-Principal")
Audit::cli()
Audit::unattributed()

actor_source records which, so an auditor can distinguish "the perimeter asserted this" from "nobody did". Passing unattributed() is a legitimate choice; it is just one that shows up in the record.

A key, in a file. For the keyed chain tag (M3.16b): at least 32 bytes, readable only by its owner, generated by chain-key-new rather than by a shell redirect that applies the umask. Not an environment variable — that is visible in /proc/<pid>/environ, survives into crash dumps, is reported by orchestrators, and is inherited by every child process.

A place for the witness that the database cannot reach. Chain checkpoints (M3.16c) are only a witness if they land off-box. Logs shipped to a remote collector qualify; logs written to a table in the same database, or to a disk the same compromised account can rewrite, do not. The library cannot enforce this and does not pretend to.

Verified transport. O10.7: the connection carries PHI. Verify the server certificate; verify-full or the engine's equivalent is the production setting. SQLite is the exception — no connection to encrypt, so the obligation is at-rest: file permissions and storage encryption.

What is honestly not covered#

Stated rather than papered over, per §13:

  • Authorization. Nothing here evaluates whether a principal may see what it asked for. The store records that it did.
  • Terminology. A required binding is enforced as the literal set of codes the generator extracted (M3.7). No code system is expanded, no subsumption computed, no $validate-code performed (V9.4). An unstated gap in validation reads, to an auditor, as a validation claim.
  • Certification. These are components. The compliance mapping shows which requirements exist to support an obligation; whether a deployment meets it depends on the perimeter, the procedures, and the evidence retained.

What the hash chain does and does not do#

The most over-claimable thing here, so stated narrowly (M3.16b):

Unkeyed, it detects careless or unaware modification — a migration, a stray UPDATE, a row restored from the wrong backup — and it supports an external witness, because a chain head recorded off-box makes truncation and wholesale rewriting detectable even against an attacker who can recompute digests.

Unkeyed, it does not stop an informed attacker with write access. The digests are unkeyed over a published pre-image, so anyone who can write the row can compute a correct digest for it.

Keyed, it does — as long as the key is somewhere the attacker is not. A key stored where they already have write access protects nothing.

Neither catches a row that is gone. A chain missing its most recent version verifies perfectly, because nothing left behind refers to what was removed. Only the checkpoint closes that gap, and only if it lands off-box.

Per-port caveats#

The table above describes what the specification requires. What each port has verified is narrower — see the conformance matrix:

  • The audit, redaction, and concurrency tests exist only in fhir-postgresql. Four ports carry ? on those rows.
  • fhir-postgresql still derives its chain pre-image in SQL, so its chains are not portable (F-07).
  • fhir-mssql and fhir-oracle have no store, so none of this applies to them yet.

Where authentication actually happens#

The six database libraries do not authenticate, and that has not changed: they take an Audit value and record it. What changed is that this repository now ships a service which supplies one.

fhir-loco verifies a PASETO v4.public token on every request. There is no unauthenticated mode and no header fallback — the process refuses to boot without an issuer public key, and a request without a valid token is 401 before any handler runs (PR12.9).

So the row that reads "Authentication — your deployment must provide" is true of the libraries and of any service you write yourself. If you run fhir-loco, authentication is provided; authorization — scopes, compartments, consent — still is not, and remains yours.

Why a token rather than the trusted header the libraries' Audit was designed around: a header is only as trustworthy as the guarantee that nothing else can reach the port, and that guarantee lives in network configuration the service cannot see, cannot verify, and cannot fail loudly about. PR12.2 requires the header be honoured only from a configured trusted proxy; fhir-loco never implemented that check, so under PR12.2 its header should have been ignored rather than trusted. A signature it can verify itself is a property of the request, not an assumption about the topology.

Edit this page on GitHub — the repository is the source of truth; this site renders it.