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.
On this page
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.
Enable 2FA
Confirm
Save recovery codes
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 metadataPOST /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
APP_KEY
Storage format: base64(JSON{v:2, n:nonce, c:ciphertext})
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:
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.
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
Session context
set_config('app.current_team_id', team_id, false)
Role switch
agent_fleet_rls)
that is subject to FORCE ROW LEVEL SECURITY.
Policy enforcement
WHERE team_id = current_setting('app.current_team_id').
Reset
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.
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.
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.
Execution
Settlement (after AI call)
SettleBudgetAction compares actual vs reserved cost.
Overage is charged; surplus is refunded — all within a locked transaction.
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:
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.