Skip to main content

Jurisdictional Lineage Attestation

Per-request signed claims about the model, region evidence and log jurisdiction. Verification establishes integrity of the recorded claim; it does not independently establish the physical location of inference. Ed25519-signed. Append-only, tamper-evident ledger.

Architecture​

Gateway:
┌─ RegionResolver ─→ header evidence / pinned endpoint → ISO-3166 region + evidence tier
├─ KeyStore ───────→ EnsureKeyPair(orgID) → Ed25519 keypair (AES-GCM encrypted)
├─ LedgerStore ────→ Append(attestation, signature, keyID)
└─ EventPublisher ─→ InferenceRegion, ModelOriginCountry, LogStorageRegion,
ProviderEntityJurisdiction, AttestationHash, Signature, KeyID

Loggerd:
└─ Consumer ───────→ Extracts lineage fields from Redis event → request_lineage table

Admin API:
GET /api/lineage/{request_id} → attestation JSON
POST /api/lineage/{request_id}/verify → {valid: bool}
GET /api/organizations/{id}/lineage → recent attestations

Cryptography​

  • Algorithm: Ed25519 (stdlib crypto/ed25519)
  • Hash target: hex SHA-256 of canonical attestation JSON (json.Marshal of struct)
  • Key storage: private key AES-GCM encrypted with org encryption key (enc: prefix)
  • Determinism: Go marshals struct fields in source order → byte-identical across builds

Evidence tiers (what the region field actually proves)​

inference_region in the attestation is graded. The attestation carries the tier it was resolved at (region_evidence), so a reviewer never has to guess how strong the claim is:

TierMeaningExample
endpoint_pinnedThe destination endpoint itself encodes the region — the gateway sent the request to a region-specific resource and records that factself-hosted vLLM/llama.cpp; Azure OpenAI Japan East; Bedrock ap-northeast-1; any model registered with a non-GLOBAL region
header_observedResponse headers provide a location signal, which may identify a CDN edge rather than inference computecf-ray colo suffix, x-served-by
provider_attestedThe provider explicitly attests the region via the trusted x-yabanin-region opt-in headerprovider opt-in integrations
provider_declaredThe vendor does not disclose serving regions; the value is the provider's registered defaultglobal endpoints like api.openai.com

Honest scope: for global vendor endpoints, no provider discloses the physical processing location — the attestation records provider_declared rather than implying knowledge it does not have. Residency packs (fail-closed) let an org forbid provider_declared-tier models entirely, which is the correct posture for region-pinned buyers.

Database schema​

TablePurpose
request_lineage (baseline 0001_init)Append-only ledger, tamper-evident (UPDATE/DELETE blocked by trigger)
provider_jurisdictions (baseline 0001_init)Provider→country mapping with seed data
org_lineage_keys (baseline 0001_init)Per-org Ed25519 keypairs, versioned, revocable

Configuration​

gateway:
lineage_enabled: true # PB1.6 — gate all lineage functionality

When disabled (false or unset), the gateway skips region resolution and signing entirely — zero latency overhead.

Relationship to other features​

Lineage and residency are complementary, not redundant:

  • Lineage records region evidence after the request, with a verifiable signature.
  • Residency prevents data from going to the wrong jurisdiction (before the fact, via policy enforcement). See Residency policy-packs.

Lineage integrates with the following deployment components; their presence alone does not establish legal compliance:

  • Multi-region active-active — data pinned to a home region via Postgres Row-Level Security; lineage attests the region per request.
  • Air-gap / on-prem — single-tenant deployments with bundled KMS and license/PKI; lineage keys never leave the customer environment.
  • Sovereign-model registry — model_origin_country is part of the signed attestation, as a recorded model-registry claim.
  • Admin audit log — mutations are written to admin_audit_logs as tamper-evident hash-chained rows (wired since AF-10, 2026-10-02: the admin write path appends through the shared protocol auditlog.AppendInTx — pkg/adminstore.InsertAuditLogEntry — with persisted before/after/reason snapshots and an explicit hash_version, migration 0019; the chain is verified end-to-end against a real Postgres by integration tests, including concurrency and tamper detection). The JIT-access package (pkg/jitaccess) exists in-tree but is not wired into the admin write path; do not present it as shipped. Formal SOC 2 / ISO 27001 control worksheets are internal drafts, not certifications.

Durability & loss-window (design decision)​

The gateway commits each attestation through pkg/lineage.AsyncLedger: a bounded in-memory queue (capacity 1024) drained by a single background worker that writes in batches (pgx.Batch, flush every 100 ms or 100 rows). Because the ledger is evidence (audit/compliance), the design contract is lossless-by-fallback: the queue never drops, and any batch failure degrades to synchronous single-row appends. This section records the failure matrix and the outbox decision that bounds (or removes) the in-memory window.

Write path (today)​

request ──sign──► AsyncLedger.Append
│ queue has room? ──yes──► chan (≤1024) ──► worker ──► AppendBatch
│ no └─batch err─► sync Append (per row)
└──no (queue full)────────────► sync Append (in caller)
graceful shutdown ──► Close(5s) ──► ctx.Done ──► drain queue ──► final flush

Failure matrix​

FailureBehaviorLoss windowNotes
Graceful stop (SIGTERM)Close(5s) cancels the worker ctx; the worker drains the queue and does a final flush before exiting~0 (bounded by the 5 s drain deadline)The 5 s budget covers both srv.Shutdown and the drain; a DB that is up at shutdown writes everything queued.
Hard kill (SIGKILL / OOM / power)No drain runs; the in-memory queue is goneup to 1024 attestations (the whole queue) + at most one in-flight batchThe only true-loss case. Bounded by queue capacity, not by time.
DB outage (Postgres down)Batch flush fails → each item falls back to a synchronous Append, which also fails while the DB is down0 lost, N unrecorded — items fail fast and are logged as lineage append lost; they are not re-queuedDegrades to "best effort while DB is down": the request succeeds (lineage is best-effort per-request), the attestation is logged as lost. When the DB returns, new appends succeed again.
Queue full (consumer slower than producers)Append falls through to a synchronous single-row write in the request path0Adds request latency under back-pressure instead of dropping; the ledger is evidence, so we never drop.
Batch write error (DB up, transient)Retry each item synchronously0 (per-row retry; a row that still fails is logged lineage append lost)Converts a batch failure into per-row attempts so one bad row doesn't take the batch with it.
Worker crash / panicWorker goroutine exits; subsequent Append calls find the queue filling and fall back to synchronous writes0 (after the queue fills)The default branch of Append is the safety net: even with no live worker, writes go straight to the DB.

Key property: the loss window is memory-bounded, not time-bounded. The worst case is a hard kill of a process that has a full 1024-deep queue; in all DB-side failure modes the ledger either writes synchronously or fails the attestation loudly (logged), never silently dropping.

Decision: outbox table + consumer (adopted) vs. synchronous append​

We evaluated replacing the in-memory queue with a transactional outbox: write the attestation row into an outbox table in the same Postgres transaction as the request log, then have a consumer relay outbox rows into request_lineage.

CriterionIn-memory AsyncLedger (today)Outbox table + consumer
Hard-kill loss windowup to 1024 attestations0 (row is in the same committed txn as the request log)
DB-outage behaviorattestation lost (logged) while DB downcannot happen — if the DB is down, the request-log txn doesn't commit either, so there is no outbox row to lose; consistency preserved
Latency on the request path+0 (async); +1 round-trip only under queue-full back-pressure+1 INSERT in the request txn (small, same connection)
Ordering / at-least-onceat-most-once per process life (queue is volatile)at-least-once (consumer must dedupe on request_id); needs idempotent consumer
Complexitylow (one goroutine, one channel)higher (outbox schema, consumer, dedupe, backlog monitoring, DLQ)
Backlog visibilityqueue depth (log-only)queryable table (count, oldest-age, per-org)

Chosen: outbox table + consumer. Rationale: the lineage ledger is compliance evidence, and the one mode where the current design loses data — a hard kill with a full queue — is exactly the mode an audit would probe. The outbox removes that window entirely and makes "DB down" a non-issue (no orphaned evidence), at the cost of one in-transaction insert and a dedupe-aware consumer. The in-memory AsyncLedger remains a valid fast path for the steady state; the outbox is the durability backstop.

Loss window with the outbox: zero for both hard-kill and DB-outage. The only residual gap is a request-log transaction that commits but whose outbox→lineage relay has not yet run — that is a delay, not a loss (the row is durable in the outbox and will be relayed; the consumer is idempotent).

Status (shipped): the outbox is implemented (baseline 0001_init, pkg/lineage/outbox.go). AsyncLedger.Append now writes the attestation to lineage_outbox — durable once it returns — and a background relay (RelayLoop) moves pending rows into request_lineage. The relay is the single writer of chained rows (a batch writer racing it could interleave per-org chain links) and is idempotent on request_id (INSERT … ON CONFLICT DO NOTHING, then mark relayed_at). Relay work is bounded per pass (LIMIT 500); a failed row stays pending and is retried; a malformed row is marked relayed and logged loudly rather than wedging the pipeline. DB-outage behavior: Append returns an error (no false success). Kill/restart/DB-outage testcontainers tests are gated on Docker.

Out-of-scope / assumptions (inputs to the decision)​

  • The request log and the outbox row must share one Postgres transaction (same *pgx.Tx); the consumer relays outbox → request_lineage and is idempotent on request_id (unique key).
  • request_lineage already has a tamper-evident trigger (UPDATE/DELETE blocked); the outbox table is a separate, mutable staging table and is not part of the signed evidence chain — only the relayed request_lineage row is.
  • A follow-up work item implements the outbox schema + consumer and flips the gateway to the in-txn write; this section records the decision that gates it.

Tamper-evidence: hash chain + WORM export​

There are two independent hash chains in this system. Do not mix them up:

ChainTableScopeOriginStatus
Request lineagerequest_lineageper-org, every inference requestbaseline 0001_initshipped (chain + verify + export)
Admin audit logadmin_audit_logsper-operator, admin mutationsbaseline 0001_init; chain protocol wired since AF-10 (0019_audit_snapshots_chain)shipped (chain written by the admin write path; verify — integration tests)

They are separate append-only ledgers with separate schemas, separate writers, and separate verification. This section documents the request-lineage chain only. The admin_audit_logs table is a different concern (admin action evidence) with its own chain protocol — see the note above.

How the request-lineage chain works​

Each request_lineage row carries two extra columns (baseline 0001_init):

  • record_hash — sha256(prev_hash || canonical_record), the row's own link hash. A fixed-order, |-delimited canonical_record string (over the INSERT columns) is the single source of truth shared by the writer and the verifier.
  • prev_hash — the previous row's record_hash (NULL for the chain head).

Because every row embeds the previous row's hash, any edit, insertion, or deletion in the middle of an org's history breaks every link after it. The writer (pkg/lineage/ledger.go) computes the chain head per org on append; the async-ledger writer is single-goroutine, so per-org commit order is stable.

Legacy rows written before the chain shipped have record_hash = ''; the verifier counts and skips them (they are not part of the chain) rather than treating them as breakage.

Verification​

yb lineage verify --pg-uri <url> --org <org-id> walks an org's rows in timestamp order, recomputes each chained row's hash, and checks that prev_hash matches the previous record_hash (NULL head). It reports the row count, the chained/legacy split, and whether the chain is intact (and, if broken, the offending row). It exits non-zero on a broken chain so it can be used as a periodic compliance check.

WORM export​

yb lineage export --pg-uri <url> --org <org-id> --out <file.jsonl> [--s3 <bucket> --region <r> --lock-compliance --lock-days <n>]

  • Writes the org's rows as JSONL, one object per row, plus a sha256 checksum of the file.
  • With --s3 it uploads the archive and (with --lock-compliance --lock-days N) sets an S3 Object Lock in COMPLIANCE mode for N days — nobody, including a root account, can alter or delete the export during the retention window. This is the "write once, read many" (WORM) evidence copy: you can keep the live DB mutable for operations while retaining an immutable, independently verifiable snapshot.

Verifying an export​

An exported JSONL file is self-describing: recompute each row's record_hash from its canonical_record + the previous row's record_hash and confirm the sha256 file checksum matches. A compliant WORM export therefore doubles as an offline audit artifact that does not require read access to the live database.