Skip to content

Security

Yigtwxx edited this page Jul 12, 2026 · 1 revision

Security

Maestro's security model spans authentication, secret handling, agent-loop safety, input hardening, and data-deletion guarantees. This page collects the decisions; the mechanisms are cross-referenced to their pages.

Authentication

  • Passwords — hashed with Argon2 (PasswordHasher).
  • JWT — HS256, three token types: access, refresh, mfa (create_token / decode_token in core/security.py). Access token TTL 30 min, refresh 7 days (configurable).
  • Refresh-token rotation + reuse-detectionservices/auth_service.py. Every refresh issues a new token in the same family_id (session lineage). If a already-rotated token is presented again (reuse), the entire family is revoked — a stolen refresh token can't outlive one use. list_active_sessions, revoke_family, revoke_other_families, logout.
  • 2FA (TOTP)services/two_factor_service.py: begin_setup (QR SVG), enable (returns recovery codes), verify_login. Login returns an mfa challenge; the client completes via POST /auth/login/totp. Recovery codes are Argon2-hashed and single-use.

Secret handling (BYOK)

  • API keys encrypted with AES-256-GCM; the master key (API_KEY_MASTER_KEY) is a 32-byte key held only in the environment. The TOTP secret uses the same key. See LLM-Providers-and-BYOK.
  • Keys are never logged, stored in plaintext, or returned to the frontend — only provider, label, key_hint.
  • If a task starts without the required key, it stops and warns the user.
  • Email verification / reset tokens store only a SHA-256 hash; the raw token lives only in the email link.

Rate limiting

utils/rate_limiter.py — sliding-window limiter with a Redis Lua backend (atomic ZSET log) and an in-memory fallback (MemoryBackend, self-draining). The dispatcher tries Redis, and on RedisError/OSError/timeout logs a warning, falls back to memory, and opens a 10-second circuit breaker (RATE_LIMIT_REDIS_COOLDOWN_SECONDS). A Redis outage is not an API outage.

  • Identity — a valid access token keys by user:{jwt_sub}, otherwise ip:{addr}. Behind a proxy (TRUST_PROXY_HEADERS=true), the IP is read from the rightmost X-Forwarded-For entry (the hop the proxy adds — the one a client can't forge).
  • Tiers (constants.py, all 60s windows): PUBLIC 30, AUTH 20, READ 60, WRITE 20, PAYMENT 10, EXPENSIVE 30, UPLOAD 10, WEBSOCKET 30. Bucket key rl:{tier}:{scope}:{identity}.
  • Every HTTP route declares an explicit dependencies=[rate_limit(...)]; tests/test_rate_limiter.py::test_every_http_route_declares_a_rate_limit fails the build if one is missing. WebSocket routes call check_websocket before accept() and close with 1013 on excess.

Deployment note: TRUST_PROXY_HEADERS must be true only behind a proxy that sets X-Forwarded-For (e.g. Caddy). Directly internet-facing with it on, a client forges the header and opens a new bucket per request; behind a proxy with it off, everyone shares the proxy's single bucket.

Input hardening

  • SSRF guardutils/url_guard.py check_public_url blocks private/loopback/link-local targets. Applied to custom LLM endpoints and the data_fetch tool (LLM_SSRF_GUARD_ENABLED).
  • Prompt-injection scanutils/prompt_guard.py scan_prompt runs heuristics on system prompts. Mandatory on marketplace publish and on custom-agent writes; malicious patterns are rejected.
  • Code-execution sandboxcode_execution_service.py runs Python in a throwaway Docker container with CPU/memory/time limits. Off by default in production (CODE_EXECUTION_ENABLED=false) because enabling it requires mounting the Docker socket = host takeover risk.

Agent-loop safety

Infinite-loop and runaway-cost protection (see Agent-Orchestration):

  • MAX_ITERATIONS = 10 per subagent.
  • MAX_REVIEW_ITERATIONS = 3 for the reviewer↔subagent loop.
  • TASK_TIMEOUT_SECONDS = 1800 total.
  • Hierarchical token budget (TASK_TOKEN_BUDGET_DEFAULT = 200,000) with a per-call BudgetGuard; step-boundary quota re-checks.
  • Exceeding any limit stops the task, informs the user, and logs. Because the engine is durable, a killed task still records the tokens it spent.

User-data isolation

All RAG queries filter by user_id; one user's memory can never surface for another. Custom agents run inside a sandboxed <agent_persona> block. Marketplace agents cannot read the installing user's keys directly — all provider calls go through the sandboxed service layer.

Account deletion (GDPR Art. 17 / KVKK Art. 7)

users.deletion_requested_at is the single source of truth.

  1. Request (DELETE /users/me) — account is immediately locked: ActiveUser returns 403 on all product endpoints, but login/refresh still work so the user can self-serve undo.
  2. Grace period — 30 days (ACCOUNT_DELETION_GRACE_DAYS). POST /users/me/deletion/cancel restores it. Only an active subscription is cancelled on request.
  3. Purge — the cron job python -m app.scripts.purge_deleted_accounts runs user_service.purge_user_data after the grace period.

Purge contract: delete order is Mongo → Qdrant → PostgreSQL last (the PG row carries the flag that lets the sweep re-find the account). purge_user_data raises rather than swallowing errors, so a failed run leaves the flag in place and the next sweep retries. All operations are idempotent. marketplace_items are not deleted — the author is unset and anonymized. GET /users/me/export provides the Art. 20 data export.

Observability

Errors flow to Sentry (env-gated, PII scrubbed, sendDefaultPii: false). Structured JSON logs (LOG_FORMAT=json) carry request_id / task_id / user_id. Agent fallbacks log at WARNING (below Sentry's ERROR event threshold) so one task raises at most one Sentry event. See Configuration and Deployment.

Clone this wiki locally