Skip to main content

Security & Data Protection

FleetQ is built with a defense-in-depth security model. Multiple independent layers protect your data, credentials, and integrations — so that no single layer failure compromises the system. This page documents every protection running behind the scenes.

1. Authentication

FleetQ supports five authentication methods, each designed for a different access pattern.

Method Mechanism Expiry Use case
Session (Web) Laravel Fortify + Redis sessions Session TTL Browser access
Two-Factor (TOTP) Authenticator app + recovery codes Per-session Hardened web login
API Token (Sanctum) Bearer token, af_ prefix 30 days REST API, mobile, CLI
OAuth2 (Passport) Authorization Code + PKCE 24h access / 30d refresh MCP clients (Cursor, Claude.ai)
Webhook HMAC HMAC-SHA256 signature verification Per-request Signal ingestion, integrations

Two-Factor Authentication (2FA)

FleetQ supports TOTP-based two-factor authentication via authenticator apps (Google Authenticator, Authy, 1Password, etc.). Enable it from your profile settings.

1

Enable 2FA

Scan the QR code with your authenticator app.
2

Confirm

Enter the 6-digit code from your authenticator to activate. 2FA is not active until confirmed.
3

Save recovery codes

Download or copy the auto-generated recovery codes. These are single-use codes in case you lose your authenticator device.

The 2FA secret is encrypted with APP_KEY and never stored in plaintext. Recovery codes can be regenerated at any time from your profile. 2FA challenges are throttled to 5 attempts per minute.

API Token Scoping

Sanctum tokens are issued with team:{team_id} abilities — never the wildcard *. The ScopeTokenToTeam middleware rejects any request where the token's team claim doesn't match the target resource. Token prefix af_ allows secret-scanning tools to identify leaked tokens.

Manage active tokens from Team Settings → API Tokens or via GET /api/v1/auth/devices. Revoke individually or all at once.

OAuth2 for MCP Clients

The MCP server uses OAuth 2.1 with PKCE for HTTP/SSE clients (Cursor, Claude.ai, etc.). The platform publishes standard discovery endpoints:

  • /.well-known/oauth-authorization-server — RFC 8414 server metadata
  • /.well-known/oauth-protected-resource — RFC 9728 resource metadata
  • POST /oauth/register — RFC 7591 dynamic client registration (20/hour limit)

Local MCP connections (stdio) auto-authenticate as the default team owner — no token exchange needed.

Chatbot Token Authentication

Chatbot embeds use dedicated tokens stored as SHA256 hashes (plaintext never stored). Each token supports optional origin validation (allowlisted domains), expiry dates, and automatic last_used_at tracking.

Login Throttling

Endpoint Limit Key
Login 5/min email + IP
2FA challenge 5/min session ID
Password reset request 3/min email + IP
Token refresh 10/min IP
OAuth client registration 20/hour IP

2. Credential Encryption

Every API key, OAuth token, and bearer credential is encrypted using per-team envelope encryption with two independent layers:

2-Layer Encryption Architecture

Layer 1 Team DEK (32-byte key) encrypted with platform APP_KEY
Layer 2 Credential data encrypted with Team DEK using XSalsa20-Poly1305 (libsodium)

Storage format: base64(JSON{v:2, n:nonce, c:ciphertext})

Even a full database breach cannot expose raw credentials without the APP_KEY. The per-team DEK adds a second layer: compromising one team's key doesn't affect others.

Encrypted across 7 columns in 5 models: Credential secrets, team provider credentials (BYOK), tool credentials, outbound connector configs, Telegram bot tokens, and webhook signing secrets.

Backward Compatibility

Three encryption formats are supported transparently:

  • v2 (current): Team DEK + XSalsa20-Poly1305
  • v1: Laravel's standard encrypt() with APP_KEY
  • v0: Legacy PHP-serialized format

Decryption auto-detects the format. New writes always use v2. Batch-migrate with:

bash
php artisan credentials:re-encrypt --batch=50
# Add --dry-run to preview without changes

3. External KMS Integration

For enterprise deployments, the team DEK can be wrapped by an external Key Management Service instead of the platform APP_KEY. This means revoking KMS access immediately revokes all credential access — there is no APP_KEY fallback when KMS is active.

AWS KMS

Key ARN + optional AssumeRole with external ID for cross-account access.

Azure Key Vault

Vault URL + client credentials (tenant ID, client ID).

Google Cloud KMS

Project, location, key ring, and key name.

Unwrapped DEKs are cached in a 3-layer cache (in-memory → Redis 5min → KMS API call) to minimize latency. KMS unwrap failures are logged to the audit trail and set the config status to Error.

KMS credentials are encrypted with APP_KEY (not the team DEK) to avoid a circular dependency — you need KMS to unwrap the DEK, but you'd need the DEK to decrypt KMS credentials.

4. Tenant Isolation

Data isolation is enforced through four independent layers. A bug in any single layer cannot expose cross-tenant data because the other three layers independently enforce boundaries.

Layer Mechanism Scope
Application TeamScope global scope + BelongsToTeam trait auto-apply WHERE team_id = ? to every Eloquent query All models
Database PostgreSQL Row-Level Security policies enforce team_id filtering at the database engine level All team-scoped tables
MCP Tools All 268+ tools use explicit where('team_id', $teamId) guards MCP server
Encryption Per-team DEK means even raw database access can't decrypt another team's credentials All encrypted fields

Platform records (team_id = NULL) are shared reference data visible to all teams but read-only — the PlatformRecordGuardObserver returns 403 on any update or delete attempt.

5. PostgreSQL Row-Level Security

Beyond the application-level TeamScope, FleetQ uses PostgreSQL's native Row-Level Security (RLS) as a second enforcement layer directly in the database engine. Even if the application layer has a bug that bypasses TeamScope, the database itself prevents cross-tenant queries.

How it works

1

Session context

Each web request sets a PostgreSQL GUC variable: set_config('app.current_team_id', team_id, false)
2

Role switch

The connection switches to a non-superuser role (agent_fleet_rls) that is subject to FORCE ROW LEVEL SECURITY.
3

Policy enforcement

RLS policies on every team-scoped table restrict rows to WHERE team_id = current_setting('app.current_team_id').
4

Reset

After the request, RESET ROLE restores the original connection role.

Queue jobs (Horizon)

Horizon workers reuse database connections across jobs, so queue jobs use SET LOCAL inside a transaction. This scopes the team context to the current transaction only — it automatically resets at COMMIT or ROLLBACK, preventing context leak between jobs.

RLS is a defense-in-depth addition — it does not replace TeamScope. If the agent_fleet_rls role doesn't exist (migration hasn't run), the middleware gracefully becomes a no-op.

6. Authorization & Roles

Team members are assigned one of four roles. Permissions are enforced via Laravel gates.

Role Manage team Edit content View Billing
Owner Yes Yes Yes Yes
Admin Yes Yes Yes No
Member No Yes Yes No
Viewer No No Yes No

The AI Assistant's tools are also role-gated: read tools (list, get, search) are available to all roles, write tools (create, update) require Member+, and destructive tools (delete, toggle status) require Admin or Owner.

7. Budget Enforcement

Budget controls use pessimistic locking and atomic Redis operations to prevent race conditions. No TOCTOU vulnerability is possible.

1

Reservation (before AI call)

ReserveBudgetAction acquires a SELECT FOR UPDATE lock on both the experiment and credit ledger, then deducts the estimated cost (with 1.5x safety multiplier) within a DB transaction.
2

Execution

The AI call proceeds. If the model call fails, the reservation is released (no charge).
3

Settlement (after AI call)

SettleBudgetAction compares actual vs reserved cost. Overage is charged; surplus is refunded — all within a locked transaction.
4

Circuit breaker

PauseOnBudgetExceeded fires on every experiment transition. If the budget is exhausted, the experiment is paused before the next job runs.

Usage counters for plan limits use Redis Lua scripts for atomic check-and-increment — the check and increment happen in a single Redis operation, preventing any race between concurrent requests.

8. Rate Limiting (5 Layers)

Five independent rate-limiting layers protect different parts of the system:

Layer Scope Default limit Mechanism
HTTP Per route 5–120/min Laravel throttle middleware
AI Provider Per provider 30–100/min AI middleware pipeline
Queue Per team × queue 60/min Redis sorted set sliding window
Outbound channel Per channel × experiment 10–50/window Configurable per channel
Outbound target Per recipient 7-day cooldown Prevents duplicate outreach

9. AI Gateway Security

Every LLM call passes through a 6-stage middleware pipeline:

RateLimiting BudgetEnforcement IdempotencyCheck SemanticCache SchemaValidation UsageTracking

Circuit Breaker

A per-provider circuit breaker protects against cascading failures from external LLM APIs. Three states: Closed (healthy) → Open (after 5 failures, all calls rejected) → Half-Open (after 60s, probe request sent) → back to Closed on success.

Idempotency (Deduplication)

Duplicate LLM requests are detected via xxh128 hash of the system prompt + user prompt. Identical requests return the cached response at zero cost. Pending or failed requests are retried automatically.

Semantic Cache

Near-duplicate prompts are matched via pgvector cosine similarity (threshold 0.92). Only the normalised prompt text is stored — never raw credentials, PII, or personally identifiable data. Cache lookups are team-scoped via explicit where('team_id', ...).

Kill Switch

The CheckKillSwitch job middleware runs on every pipeline stage job. Killed experiments: job silently skipped. Paused experiments: job released back to the queue with a 60-second delay, automatically resuming when un-paused.

10. SSRF Protection

SsrfGuard validates all outbound URLs (webhooks, RSS feeds, OAuth callbacks, SMTP hosts) against private address ranges:

IPv4 Private

10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16

Loopback

127.0.0.0/8, ::1

Link-local / Metadata

169.254.0.0/16, 100.64.0.0/10

IPv6 Private

fc00::/7 (unique local), fe80::/10 (link-local)

An attempt to configure a webhook or connector pointing at an internal IP returns 422 Validation Error.

Outbound Header Allowlist

Outbound webhook connectors only forward headers matching x-* and content-type. Headers like Authorization, Host, and Cookie are stripped — preventing header injection attacks that could proxy authentication credentials.

SMTP Host Validation

Custom SMTP servers are validated against the same CIDR blocklist. Email recipient addresses are validated via FILTER_VALIDATE_EMAIL, and from-address headers are sanitised to prevent header injection (\r, \n, <, > stripped).

Outbound Blacklist

Before every outbound delivery, a 4-level blacklist check runs: email exact match, domain match, company name (case-insensitive), and keyword substring search. Blacklisted deliveries are blocked with a logged reason.

11. Webhook Security

Inbound Webhook Verification

Signal webhooks require HMAC-SHA256 signature verification via the X-Webhook-Signature header. Signatures are compared using constant-time hash_equals() to prevent timing attacks. In production, unsigned requests are rejected with 403 (fail-closed).

Integration-Specific Verification

Each integration connector has its own signature verification method:

  • GitHub: HMAC-SHA256
  • Slack: HMAC-SHA256 (Events API)
  • Discord: Ed25519 signature
  • Stripe: Stripe signature verification
  • Jira, Linear, Zendesk, PagerDuty: Per-provider HMAC

Replay Protection

Inbound webhooks include an X-Webhook-Timestamp header. Requests older than 5 minutes are rejected — preventing replay attacks. Outbound webhooks use idempotency keys for deduplication and HMAC-SHA256 signing.

12. Security Headers & CSP

The SecurityHeaders middleware sets protective headers on every response:

Header Value Protection
X-Content-Type-Options nosniff Prevents MIME-type sniffing
X-Frame-Options SAMEORIGIN Clickjacking protection
Referrer-Policy strict-origin-when-cross-origin Limits referrer leakage
Permissions-Policy camera=(), microphone=(), geolocation=() Restricts browser APIs
Strict-Transport-Security max-age=31536000 HSTS (production only)

Content Security Policy: default-src 'self', scripts from allowlisted CDNs only, form-action 'self', object-src 'none', base-uri 'self'. CSRF is enforced on all web routes via Laravel's built-in middleware.

13. Credential Lifecycle

Types

Type Fields Use case
api_key token External API services
oauth2 access_token, refresh_token OAuth2 integrations
basic_auth username, password Legacy HTTP auth
ssh_key private_key, passphrase SSH/Git operations
custom Flexible key-value Custom credential schemas

Human vs AI-Created Credentials

Credentials created by a human are Active immediately. Credentials created by AI agents enter PendingReview status and require human approval before they can be used — preventing AI agents from autonomously creating and using credentials without oversight.

Secret Rotation

Rotate any credential's secrets via the detail page or POST /api/v1/credentials/{id}/rotate. Rotation re-encrypts with the current team DEK and updates the last_rotated_at timestamp.

BYOK (Bring Your Own Key)

Teams configure their own LLM provider API keys (Anthropic, OpenAI, Google, custom endpoints) via Team Settings. Keys are encrypted with the team DEK and masked in the UI (**** + last 4 chars). The provider resolver injects credentials at runtime: Skill → Agent → Team → Platform default.

SSH Fingerprint Verification (TOFU)

SSH connections use Trust On First Use: the first connection stores the host's SHA256 fingerprint. Subsequent connections verify the fingerprint matches — a mismatch (possible MITM attack) throws an error and blocks the connection.

14. Immutable Audit Log

Every significant action is recorded in an append-only audit_entries table. Entries are never updated or deleted within the retention window.

Experiment transitions

Every state change — who triggered it, when, and why

Approval decisions

Approve/reject with reviewer identity and decision context

Budget events

Reservations, settlements, alerts, and exhaustion events

Agent events

Health check failures, status changes, execution starts

Credential access

Every decryption — requesting agent, experiment, and purpose

Team changes

Invitations, role changes, token creation/revocation

Retention is enforced by your plan. See Audit Log for details on retention periods, compliance use cases, and API access.

Defense in Depth

FleetQ's security model ensures that no single layer failure compromises the system. Application-level scoping, database RLS, per-team encryption, and MCP tool guards all enforce boundaries independently — so a bug in one layer is caught by the others.

See also: Credentials (managing secrets), Audit Log (querying the audit trail), Budget & Cost (spending controls).