Security
tresor's security model covers the protocol and the extension: tokens, the permission model (administrators manage, roles use), servers acting for users. This page covers what the service adds.
What it guarantees
- Fail closed. A KEK that does not unwrap, a sealed value that does not open, a reference that does not resolve, a store that does not answer: an error for that request, never an empty or a stale value.
- Nothing sensitive leaves the service but material, to a caller allowed to
useit. No material, token, data key, grant id or configuration value in a log, an error or the readiness answer. - Material is sealed at rest: AES-256-GCM under data keys the KEK wraps, bound to its secret and version. A delegation grant's tokens are sealed too, and its id is stored only as a hash.
- References are an administrator's, within the allowlist, checked at the write and again at each resolution.
- Every write is compare-and-set, so several replicas never lose or cross a write.
- No password of the service's own on Azure: a managed identity (Container Apps) or workload identity (AKS) for Key Vault and the database.
- Transport: HTTPS (or TLS at the ingress,
tls.offload); to PostgreSQLsslmode=verify-full, to SQL Serverencrypt=true, with the certificate checked, off the local machine.
What it trusts
- The identity providers it is configured with, and their signing keys.
- Whoever can write its database. The sealed values are bound to their rows, but a secret's grants, a
delegation grant's user and expiries are not sealed: write access to the database can change who may use
what. Protect it as the service itself. These rows carry a MAC, checked by default on the Kubernetes store and
(since 0.4,
state.mac) on the SQL stores: a change by hand is refused - a secret's grants are inside its MAC - but a whole secret, delegation or token deleted, or an older one put back (a rollback), is not. Only the service's account should write them. - Whoever holds the KEK, with the database, holds the material.
- Administrators: they write secrets and references. The allowlist bounds what a reference can read.
Verifying the artifacts
From 0.4 the image and the chart are signed keylessly (cosign, spec 022): by the workflow that built them, with its GitHub OIDC identity - no key exists to steal. Check the exact identity - the workflow and the tag - with cosign v3 or later:
cosign verify ghcr.io/hugr-lab/tresor-server:v0.8.0 \
--certificate-identity https://github.com/hugr-lab/tresor-server/.github/workflows/image.yml@refs/tags/v0.8.0 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
cosign verify ghcr.io/hugr-lab/charts/tresor-server:0.8.0 \
--certificate-identity https://github.com/hugr-lab/tresor-server/.github/workflows/chart.yml@refs/tags/v0.8.0 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
:edge is signed from main: image.yml@refs/heads/main. A looser check (any workflow of the repository) would
also accept a signature made from a branch.
-
Verify a digest, deploy that digest: a tag can move;
image.tag: v0.8.0@sha256:…on the chart (the same reference in a recipe's image) pins what was verified. A policy controller (Kyverno, Sigstore's policy-controller) can require the signature at admission. -
The SBOM and the provenance (BuildKit's attestations, beside the image in the registry):
docker buildx imagetools inspect ghcr.io/hugr-lab/tresor-server:v0.8.0 --format '{{ json .SBOM }}'docker buildx imagetools inspect ghcr.io/hugr-lab/tresor-server:v0.8.0 --format '{{ json .Provenance }}' -
The scan: the linux/amd64 image is scanned (Trivy) before anything is pushed; a HIGH or CRITICAL finding with a fix stops the image's push (the arm64 image is the same Go binary on the same base, not scanned apart). The chart is published by its own workflow.
Report a vulnerability as SECURITY.md says.
A sandboxed DuckDB
A DuckDB with enable_external_access = false reaches only what allowed_directories names - tresor's requests
included (tresor specs/036). For a client in a sandbox, name the service's URL (public_url), every endpoint
its issuers' discovery names (token, device, revocation), and for a managed identity's login the platform's
endpoint (http://169.254.169.254/), each ending in / and spelled as the URL spells it. A secret already
fetched is served from memory until it expires; nothing new is fetched. See tresor's DuckDB's
sandbox.
Static secrets that remain
- A local KEK and a database password: for development and small installs.
- The client secret of the service's own client at the IdP, when it mints tokens for callers
(
token_exchange).