Skip to content

Security Considerations

Editions: OSS. Contributor documentation for this repository.

Auth, tenancy, redaction, and secret custody are platform concerns, not agent self-reporting. This chapter covers the security checklist, redaction policy, secret service, the tamper-evident audit chain and record signatures, security-screen scoring, and preloop.security.

Security Screen Scoring (QM Proxy Contract)

  • Purpose: Let external agent platforms delegate content security screening to Preloop through a documented HTTP contract, starting with QM's securityScreen: { backend: "proxy" } deployment option.
  • Endpoint: POST /api/v1/security-screen/score (api/endpoints/security_screen.py) accepts {text, hook, metadata} with the caller's token in x-api-key (Bearer fallback) and returns {score, threshold, primary_outcome}; primary_outcome is omitted for benign content. Auth reuses the model gateway's authenticate_bearer_token, so a standard Preloop API key is the routed credential.
  • Scoring: services/security_screen.py is a pure, deterministic rule engine: compiled case-insensitive regex categories (prompt_injection, destructive_command, destructive_sql, secret_exfiltration) with max-match-wins scoring. No I/O, no model calls, no persistence on the scoring path; the threshold comes from PRELOOP_SECURITY_SCREEN_THRESHOLD (default 0.7, clamped).
  • Privacy: Screened text is never logged or stored. Flagged chunks log score, outcome, matched rule names, and caller chunk coordinates only.
  • Rollout Semantics: Shadow vs enforce and fail-closed error handling live on the caller's side (per QM's contract); Preloop only scores.

Secret Service

  • Purpose: Provider-agnostic custody and resolution of model credentials.
  • Built-in Backend: local_encrypted for encrypted-at-rest credentials stored in Preloop-managed storage.
  • External Backends: Optional Vault/OpenBao-compatible KV v2 references via SecretReference.external_ref.
  • Runtime Boundary: Gateway-enabled runtimes receive Preloop gateway tokens instead of provider API keys.

preloop.security (./backend/preloop/security)

  • Purpose: Deterministic, server-side validation of security audit results. It is result validation, not scanning.
  • Gap-Register Freeze: gap_register.py validates the result.json produced by release security audit runs. Previous SHA+path finding rows are a floor: dropping one without a resolved marker plus a reason fails, unclassified rows fail, and secrets_findings_count must match the row count. The agent never self-grades the floor.
  • Git Guard: git_guard.py allow-lists metadata-only git invocations so no historical blob contents can be dumped into logs or transcripts. It has no production callers yet; it is the enforcement half of the planned follow-up that wires validate_gap_register into result ingestion.
  • Waiver Inputs: waivers.py deterministically factors human-authored waiver entries ({id, reason, author, date}, plus optional scope/expiry/package/version and the platform approval id for interactively collected ones) into a severity-gate outcome: alias-aware matching (a CVE id waives the same advisory surfaced under a GHSA/OSV alias), verbatim echo of applied entries, unmatched/invalid entries surfaced rather than dropped, and an unwaived failure always keeps the gate failed. Interactive 006 waivers bind stored ask_user tool_result/responses (exact finding id and human reason), not status=approved or question text. Ambiguous request_approval prose, including a waive_finding operation, is not a waiver. AI-judged and auto-approved rows cannot authenticate a human waiver or due-diligence decision. The severity gate is KEV or CVSS >= 9.0 unless trigger/CI gate.fail_on_kev / gate.fail_on_cvss_gte (in [0, 10]) override it: never model-authored gate.policy display text. There is no per-product policy table.
  • Scanner Boundary: Scanners (gitleaks, zizmor) are installed and run inside the agent execution sandbox per the release security audit preset (backend/presets/006-release-security-audit.yaml), never on the platform control plane.

Tamper-evident audit trail and signed records

  • Chain: A background pass seals audit rows per account into a hash chain (chain_seq, prev_hash, row_hash). Editing, deleting, or reordering a sealed row breaks every hash from that point. Sealing is off the request path so a chain error cannot fail the audited action. Signed checkpoints over the chain head are the artifact a customer can keep off the platform.
  • Keys: One active Ed25519 key per account. The private half is Fernet-encrypted with SECURITY__ENCRYPTION_KEY; the public half is published. Rotation retires the old key without invalidating signatures it already made.
  • Signatures: Detached, over a digest, with the payload type inside the signed bytes so a period-export signature cannot be replayed as an evidence-pack signature. Evidence packs are signed at capture, not at download. Signing is never a precondition: an unsigned export or pack is still served.
  • Verification: preloop audit verify and preloop evidence verify recompute hashes and signatures on the caller's machine and tell the caller to trust that walk over the server's verdict. Operator guide: Evidence storage and signed records.
  • What this does not prove: A compromised server can forge a record before it is signed. The chain is not WORM. Rows below the retention purge floor and rows not yet sealed are outside any result.

Authentication & Authorization

Preloop implements authentication and multi-tenancy:

Authentication: - JWT-based authentication for REST API and MCP endpoints - Token-based authentication with refresh token support - Email verification for new user accounts - Integration points for SSO and OAuth providers (future) - Per-user auth_generation (JWT gen claim). POST /auth/sessions/revoke-all increments it; get_current_user, POST /auth/refresh, the CLI JWT branch of POST /oauth/token, and the WebSocket upgrade (WebSocketAuthMiddleware) reject a token whose gen is behind the user. Tokens minted before the claim existed are treated as generation 0, so one bump also invalidates them. API keys and runner tokens are unchanged. preloop auth logout --all and the console Sign out everywhere control call that endpoint. - POST /auth/logout is the console's server-side sign out. It runs the registered H9 SessionHook.on_logout (from preloop.plugins.account_hooks) with the current token's claims and returns redirect_url: a same-origin path the console navigates to next, or null for the default. run_logout_hook drops any redirect that is not a same-origin path, and the console checks it again. With no hook registered it changes nothing server-side; the console clears its own tokens either way. Sign out everywhere skips this call because revoke-all already ended the session. - SessionHook.is_token_revoked runs after the generation and CLI session checks in reject_revoked_token, so an extension can revoke a single JWT it issued. With no hook registered it is not called. - Console refresh tokens (POST /auth/refresh) carry sat and are capped at MAX_SESSION_DAYS (default 30). CLI login refresh tokens stay long-lived (CLI_JWT_REFRESH_TOKEN_EXPIRE_DAYS, default 365); revocation is the control for those, not the session cap. - Each CLI login (/oauth/token without PKCE) records a cli_session row. Its access and refresh JWTs carry the row id as sid; the refresh token also carries a jti that must equal cli_session.refresh_jti. Rotation swaps the jti in one conditional UPDATE, so a refresh token that was already rotated away is rejected. Revoking the row (POST /oauth/revoke with either token, DELETE /auth/sessions/cli/{id}, preloop auth logout) rejects both tokens of that login in get_current_user, the WebSocket upgrade and the refresh path; other logins are unaffected. POST /auth/refresh does not accept a token with sid, so a CLI refresh token cannot be turned into console tokens that escape the row check. A CLI refresh token from before sid existed is moved onto a new row the next time it rotates; the old token itself stays covered by the generation check only. - POST /oauth/revoke still revokes opaque MCP tokens. A JWT without sid (console tokens, CLI tokens from before cli_session) gets 400 unsupported_token_type pointing at POST /auth/sessions/revoke-all instead of a false success.

Restricted CI identity foundation

CiPrincipal is an OSS machine principal with a versioned, typed grant for one account-local project and its explicitly bound hosted flow. Separate ApiKey rows have an explicit restricted_ci mode, version, principal reference and optional narrower action ceiling. Tokens use the existing high-entropy hashed key lifecycle and are disclosed once. Authentication reads current account, principal, key and binding state, intersects action ceilings, and returns a machine context with no human permissions. Scope enforcement audit/off settings never relax this mode. The binding snapshots provider identity, clone path, tracker host/type and organization lineage; changing any of these invalidates access rather than redirecting an existing grant.

This is a staged foundation. There are no public CI provisioning routes or usable CI setup controls yet. Generic REST, MCP, model gateway, WebSocket and session exchange authentication cannot treat a restricted key as its issuing human. The legacy key management surface excludes these keys; human CI administration must use the dedicated lifecycle seam. Operation enforcement, execution ownership and completion dispatch filtering must be implemented before usable provisioning is exposed.

The restricted ASGI guard covers every application role before handler work. Only one canonical Bearer transport can enter an explicitly classified, machine-aware HTTP handler. Duplicate or custom token transports, WebSockets, streams, unclassified operations and generic credential/session exchanges deny. An explicit test inventory includes lazy included routers, hidden routes and mounted applications through their middleware wrappers. OpenAPI records each operation's x-restricted-ci policy and authentication/authorization errors.

Machine-aware handlers use get_current_actor and an explicit ci_action on require_permission; unmarked handlers reject machine actors, including direct service calls. The machine branch never invokes the human RBAC/owner path. CRUD revalidates key, principal, account, action and immutable binding before protected work, so a retained request context cannot preserve revoked authority. The optional machine account-policy hook can only deny this core ceiling; missing hooks preserve OSS behavior and hook errors fail closed. Invalid restricted credentials receive 401, forbidden operations receive 403, and protected object handlers apply their consistent not-found policy. Decision logs include safe machine/resource identifiers, never token or result contents.

The production operation map remains empty during this stage. Execution and completion handlers will opt in only with typed inputs, own-resource CRUD and dispatch checks; this foundation does not expose usable CI provisioning.

The account owner can administer through core CRUD. An optional edition hook checks human operation/resource authority and may delegate or deny human administration, while the core account, project, flow and action checks always apply. Grant changes, disablement and key lifecycle changes are audited with human, principal, key and resource/action identifiers, never token material. Disabling an unusable binding or revoking its key remains possible for an authorized administrator. Permission changes to the provisioning human do not implicitly change the machine's persisted grant.

Execution and subscription ownership is nullable for historical rows and is never guessed or backfilled. Rotation atomically replaces a key, preserving principal ownership and narrowing to the previous key/current grant intersection. Revoking one key does not stop existing work or transfer/delete owned resources. Disable a principal to suspend all its machine access; stopping existing runs is a separate administrator action. Principals and bound resources use restrictive foreign keys to preserve attribution: retire identities by disabling them, retain historical owned rows, and explicitly remove retained dependent records before resource deletion. Deleting a human retains the principal's nullable administration attribution; existing key/user deletion semantics still revoke that human's keys. Schema rollback refuses restricted keys or retained principals rather than erase machine markers and turn a CI key into an owner key.

Multi-User Architecture: - Account Model: Represents an organization/company - User Model: Represents individual users within an account - All data is scoped by account_id for multi-tenancy isolation

Security Features: - Password hashing with industry-standard algorithms - Account-level data isolation (all queries filtered by account_id) - User invitation system with secure token-based email verification - Email verification and password reset links are bound to one user row (a uid claim next to the address). One address can hold a user in several accounts, so a link never resolves by address alone: it is refused if it has no uid (links minted before this binding) or if the row's address changed after it was sent, and the person requests a new one. The forgot-password and resend-verification forms send one link per row holding the address, each naming its username. crud_user.get_by_email raises AmbiguousEmailError rather than choose between rows; use list_by_email or an account scope.

Plugin System: - Extensible plugin architecture for adding custom functionality - Plugins can provide services, API routes, middleware, and dependencies - Built-in plugins: Argument-based condition evaluator for approval workflows - Plugin discovery via module paths or file system paths - Lifecycle hooks: on_startup() and on_shutdown()

Enterprise Features: Preloop Cloud and Preloop Enterprise add RBAC with 7 system roles, fine-grained permissions, team management, and comprehensive audit logging. Contact sales@preloop.ai for more information.

  • [x] All API requests authenticated via JWT tokens
  • [x] Multi-tenant data isolation (all queries scoped by account_id)
  • [x] User invitation system with secure token-based verification
  • [x] Password hashing with industry-standard algorithms
  • [x] Input validation for all parameters via Pydantic models
  • [x] Issue tracker credentials encrypted at rest via the Secret Service (credentials_secret_id/webhook_secret_id → SecretReference; a startup backfill migrates legacy plaintext rows)
  • [x] Sensitive data masked in logs (see Redaction Policy below)
  • [ ] Rate limiting to prevent abuse (partial implementation exists)
  • [ ] 2FA/MFA support for user accounts
  • [x] Session revocation via per-user token generation (auth_generation)
  • [x] Per-login CLI session revocation (cli_session, JWT sid/jti)
  • [ ] Regular security audits and dependency updates

Enterprise Security: Preloop Cloud and Preloop Enterprise add RBAC and comprehensive audit logging. Contact sales@preloop.ai for more information.

Redaction Policy

Preloop redacts sensitive data before logging, persisting to audit surfaces, or sending notifications. The centralized redaction module (preloop.utils.redaction) provides:

  • redact_dict(data): Recursively replaces values for sensitive field names (e.g. password, api_key, token, secret, credential) with ***REDACTED***.
  • redact_for_log(data): Produces a safe JSON string for log messages, with sensitive fields redacted and output truncated.

Redaction is applied in: - MCP tool execution logs (tool arguments) - Approval flow logs and notifications (tool args, approval URLs) - Flow execution MCP usage logs (persisted to DB) - Audit trail (configuration changes, tool executions) - Approval request emails and Slack/Mattermost messages

Known exceptions: Approval URLs are not logged in full (replaced with [sent via notification]). Progress tokens and request context metadata are not logged. Tracker credentials and AI model API keys are not logged when present in payloads.

Tests: tests/utils/test_redaction.py asserts that representative secrets never appear in redacted output.