Deploying the verifier
For platform and ops engineers. At the end of this page, a verifier runs in your infrastructure, serves your published policy and passes attest-verifier check.
The verifier is a self-hosted service you run next to your API. It verifies platform evidence against your published policy and issues the tokens your API checks. It is distributed as a container image, trustpincloud/attest-verifier, for linux/amd64 and linux/arm64, signed with cosign. It is configured entirely through environment variables; there are no configuration files.
Before you start
- Attestation set up and a policy published on the project’s Attestation page. See Setting up apps and the policy.
- A public
https://URL for the verifier, behind your API gateway or load balancer. The gateway provides TLS termination, rate limiting and IP filtering. - Clocks synchronised with NTP on every host, because challenge and token lifetimes depend on them.
The deployment bundle
The recommended start is the bundle the console generates. It fills in every identifier and leaves only the secrets as placeholders.
- Open the project’s Attestation page, Deployment tab.
- Add an environment: a name and its verifier URL, which must be the verifier’s final public URL, including any path prefix. It becomes the token issuer.
- Choose Docker Compose, Kubernetes or .env file only, and download.
The CLI makes the same bundle:
trustpin-cli projects attestation bundle <organization id> <project id> \
--format compose|kubernetes|env [--environment <name>]Quick start
docker run --rm -p 8080:8080 -p 127.0.0.1:9090:9090 \
-e TRUSTPIN_ORG_ID=<organization id> \
-e TRUSTPIN_PROJECT_ID=<project id> \
-e TRUSTPIN_PROJECT_PUBLIC_KEY=<project public key, from the console> \
-e TRUSTPIN_TOKEN_SIGNING_KEYS=<from attest-verifier keygen> \
-e TRUSTPIN_CHALLENGE_SECRETS=<from attest-verifier keygen --secret> \
-e TRUSTPIN_MONITOR_LISTEN_ADDR=:9090 \
-e TRUSTPIN_MONITOR_API_KEY=<from attest-verifier keygen --secret> \
trustpincloud/attest-verifier:1.0.0At startup the verifier downloads your signed policy from the TrustPin CDN, validates its signature with TRUSTPIN_PROJECT_PUBLIC_KEY, and starts serving.
| Exit code | Meaning | What to do |
|---|---|---|
0 | Clean shutdown | Nothing |
1 | Runtime or startup failure: no policy source reachable and no cache, an invalid policy, or a store that is unreachable or behind its schema | Read the log, fix the cause and restart |
2 | Configuration error | Every problem is listed on stderr; fix them all, then restart |
3 | Attestation is not set up for the project: no published policy | Set up attestation in the console, then restart |
Generating secrets
docker run --rm trustpincloud/attest-verifier:1.0.0 keygen # a signing key
docker run --rm trustpincloud/attest-verifier:1.0.0 keygen --secret # a challenge secret or monitor API key
docker run --rm trustpincloud/attest-verifier:1.0.0 keygen --debug-token # a debug token and its hashAdd --json for machine output. attest-verifier check validates a configuration without serving; run it in CI. migrations print prints the Postgres schema, and version prints the version.
Required variables
| Variable | Description |
|---|---|
TRUSTPIN_ORG_ID | Your organization ID |
TRUSTPIN_PROJECT_ID | Your project ID |
TRUSTPIN_PROJECT_PUBLIC_KEY | The project public key (base64 DER), the same value your apps are configured with. The project key never rotates |
Secrets you own
| Variable | Description |
|---|---|
TRUSTPIN_TOKEN_SIGNING_KEYS | Comma-separated base64 PKCS#8 EC P-256 private keys. The first signs; all are published in the JWKS and accepted for verification. These sign the tokens and key certificates the verifier issues; they are unrelated to the TrustPin project key |
TRUSTPIN_CHALLENGE_SECRETS | Comma-separated base64 secrets of at least 32 bytes. The first signs challenges; all verify |
TRUSTPIN_PLAY_INTEGRITY_CREDENTIALS | Base64 Google service-account JSON, only when the policy accepts Play Integrity evidence. GOOGLE_APPLICATION_CREDENTIALS is also honoured. Apple and Key Attestation need no credential |
If the signing keys or challenge secrets are unset, the verifier generates them in memory and logs a warning. That is fine for development only: every restart forces every device to attest again.
Rotating signing keys: add the new key first in the list so it starts signing, keep the old key listed for at least the Key certificate lifetime of your policy (30 days by default) because key certificates signed with it are still in the field, then remove it. Each key’s kid is its RFC 7638 JWK thumbprint, so every instance derives the same ID.
All instances of one deployment must share the same signing keys, challenge secrets and store. Each deployment (staging, production, each project) has its own keys and secrets. Keep them in a vault or KMS and inject them at start.
Tokens and discovery
| Variable | Default | Description |
|---|---|---|
TRUSTPIN_TOKEN_ISSUER | tprn::attest::<organization id>::<project id> | iss of issued tokens. Set it to the verifier’s public https:// base URL to enable OIDC discovery for gateways that locate keys through the issuer |
TRUSTPIN_TOKEN_AUDIENCE | tprn::project::<organization id>::<project id> | aud of issued tokens. Your API must expect exactly this value |
If a gateway mounts the verifier under a path prefix, the gateway strips the prefix and TRUSTPIN_TOKEN_ISSUER is the full external URL including it, so that <issuer>/.well-known/openid-configuration resolves from the outside. The SDKs are configured with the same base URL.
Replay protection
TRUSTPIN_STORE_URL selects where one-time challenges and App Attest counters are kept:
| Value | Use |
|---|---|
memory:// (default) | A single instance: one-time challenges and increasing App Attest counters within that instance. A restart forgets them. Several instances need a shared store; startup warns otherwise |
rediss://…, rediss-sentinel://…, rediss-cluster://… | Several instances sharing one store. Plaintext forms work but are warned about when the store is off-host |
postgres://…?sslmode=verify-full | Several instances. Apply the schema from attest-verifier migrations print first; the verifier never runs DDL |
none | none turns replay protection off: challenges aren’t one-time and App Attest counters aren’t checked |
If the store fails, /v1/attest and /v1/token answer 503 not_ready (TRUSTPIN_STORE_FAILURE=closed, the default; TRUSTPIN_STORE_TIMEOUT 2 s). open serves without replay protection.
Policy loading and refresh
| Variable | Default | Description |
|---|---|---|
TRUSTPIN_ATTEST_CONFIGURATION_URL | none | A self-hosted copy of the policy file, tried first with the TrustPin CDNs as fallback. Air-gapped deployments set it and block the CDNs |
TRUSTPIN_POLICY_REFRESH_INTERVAL | 5m | How often the policy is fetched again (each wait ±10%). A newly published version is live within one interval |
TRUSTPIN_POLICY_CACHE_DIR | none | On a persistent volume, persists the last good policy so an instance can start while the CDN is unreachable. It is also the rollback floor across restarts |
TRUSTPIN_STARTUP_TIMEOUT | 60s | How long startup retries the CDN |
The verifier never accepts a policy version lower than the last one it accepted.
Ports, health and hardening
| Port | Reach | Endpoints |
|---|---|---|
| 8080 | Public, behind your gateway | /v1/challenge, /v1/attest, /v1/token, /v1/assertion, /.well-known/jwks.json, /.well-known/openid-configuration, /healthz |
| 9090 | Monitor, private | Read-only status: /healthz, /readyz, /metrics (Prometheus), /openapi.yaml. Every endpoint is GET; nothing on this port changes the verifier |
Every endpoint of both ports is described in the Verifier API reference. Operating the verifier shows how to call the monitor port with its key and scrape it with Prometheus.
| Variable | Default | Description |
|---|---|---|
TRUSTPIN_LISTEN_ADDR | :8080 | Public listener |
TRUSTPIN_MONITOR_LISTEN_ADDR | 127.0.0.1:9090 | Monitor listener. Set :9090 only together with TRUSTPIN_MONITOR_API_KEY when probes or Prometheus connect from outside the container. Never expose it publicly |
TRUSTPIN_MONITOR_API_KEY | none | Protects the monitor listener (Basic password or Bearer token), at least 16 characters. /healthz stays open, and /readyz without the key returns only the status |
TRUSTPIN_TLS_CERT_FILE, TRUSTPIN_TLS_KEY_FILE, TRUSTPIN_TLS_CLIENT_CA_FILE | none | Serve HTTPS, optionally with mutual TLS. Off by default because TLS normally ends at your gateway |
TRUSTPIN_RATE_LIMIT | none | <n>/s, <n>/m or <n>/h per client IP on /v1/*; defence in depth behind your gateway’s own limiting |
TRUSTPIN_KEY_RATE_LIMIT | 60/h | <n>/s, <n>/m, <n>/h or <n>/d per device key. Excess requests get 429 too_many_attestations with the time to wait, and the SDKs follow it. none turns it off |
TRUSTPIN_TRUSTED_PROXIES | none | CIDRs whose X-Forwarded-For names the client |
TRUSTPIN_LOG_LEVEL, TRUSTPIN_LOG_FORMAT | info, json | Logging |
OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_* | none | OpenTelemetry tracing |
GET /healthz is liveness. GET /readyz (monitor port) returns 200 with ok or degraded, and 503 with unavailable when no policy is loaded or the store is unreachable. degraded is worth alerting on. It covers:
- ephemeral signing keys or challenge secrets;
- debug tokens configured;
- a policy missing or incompatible at refresh;
- three consecutive refresh failures;
- a revocation list older than 7 days.
See Operating the verifier for the fix for each.
Registering the verifier’s domain
Register the verifier’s host as a domain of your TrustPin project. The SDKs pin their calls to the verifier, and in strict mode they refuse an unregistered host. See Setting up apps and the policy.
Development: debug tokens
Simulators and emulators cannot produce platform evidence. For development, set TRUSTPIN_DEBUG_TOKEN_HASHES to comma-separated base64url(SHA256(debugToken)) values (attest-verifier keygen --debug-token prints a token and its hash) and turn on Allow debug tokens in the policy. Developers put the token in the app’s run environment, never in the app bundle. Tokens obtained this way carry tp.env debug and tp.plat debug, and your API must reject them outside development. Set the hashes on staging only: /readyz reports degraded while they are set.
SDK and verifier compatibility
| SDK | Verifier |
|---|---|
| 7.1.0 | Contract version 2 or newer: verifier 0.2.2 or later |
The image and its licence
Pin an exact version or digest in anything long-lived, never beta or latest. To verify the signature:
cosign verify trustpincloud/attest-verifier:<version> \
--certificate-identity-regexp '^https://github\.com/trustpin-cloud/cloud-console-attest-verifier/\.github/workflows/release\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+.*$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comThe verifier is under the TrustPin Binary License Agreement: Attestation Verifier, which ships inside the image and the binary archives. You may run it in your own infrastructure, in any number of instances, while your TrustPin plan is active. Service providers may run it for a client, for that client’s TrustPin projects.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Exit code 3 | No published policy for the project | Sign and publish on the project’s Attestation page, then restart |
| The monitor port is unreachable | The monitor listener binds 127.0.0.1 by default | Set TRUSTPIN_MONITOR_LISTEN_ADDR=:9090 with TRUSTPIN_MONITOR_API_KEY, and publish the port on loopback |
The app reports verifierUnavailable | The verifier’s domain isn’t registered, and the app pins strictly | Register the domain |