Trust & honesty

Production hardening

Signata validates its configuration at startup and refuses to boot in an unsafe production state. This page lists what production requires and why. Environment variables are referenced by their exact names.

Fail-fast configuration

Configuration is parsed and validated once at boot. In production the validator hard-fails rather than starting with insecure defaults. A missing secret or a SQLite DATABASE_URL aborts startup with a precise error instead of silently running in a demo posture.

Demo defaults are not for production

Development falls back to a SQLite file and a placeholder secret so you can run instantly. Those fallbacks are rejected in production: a file: database URL and the dev placeholder secret both cause a boot-time failure.

Required secrets

In production the following must be set to strong values, or the process refuses to start:

  • SIGNATA_SECRET: the root secret. SIGNATA_COOKIE_SECRET, SIGNATA_API_KEY_PEPPER, and SIGNATA_WEBHOOK_SECRET_PEPPER derive from it when not set independently; set them explicitly for separation.
  • SIGNATA_ISSUER_PRIVATE_KEY_JWK and SIGNATA_ISSUER_PUBLIC_KEY_JWK: both required in production. The boot check enforces this so issuer keys never live in the database.
.env (production)
# Core: required and validated at boot in production.
SIGNATA_SECRET=                 # 32+ byte random; base for derived secrets
SIGNATA_APP_URL=https://app.signata.dev
DATABASE_URL=postgres://…         # MUST be PostgreSQL in prod (no file: URLs)
REDIS_URL=redis://…               # required for multi-instance correctness

# Issuer signing keys: loaded from env/KMS, never stored in the database.
SIGNATA_ISSUER_PRIVATE_KEY_JWK={"kty":"OKP",…}
SIGNATA_ISSUER_PUBLIC_KEY_JWK={"kty":"OKP",…}
SIGNATA_ISSUER_KID=key_2026_01

# Optional, but recommended to set explicitly in prod.
SIGNATA_COOKIE_SECRET=          # defaults to SIGNATA_SECRET if unset
SIGNATA_API_KEY_PEPPER=         # peppers stored API-key hashes
SIGNATA_WEBHOOK_SECRET_PEPPER=  # peppers stored webhook secrets
SIGNATA_ALLOWED_ORIGINS=https://app.example.com,https://www.example.com
SIGNATA_BASE_URL=               # public base URL if it differs from APP_URL

Issuer keys from KMS, never the database

The keys Signata signs credentials with are loaded from the environment (sourced from a KMS or secret manager), not persisted in application storage. Keeping private key material out of the database means a database compromise cannot forge credentials. The matching public keys are published at the JWKS endpoint so anyone can verify Signata-issued signatures independently; rotate by issuing a new SIGNATA_ISSUER_KID and keeping the prior public key in the set until old credentials age out.

Postgres & Redis

Production uses PostgreSQL. The validator rejects file: URLs. Set REDIS_URL as well: rate-limit counters and idempotency records belong in a shared store so they are correct across instances. Without Redis the app falls back to the database and logs a warning, which is acceptable for a single instance but not for a horizontally scaled deployment.

Security headers, CORS & CSRF

Security headers are applied to responses, and browser-origin requests are checked against SIGNATA_ALLOWED_ORIGINS (a comma-separated allowlist) for CORS and CSRF protection. Set it to the exact origins that embed the badge or call the API from a browser; cookie-authenticated state-changing requests from other origins are rejected. API-key requests are bearer-style and not subject to the browser-origin check.

Rate limiting & idempotency

Endpoints are rate-limited per key, and POSTs honor an idempotency-key header so retries are safe and do not double-process. Both are backed by Redis in production for correctness across instances; a processed idempotency key is remembered for 24 hours.

Audit log & signed webhooks

Administrative actions (key issuance, trust changes, policy edits) are written to an append-only audit log. Webhook deliveries are signed with an HMAC over the payload in the x-signata-webhook-signature header; verify it on receipt (the signing secret is peppered with SIGNATA_WEBHOOK_SECRET_PEPPER) to reject forged or replayed deliveries.

Health, readiness & JWKS

Wire GET /api/health to your liveness probe and GET /api/ready (which checks the database, and Redis when configured) to your readiness gate, so traffic is only routed to instances whose dependencies are reachable. The JWKS endpoint is public.

Docker & Compose

A minimal production stack (the web image plus Postgres and Redis):

docker-compose.yml
services:
  web:
    image: signata/web:latest
    environment:
      SIGNATA_SECRET: ${SIGNATA_SECRET}
      DATABASE_URL: postgres://signata:${PG_PASSWORD}@db:5432/signata
      REDIS_URL: redis://cache:6379
      SIGNATA_ISSUER_PRIVATE_KEY_JWK: ${SIGNATA_ISSUER_PRIVATE_KEY_JWK}
      SIGNATA_ISSUER_PUBLIC_KEY_JWK: ${SIGNATA_ISSUER_PUBLIC_KEY_JWK}
      SIGNATA_ISSUER_KID: ${SIGNATA_ISSUER_KID}
      SIGNATA_ALLOWED_ORIGINS: https://app.example.com
    depends_on: [db, cache]
  db:
    image: postgres:16
  cache:
    image: redis:7
Pass secrets via your orchestrator’s secret mechanism rather than committing them. The same boot-time validation runs inside the container, so a misconfigured deployment fails immediately and visibly instead of starting in an unsafe state.