Documentation

Security

What a security review of a Guard Spec deployment needs in one place: the components and their trust boundaries, which data goes where, what is stored, for how long and how it is protected, and how to check that the release you run is the one this project built. Everything here is derived from the code and migrations of the release it ships with (0.1.x); where a statement depends on a setting, the setting is named.

Document Covers
This page Architecture, data flow, stored data and retention, secrets, encryption
Verifying a release cosign signatures of the images and archives, SBOMs, SLSA provenance, with the commands and their output for v0.1.1
Security risks and hardening The threat model: risks that remain in a deployment, what reduces each, and a hardening checklist
SECURITY.md Reporting a vulnerability, supported versions, response targets
Security model and limitations What detection does and does not protect against

Contents

Architecture

Two Go services and one PostgreSQL database. Nothing else is required.

flowchart LR
  app[Your applications] -->|"HTTPS (your proxy) · API key"| gw
  admin[Admins, browser] -->|"HTTPS (your proxy) · JWT session"| cp
  subgraph yours[Your network]
    gw[gateway :8080<br/>metrics :9090]
    cp[control plane :8081<br/>admin UI, API<br/>metrics :9091]
    db[(PostgreSQL 17)]
    models[[model files<br/>read-only]]
    gw -->|guardspec_app, RLS| db
    cp -->|guardspec_app, RLS| db
    models -.-> gw
  end
  gw -->|prompt, after the checks| llm[Upstream LLM API]
  cp -.->|alert metadata| chan[Slack, Teams, Telegram,<br/>e-mail, webhooks, A2A]
  cp -.->|OIDC| idp[SSO provider]
Component Trust boundary Listens on Talks to
Gateway (cmd/gateway) Facing your applications. Authenticates them with API keys (Authorization: Bearer gs_...) 8080 API, 9090 metrics PostgreSQL; the upstream LLM; an OTLP collector if configured
Control plane (cmd/control-plane) Facing admins only; keep it off the internet (P2). Authenticates users with passwords, TOTP and OIDC; issues HS256 JWTs bound to a session row 8081 API and admin UI (/ui/), 9091 metrics PostgreSQL; SSO providers; alert channels; the upstream only for an owner's "test upstream" call
PostgreSQL Holds all state. The services connect as guardspec_app (no superuser, no BYPASSRLS); the owner role runs the migrations only 5432 —
Model files L2 classifier and L3 embedder, ONNX, loaded in process. Pinned by SHA-256 in models/manifest.yaml, or inside the gateway-full image — —

Neither service terminates TLS: put a reverse proxy or load balancer in front (P1), and set TRUSTED_PROXIES on the control plane so that rate limits, sessions and the activity log see the client address (I5). The release images run as non-root with a read-only root filesystem.

Data flow of one request

  1. An application sends an OpenAI-compatible request to the gateway with its API key. The gateway hashes the key (SHA-256) and looks it up; the plaintext key is never stored.
  2. The sanitizer (L0) strips invisible and bidi characters. The detection layers score the parts listed in SCAN_ROLES (L1 rules, L2 classifier, L3 similarity, all in process). The org's policies (CEL) decide: allow, block, log or route.
  3. Blocked: the gateway answers 400 and the upstream is never called. Allowed: the request goes to the upstream LLM chosen by routing, with the upstream's key from an environment variable.
  4. The output guard checks the answer (links and images to unknown hosts, personal data, a leaked canary), also while streaming, and the answer goes back to the application.
  5. The gateway writes one audit row (requests) and, for blocks, an event (events) that the control plane's alert worker turns into notifications.

Prompt and answer text exist in the gateway's memory during the request. By default they are not written anywhere by Guard Spec: not to the audit row (only a SHA-256 and the length), not to logs (log lines carry scores, rule ids and decisions), not to traces (span attributes are the HTTP method, route, status and upstream path), not to events or notifications (metadata only: ids, categories, scores).

What is stored, where and for how long

Everything is in PostgreSQL. The services keep no state on disk; the gateway's rate-limit counters and caches are in memory.

Data Table(s) Contents Kept for
Request log (audit) requests Org, project, SHA-256 of the API key, model, decision, rule id, layer scores, PII entity types found, sanitizer flags, token counts, latency, route target, SHA-256 and length of the request body and of a buffered response. Prompt and response text only when content storage is on: AUDIT_STORE_CONTENT=true on the gateway (every org) or audit_store_content on one organization (admin role, recorded in the activity log) AUDIT_RETENTION_DAYS (default 90; 0 keeps them forever). The control plane deletes older rows in batches at start and every AUDIT_RETENTION_INTERVAL (default 1h); one replica at a time (advisory lock)
Activity log activity_log Who changed what: members, invites, API keys, policies, upstreams, security and config changes, 2FA resets, SSO providers, logins (success and failure). Actor, action, target, client IP, details without secrets; a failed login keeps the e-mail masked (a***@example.com) No automatic deletion. Delete or archive by hand if your policy requires
Users and memberships users, org_memberships, project_memberships, user_identities E-mail (lower-cased), display name, bcrypt password hash (none for SSO-only accounts), roles, linked SSO subject per provider Until the user is deleted
Sessions sessions One row per login: method, user agent, client IP, created, last seen, revoked. Tokens last 12 hours (or the org's session_lifetime_hours) Rows stay after expiry or revocation (no cleanup job yet)
Two-factor authentication user_mfa, mfa_recovery_codes, mfa_challenges TOTP secret encrypted (AES-256-GCM with MFA_ENC_KEY, bound to the user id); 10 recovery codes as bcrypt hashes; login challenges Secrets until 2FA is reset; challenges deleted an hour after they expire (5 minutes)
SSO flow state oidc_states, oidc_codes State, nonce and PKCE verifier (state stored as a hash, bound to the browser); one-time exchange codes as hashes States 10 minutes, deleted when expired; codes 60 seconds, deleted an hour later
Invites and link codes invites, email_link_codes, telegram_link_codes, channel_link_codes, channel_email_addresses Only the SHA-256 of each token or code, plus the e-mail or chat it is for Until used or expired, or the parent is deleted
API keys api_keys SHA-256 of the key, a short prefix for display (gs_ and 8 characters), alias, limits Until deleted
Configuration organizations, projects, policies, upstreams, org_security, identity_providers, notification_channels, alert_rules, user_groups Policies (CEL), upstream URLs and the name of their key variable, SSO client ids and the name of their secret variable, channel settings and the name of their secret variable Until changed or deleted
Org exemplars (L3) org_exemplars Example attack texts an admin adds for that org's similarity layer Until deleted
Alerts events, alert_rule_hits, alerts, notification_outbox, notifications Event type, severity, ids, categories, scores; delivery state Processed events 30 days, finished deliveries 30 days, read in-app notifications 90 days, rule hits 24 hours; alerts are kept

Outside the database:

Secrets

Secret Used by Stored Notes
GUARDSPEC_APP_PASSWORD (as PGPASSWORD) both services Your secret store; the database keeps its SCRAM hash Set with deploy/postgres/set-app-password.sh after each migration run
JWT_SECRET control plane Your secret store At least 32 bytes; signs session tokens (HS256). Rotating it logs everyone out
MFA_ENC_KEY control plane Your secret store 32 bytes, base64. Not recoverable: without it nobody with 2FA can log in (I2). Back it up with the database; there is no re-encryption, so do not rotate it automatically
ADMIN_PASSWORD control plane, first start only Your secret store; bcrypt hash in users Creates the first owner once
UPSTREAM_LLM_KEY, UPSTREAM_KEY_* gateway (and the control plane for upstream tests, if given) Environment only The database holds only the variable name; UPSTREAM_KEY_ORGS decides which org may use which key
OIDC_SECRET_* control plane Environment only Named by each SSO provider's client_secret_env
Alert channel secrets (SLACK_*, TEAMS_*, TELEGRAM_BOT_TOKEN_*, WEBHOOK_SECRET_*, A2A_*, GUARDSPEC_ALERT_*) control plane Environment only Named by each channel's secret_env

Outside GUARD_ENV=dev the services refuse to start with a missing, short or known default secret. API keys, invite tokens, link codes and recovery codes are shown once and stored only as hashes.

Encryption

Tenant isolation

Every org-scoped table has PostgreSQL row-level security on app.org_id, which the services set per transaction; the application role cannot bypass it. Tables keyed by user (sessions, identities, 2FA) are not org-scoped and are read by user id. A failure to evaluate policies or org settings fails the request closed (503).

What leaves your network

To What When
Upstream LLM The request as the application sent it, after sanitizing (and spotlighting, if on) Every allowed request
SSO provider OIDC authorization flow SSO logins
Alert channels Alert metadata: event type, severity, ids, categories, scores. No prompt text Matching alert rules
OTLP collector Spans without prompt text; traceparent to the upstream only with OTEL_PROPAGATE_UPSTREAM=true Tracing on
Hugging Face (or MODELS_MIRROR) Model downloads, checked against SHA-256 pins models pull only; gateway-full needs none

There is no telemetry or call home.

Threat model

The risks that remain when you run Guard Spec, grouped as detection, application, deployment, identity, data, supply chain and operations risks, each with what reduces it, are in Security risks and hardening (Vietnamese: security-risks.vi.md). Its hardening checklist is the shortest path to a reviewed deployment.

Verifying a release

Images are signed with cosign (keyless, GitHub Actions OIDC) and carry SPDX SBOMs and SLSA provenance; archives are covered by signed checksum files. The commands, what each check proves and their output for v0.1.1 are in Verifying a release.

Source: docs/security/README.md @ ca69dad29f02