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 inx-api-key(Bearer fallback) and returns{score, threshold, primary_outcome};primary_outcomeis omitted for benign content. Auth reuses the model gateway'sauthenticate_bearer_token, so a standard Preloop API key is the routed credential. - Scoring:
services/security_screen.pyis 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 fromPRELOOP_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_encryptedfor 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.pyvalidates theresult.jsonproduced by release security audit runs. Previous SHA+path finding rows are a floor: dropping one without aresolvedmarker plus a reason fails, unclassified rows fail, andsecrets_findings_countmust match the row count. The agent never self-grades the floor. - Git Guard:
git_guard.pyallow-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 wiresvalidate_gap_registerinto result ingestion. - Waiver Inputs:
waivers.pydeterministically 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. Interactive006waivers bind storedask_usertool_result/responses(exact finding id and human reason), notstatus=approvedor question text. Ambiguousrequest_approvalprose, including awaive_findingoperation, 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/CIgate.fail_on_kev/gate.fail_on_cvss_gte(in[0, 10]) override it: never model-authoredgate.policydisplay 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 verifyandpreloop evidence verifyrecompute 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, JWTsid/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.