Skip to Content
App AttestationDeploying the verifier

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.

  1. Open the project’s Attestation page, Deployment tab.
  2. 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.
  3. 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.0

At startup the verifier downloads your signed policy from the TrustPin CDN, validates its signature with TRUSTPIN_PROJECT_PUBLIC_KEY, and starts serving.

Exit codeMeaningWhat to do
0Clean shutdownNothing
1Runtime or startup failure: no policy source reachable and no cache, an invalid policy, or a store that is unreachable or behind its schemaRead the log, fix the cause and restart
2Configuration errorEvery problem is listed on stderr; fix them all, then restart
3Attestation is not set up for the project: no published policySet 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 hash

Add --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

VariableDescription
TRUSTPIN_ORG_IDYour organization ID
TRUSTPIN_PROJECT_IDYour project ID
TRUSTPIN_PROJECT_PUBLIC_KEYThe project public key (base64 DER), the same value your apps are configured with. The project key never rotates

Secrets you own

VariableDescription
TRUSTPIN_TOKEN_SIGNING_KEYSComma-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_SECRETSComma-separated base64 secrets of at least 32 bytes. The first signs challenges; all verify
TRUSTPIN_PLAY_INTEGRITY_CREDENTIALSBase64 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

VariableDefaultDescription
TRUSTPIN_TOKEN_ISSUERtprn::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_AUDIENCEtprn::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:

ValueUse
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-fullSeveral instances. Apply the schema from attest-verifier migrations print first; the verifier never runs DDL
nonenone 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

VariableDefaultDescription
TRUSTPIN_ATTEST_CONFIGURATION_URLnoneA 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_INTERVAL5mHow often the policy is fetched again (each wait ±10%). A newly published version is live within one interval
TRUSTPIN_POLICY_CACHE_DIRnoneOn 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_TIMEOUT60sHow long startup retries the CDN

The verifier never accepts a policy version lower than the last one it accepted.

Ports, health and hardening

PortReachEndpoints
8080Public, behind your gateway/v1/challenge, /v1/attest, /v1/token, /v1/assertion, /.well-known/jwks.json, /.well-known/openid-configuration, /healthz
9090Monitor, privateRead-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.

VariableDefaultDescription
TRUSTPIN_LISTEN_ADDR:8080Public listener
TRUSTPIN_MONITOR_LISTEN_ADDR127.0.0.1:9090Monitor 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_KEYnoneProtects 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_FILEnoneServe HTTPS, optionally with mutual TLS. Off by default because TLS normally ends at your gateway
TRUSTPIN_RATE_LIMITnone<n>/s, <n>/m or <n>/h per client IP on /v1/*; defence in depth behind your gateway’s own limiting
TRUSTPIN_KEY_RATE_LIMIT60/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_PROXIESnoneCIDRs whose X-Forwarded-For names the client
TRUSTPIN_LOG_LEVEL, TRUSTPIN_LOG_FORMATinfo, jsonLogging
OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_*noneOpenTelemetry 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

SDKVerifier
7.1.0Contract 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.com

The 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

SymptomLikely causeFix
Exit code 3No published policy for the projectSign and publish on the project’s Attestation page, then restart
The monitor port is unreachableThe monitor listener binds 127.0.0.1 by defaultSet TRUSTPIN_MONITOR_LISTEN_ADDR=:9090 with TRUSTPIN_MONITOR_API_KEY, and publish the port on loopback
The app reports verifierUnavailableThe verifier’s domain isn’t registered, and the app pins strictlyRegister the domain

Next steps