Trusty answers authorization questions, so its own security posture has to be legible. This document states what the project guarantees, what it deliberately does not, and how to report a problem.
Use GitHub's private vulnerability reporting on this repository (Security → Report a vulnerability). It creates a private advisory visible only to maintainers. Please do not open a public issue for a suspected vulnerability.
Include what you need to reproduce it: the affected crate, the request or input, and the observed versus expected verdict. If you have a failing test, that is the fastest path — findings become regression tests here.
Trusty is pre-1.0 (v0.1.x). Fixes land on main; there is no maintained LTS branch.
Verification is deterministic code. No verification or authorization decision is ever produced by a language model. Checks V1–V7 are ordinary Rust with unit tests; the same inputs always yield the same verdict. This is the product's core guarantee, not an implementation detail.
Delegation credentials are signed, not asserted. Documents are canonicalized with
JCS (RFC 8785) and signed with Ed25519 (W3C Data Integrity eddsa-jcs-2022), so the
signed bytes are reproducible by anyone.
Verify requires proof of possession. Since schema 1.1.0, check V6 is a
subject-bound DPoP proof (RFC 9449): the caller must demonstrate control of the key the
credential names. A dc_ref alone is not a bearer credential.
Key rotation retires the original. Activating a rotated key retires every previously
active key, including the original did:key — a usable kill switch for a compromised key.
The registry does not persist raw identity-proofing identifiers. The raw CI is never
written to storage and anchor_id never appears in a verify response. Both are enforced
by tests (crates/trusty-registry/tests/pii_scan.rs), not by convention.
Vault storage is keyed per person, and the key is not the identifier. Each value is stored under the data subject it belongs to, so two people's data never share a slot and erasure can target one person. The subject is a pseudonym derived with HKDF from the tenant data key, so the index holds an opaque handle rather than the username or email it stands for — and, being keyed, it cannot be reversed by dictionary attack the way a bare digest of a low-entropy identifier could.
These look like gaps and are not. They are load-bearing choices.
The public verify route is unauthenticated and rate-limited. That is the single-player surface — verification has to work without an account for the registry to be useful. It returns a verdict and never returns personal data.
The verify JSON response is a versioned (semver) public contract. Changing its shape is a breaking change. Consumers may depend on it.
Both TRUSTY_ENV and the master-key environment classification fail safe. Anything
other than the exact strings dev/development is treated as production.
Read these before deploying the sidecar gateway. They are limits of scope, not defects, but deploying as if they were absent would be unsafe.
PII detection is a safety net, not a compliance boundary. The gateway's interceptor matches shapes (email, phone, national identifier) in text bodies on write methods. It is deliberately shape-strict to avoid false positives, so it will not recognize every category of personal data, nor data carried in forms it does not scan. Do not treat a clean pass as proof that a request carries no PII, and do not make it your only control.
Tokenization is scoped to configured path prefixes. The encrypt stage rewrites bodies
only on the prefixes in TRUSTY_PII_TOKENIZE_PATHS (default /admin/, the Keycloak
surface that stores profile data). Traffic on any other proxied path is forwarded
unchanged. If your deployment accepts personal data elsewhere, you must extend this list.
Consent evaluates the caller, not the data subject. Vault storage is keyed to the person the data describes, but the consent check asks a different question: may this caller submit this category of data, for this purpose? A caller holding a grant can therefore submit a third party's personal data without that person having consented. Do not treat the gateway as the consent boundary for third-party data; establish your lawful basis for processing it elsewhere.
Requiring the data subject's own consent is not achievable on the route that matters and is a rejected design rather than a pending one: a create request is what brings the person into existence, so they have no account to authenticate to a consent UI with and could never grant.
Grants are scoped by purpose, and the purpose follows the route: user-administration on
the Keycloak Admin API user endpoints, self-service on the account console,
authentication everywhere else. A grant issued for one does not authorize another, so you
can permit a service account to administer users without also permitting it anywhere else.
Tokenization refuses routes where it cannot tell whose data it is. Values are stored
per data subject, derived from the request — the Keycloak user id in the path, or the
username/email being written on a create. On a tokenized route that names no data
subject, the request is rejected (422) rather than attributed to the caller. If you widen
TRUSTY_PII_TOKENIZE_PATHS beyond the Keycloak Admin API user endpoints, expect refusals
until the derivation covers the routes you added.
Authentication bootstrap is exempt from PII scanning. /realms/{realm}/protocol/ openid-connect/* and /realms/{realm}/login-actions/* are not scanned: there the caller is
presenting their own credentials, a login identifier is often an email address, and Keycloak
must read the value verbatim to authenticate at all. The gateway stores nothing on those
paths, so consent was never the control for that data. The exemption is scoped to those two
prefixes — /realms/{realm}/account still scans, since that surface stores profile data —
and a path containing a .. segment never qualifies.
Keycloak retains what it already had. Tokenization applies to new traffic. Profile data written before the gateway was introduced, and login identifiers themselves, remain in Keycloak's database and its backups.
Use the Vault master-key provider; the environment provider exposes the key.
TRUSTY_CRYPTO_MK_PROVIDER accepts vault, env, and file. Prefer vault
(Vault/OpenBao KV v2, via TRUSTY_CRYPTO_MASTER_KEY_REF=kms:vault:<mount>/<path> plus
VAULT_ADDR and VAULT_TOKEN): it keeps the master key out of the process environment,
where /proc/<pid>/environ, container inspection, orchestrator APIs, and crash reporters
would otherwise reach it. file refuses to start in production by design — it is a
development affordance, not a key management system.
The Vault provider is a partial mitigation, not a complete one: the key is fetched once at boot and the derived wrap key lives in process memory for the process's lifetime, so a core dump still exposes it. Keeping the key inside the KMS end-to-end means remote wrap/unwrap, which would put a network round trip on every field encryption — a separate decision that has not been taken.
Providers are not interchangeable for existing data. A wrapped data key records which
master key wrapped it, so switching a live deployment from env to vault fails loudly on
the first unwrap even when the key bytes are identical; re-wrap before switching. Compromise
of the master key unwraps every data key regardless of provider — rotate on any suspicion.
Audit records go to stderr by default. Treat your log pipeline as in scope for data handling, with access control and retention to match — log infrastructure is usually readable by far more people than the vault is.
The gateway refuses to start rather than run in a weaker posture than intended. In
production (TRUSTY_ENV anything but dev/development) it requires either:
TRUSTY_CRYPTO_MK_PROVIDERandTRUSTY_VAULT_DATABASE_URL— tokenization on, with durable stores. The in-memory stores are rejected because their tokens do not survive a restart, which would strand thetvt_…references already written to Keycloak; orTRUSTY_PII_TOKENIZATION=disabled— an explicit statement that this deployment handles no personal data. Detection and consent still run, and consent remains deny-by-default, so detected PII is still blocked; what is absent is encryption at rest.
With tokenization enabled the gateway also requires a person-key salt
(TRUSTY_SUBJECT_SALT_V1, and TRUSTY_SUBJECT_SALT_ACTIVE once you rotate). It is neither
defaulted nor generated on the fly: a salt that changed at each boot would give the same
person a new vault subject on every restart, so the vault would accumulate one entry per
restart and an erasure request would miss all but the last. Keep older versions configured
after a rotation — the version travels in the subject, and the older salt is what keeps
subjects minted before the rotation resolvable.
A configured master key always outranks the disable acknowledgement. The registry applies the same principle: a master-key or salt reference it cannot resolve at boot is a startup failure, never a degraded runtime mode.
Durable vault stores are behind the pg cargo feature. docker/gateway.Dockerfile builds
with it; a binary built without it cannot serve a production deployment with tokenization
enabled, and says so at startup.
This policy covers the crates in this repository. Keycloak, PostgreSQL, and any KMS or secret store you deploy alongside Trusty have their own security models and disclosure processes; issues in them should go to those projects.