Ledger-as-a-Service (traust_ledger.service) is a FastAPI app, run via:
uvicorn traust_ledger.service.app:create_app --factory --host 0.0.0.0 --port 8000Config is entirely LAAS_* environment variables (ServiceSettings in
traust_ledger.service.settings, which extends ServiceConfig with pydantic-settings).
See ../.env.example for the full list with defaults; auth/signing
seams are documented in auth.md and service-identity.md.
The internal Backend protocol supports FileBackend (default, zero dependencies,
JSON on disk) and DbBackend (SQLite or PostgreSQL through SQLAlchemy). Select it
with LAAS_BACKEND_TYPE=file|db plus LAAS_DATABASE_URL. SQLite uses a dedicated
Ledger file; PostgreSQL uses schema traust_ledger. The implemented revisioned
schema and append-only guards are documented in data-model.md.
Containerfile: multi-stage uv build, runs as non-root (laas, uid 1001), ships
cosign for signing without a sidecar (the key itself is mounted separately, never
baked into the image). /healthz backs the HEALTHCHECK.
| File | Notes |
|---|---|
deployment.yaml |
replicas: 1 — single-writer design, exactly one replica holds the write lock. Pod runs as uid/gid 1001 with fsGroup: 1001 so the data volume is writable. Read-only root filesystem, all capabilities dropped, runAsNonRoot. Signing key mounted from an optional secret (laas-signing-key) at /var/run/secrets/laas/signing-key/cosign.key — absent by default, required when LAAS_SIGNING_REQUIRED=true. Optional laas-secrets for DB URL and OIDC. |
configmap.yaml |
Default env: file backend, LAAS_IDENTITY_PROVIDER=oidc, signing off. Supply LAAS_OIDC_JWKS_URL via laas-secrets or the overlays/oidc/ overlay. Sensitive values go in laas-secrets (see secret.example.yaml). |
secret.example.yaml |
Template for laas-secrets (DB URL, OIDC) and laas-signing-key (cosign key). Copy and customize — never commit real values. |
kustomization.yaml |
kubectl apply -k deploy/ bundles the base manifests. |
serviceaccount.yaml / service.yaml |
Dedicated laas service account; ClusterIP service on port 8000. |
Forbidden service accounts (sci-api, default) and other deploy-time invariants are
enforced by tests/test_deploy_invariants.py, not by convention — a manifest that
regresses one of them fails CI.
deploy/cronjob.yaml runs python -m traust_ledger.cli verify --all every 6 hours. The CLI
sweeps all layers, runs verify_merkle_integrity (+ optional signature check), and exits
non-zero if any layer fails or zero layers are found. Standard Kubernetes monitoring
(kube_job_status_failed) catches failures.
On-demand verification is also available via the REST endpoint:
GET /v1/ledger/layers/{layer_id}/verify
Returns {passed, findings, checked_at}. Pass ?check_signatures=true to also verify
merkle signatures (defaults to the value of LAAS_SIGNING_REQUIRED).
CronJob volume: The base manifest uses emptyDir as a placeholder. Production
overlays must bind the real data volume (PVC/NFS). The CLI exits non-zero when zero
layers are found, so a misconfigured mount fails loudly rather than passing vacuously.
GET /v1/ledger/layers/{layer_id}/findings
Returns resolved current disposition per finding with precedence applied (validation >
human > triage, two-person satisfied, assurance, conflict, severity override). The
response includes ledger_only: true to signal that base_validity defaults to
not_verified (no audit report available). Consumers with the audit report can enrich
further via build_cumulative.
The bulk endpoint resolves all layers with cursor-based pagination:
GET /v1/ledger/findings?limit=100&cursor=<layer_id>&since_epoch=<n>
cursor— resume after this layer_id (stable across rebuilds; no skip/duplicate).limit— max layers per page (1–1000, default 100).since_epoch— only include layers withmerkle_epoch >= n(incremental delta).- Response includes
next_cursorandhas_morefor iteration, plusmerkle_rootandmerkle_epochper layer for staleness detection.
For dashboard-backing tables, the materialize CLI writes resolved findings into any SQLAlchemy-compatible database:
ledger materialize --to sqlite:///findings.db
LAAS_MATERIALIZE_URL=postgresql://user:pass@host/db ledger materialize
ledger materialize --ddl # print schema DDL and exitRows are keyed on (layer_id, finding_ref) — re-running is idempotent (upsert).
Each layer commits independently, so partial failures don't corrupt prior work.
The table includes merkle_root and merkle_epoch so consumers can answer
"is my view stale?" without re-resolving.
Pass --json for per-layer JSON-line progress. Pass --ddl to emit the
CREATE TABLE DDL without connecting to a database — this is the portable
artifact downstream dashboards bind to.
The flow:
ledger (any backend)
└─ materialize CLI resolves precedence (derive_disposition)
└─ upserts rows into target store (SQLite / Postgres / …)
└─ SQL views + indexes over THAT → dashboards
The precedence engine lives in traust_ledger.api.disposition — shared by the service,
the harness engine, and SCI.
Not yet built: A push/streaming materializer (change-feed driven). Pull-on-schedule covers every current consumer. Push requires change notification the service doesn't have yet — deferred until demand materializes.
| Setting | Default | Production recommendation |
|---|---|---|
LAAS_SIGNING_REQUIRED |
false |
true — enables tamper-evident merkle signatures. Set it together with LAAS_SIGNING_KEY_PATH: the key path alone lets a broken mount degrade silently to unsigned, while REQUIRED alone fails loudly. Full variable list, and the HARNESS_SIGNING_* fallback, in auth.md. |
LAAS_BACKEND_TYPE |
file |
db for multi-node access (Postgres recommended) |
replicas |
1 |
Keep at 1 — single-writer design. The file backend uses flock(2) which is advisory and may not work on NFS. The DB backend uses SELECT ... FOR UPDATE which provides row-level locking on Postgres but is a silent no-op on SQLite. |
SQLite is a valid backend for small/single-writer deployments. Be aware:
FOR UPDATEis a no-op — SQLite has no row-level locking. Concurrency correctness relies on database-level write serialization (WAL mode +busy_timeout, both configured automatically bycreate_backend).- SQLite serializes database writes; the backend still enforces suffix-only event appends and installs update/delete/sequence-gap triggers.
- Concurrency tests must run against PostgreSQL. Set
LEDGER_TEST_DATABASE_URL;tests/test_storage_e2e.pycovers the file, SQLite, and PostgreSQL lifecycle.
When using the file backend on NFS, flock(2) is advisory and may not provide
mutual exclusion across hosts. Stick to replicas: 1 or use the DB backend for
multi-node deployments.