Security & secrets
What a leaked config file would contain, who can do what, and why the credential bootstrap looks inconvenient on purpose.
On this page
A gateway sits between your applications and every provider you pay, which makes it the single most attractive file on your network. Two design choices follow from taking that seriously, and most of this page is their consequences.
The first: a deployment you configure yourself stores no secret value. Configuration carries names and references, so a leaked config file leaks the shape of your deployment and nothing you would have to rotate. A hosted gateway can hold a provider key on your behalf, which is a different mode with a different answer, and it gets one later on this page.
The second: nothing is open by default and nothing is pre-created. No credential exists until you make one, which means there is no default to forget to change and nothing in the artifact to leak.
This page is the reasoning. Production hardening is the checklist that acts on it.
What a config file contains
Every credential field in the configuration holds an environment variable name:
upstreams:
anthropic:
base_url: https://api.anthropic.com
dialect: anthropic
api_key_env: ANTHROPIC_API_KEY # a name, never a value
The loader refuses an inline secret where a reference belongs, so a config carrying a key does
not become the running config. The same rule reaches the deployment layers: the Helm chart renders
zero Secret objects and mounts yours by reference, the Terraform module takes ARNs resolved on
the instance, and the Compose overlay reads an env_file you fill and keep out of version
control.
The admin API holds the line too. GET /config/running returns the file’s bytes verbatim,
comments and key order intact, and there is no secret in that body to redact because none was
ever written.
Credentials the product mints for itself are hashed on the way to disk. Bearer tokens and share tokens are stored as SHA-256, local dashboard users as argon2id, and virtual keys as SHA-256 — each shown once at mint and unrecoverable afterwards. A key’s prefix is stripped before the request is forwarded upstream.
Saved provider connections, the mode that does store a secret
A hosted gateway can hold your provider key so that your applications do not, and a storage claim only means something once you say which mode it describes. A saved connection’s secret is written once, sealed under its own data key, wrapped by a customer-managed KMS key in TokenTriage’s cloud account, and read back by no API, export or log. Later reads show the connection’s label, its version and its status, and nothing derived from the secret, not even its length. Your applications then carry a TokenTriage runtime key and no provider secret at all.
Request-supplied mode is the other setting, and it keeps the guarantee this page opens with: the caller sends a provider key per request and no copy is kept. Which of the two applies is gateway configuration rather than a fallback a request can select. Saved provider connections is the procedure.
Who can do what
Roles are a static three-level matrix, ranked so each subsumes the one below it:
| Role | Grants |
|---|---|
viewer |
every read endpoint, exports included — an export is a read |
operator |
viewer, plus config-emitting writes, feedback, alert silences, cache purge, provider probes, replay, saved views and schedules, reconcile imports, governance node and revoke |
admin |
operator, plus user and token management, retention deletion, governance key mint and rotate, share tokens |
Denials are legible rather than evasive. A request with no credential where one is required gets
401; a principal below a route’s floor gets 403, not a 404 pretending the route does not
exist. Any unmatched path under the versioned API fails closed before reaching a handler.
Three paths are open, and the list is short enough to audit: /health, because the
kubelet needs no credential to ask whether a process is alive; /metrics, because a scrape
carries no secret; and the dashboard’s static assets, because every API call the interface makes
is gated anyway.
The bootstrap, and its honest limitation
The dashboard is secured before it serves anything. It renders, and no login succeeds, until you mint a credential out of band:
$ kubectl exec -n tokentriage tt-tokentriage-0 -- \
tokentriage token new --role admin --state-dir /data/.tokentriage/dash
token id: 40bd4df95ae0d61b role: admin
This token is shown ONCE and cannot be recovered:
ttk_...
The token is effective immediately. The stores are re-read on every verification, so there is
no restart and no propagation delay. The 401 → mint → 200 transition is what the bundled
helm test asserts, without a credential, on every install and upgrade.
Auth modes are chosen with dashboard.auth.mode:
token, bearer tokens. The chart’s default, and the better zero-state choice: no user database to seed and one header to present.users, named local users with argon2id password hashes, a session cookie, and a CSRF token bound to the session. For teams that want named principals.off, permitted only on a loopback admin bind, and a validation error otherwise. The Helm values vocabulary does not include it at all, so an open dashboard on a container deployment is not expressible.
Sessions live in memory, so any restart logs every browser out. Minted tokens and users live on the volume and keep working; you do not re-mint after an upgrade or a restore.
The air-gap consent gate
Every outbound tier is gated on one string. Shared state, an L3 cache, an external embeddings
API, OTLP export, and a guardrail judge or sidecar are each refused unless their own config
block carries external: "allow".
shared_state:
redis:
external: "allow" # you type this; no deployment layer types it for you
addr: redis.data.svc:6379
password_env: TT_SHAREDSTATE_REDIS_PASSWORD
The gate is a property of the binary rather than of a deployment layer, which is what makes it hard to un-gate by accident: the chart copies your consent string through verbatim and refuses to render an enabled backend without it, and CI asserts the set of hosts the product dials is a subset of the upstreams you configured. A default install dials nothing.
The guardrails tier goes further. Its core and every embedded detector are import-restricted — no networking, no process execution, no TLS, no system calls, no plugins. A test sweeps the whole subtree, so an embedded detector cannot reach the network even by mistake. Air-gap & known limits covers which detectors are embedded and which leave the process.
Guardrail consent is necessary but does not enable unsupported capabilities. Stock sidecar activation remains rejected pending runtime/model qualification; positive judge hard caps require a proven reservation bound. See the current activation table.
What is recorded
Every attempted mutation writes an append-only audit row carrying the actor, the action, a content hash of any diff, and the configuration generation it applied against. Denied requests are recorded too, and a credential that appeared in a request path is redacted before the row is written.
Retention deletions are audited the same way, one row per file with its reason:
{"v":1,"ts":"2026-08-10T12:55:42.0590646Z","actor":"system","role":"","origin":"cli","action":"retention.delete","resource":"decisions-20260701T031500.jsonl.gz","summary":"deleted rotated log decisions-20260701T031500.jsonl.gz (19647 bytes, max_age)","result":"ok"}
Verbatim prompt and response capture is a separate decision and is off by default. Turned on, it sits behind its own privileged scope and writes one audit row per read attempt, because the bytes of a prompt are a different class of data from a row about it.
Where each credential lives, and what to do when one leaks
| Credential | Store | At rest | Rotate with |
|---|---|---|---|
| dashboard bearer token | dash/tokens.json |
SHA-256 | tokentriage token revoke <id> then token new |
| local dashboard user | dash/users.yaml |
argon2id | tokentriage user passwd <name> |
| share link token | dash/share-tokens.json |
SHA-256 | revoke the share |
| virtual key | the governance store | SHA-256 | rotate with a grace window, or revoke |
| provider key | your secret manager | not ours to hold | rotate at the provider, update the Secret |
Revocation of a dashboard token is immediate, because the store is consulted on every verification. Virtual-key revocation across a fleet is not instantaneous and publishes its bound. Governance across replicas covers the number and what it means.
A leaked config file is the case that needs no action beyond review: it holds hostnames, environment-variable names and policy, and nothing that authenticates anything.
What this does not cover
The steps to apply all of this, covering TLS, the admin plane, the rendered security context, network policy and OpenShift, are production hardening. Verifying the artifact you downloaded is on install air-gapped. Limiting what a tenant may spend is a different subject from authenticating an operator, and lives under governance.