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.16–M3.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
requiredbinding is enforced as the literal set of codes the generator extracted (M3.7). No code system is expanded, no subsumption computed, no$validate-codeperformed (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-postgresqlstill derives its chain pre-image in SQL, so its chains are not portable (F-07).fhir-mssqlandfhir-oraclehave 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.