Security
Floopy sits between your application and LLM providers, processing API keys, prompts, completions, and billing data at scale. Security is built into every layer — from the Rust runtime that eliminates entire vulnerability classes at compile time, to the tiered encryption system that protects your data at rest and in transit.
Security Principles
Floopy’s security model is built around three principles:
Defense in depth. Every request passes through multiple independent security layers (rate limiting, authentication, subscription validation, LLM firewall) before reaching a provider. A failure in one layer does not compromise the others.
Zero trust on content. Prompts and completions are treated as untrusted input. The LLM firewall scans for injection attacks and unsafe content before forwarding. Logs containing sensitive content are scrubbed for PII automatically.
Fail safe, not fail open. External dependency failures (ClickHouse down, Redis unreachable, provider timeout) degrade functionality gracefully without exposing data or bypassing security checks. Auth failures reject the request — they never allow it through.
Architecture Separation
graph TB
subgraph Gateway["Olympus — AI Gateway (Rust)"]
RL[Rate Limiter] --> Auth[API Key Auth]
Auth --> Sub[Subscription Check]
Sub --> FW[LLM Firewall]
FW --> SSRF[SSRF Validation]
SSRF --> Router[Provider Router]
end
subgraph Dashboard["Zeus — Dashboard (Next.js)"]
SA[Supabase Auth] --> RBAC[Org Access Control]
RBAC --> Actions[Server Actions]
end
App[Your Application] --> Gateway
Gateway --> Providers[LLM Providers]
Gateway -.->|Async Logs| CH[(ClickHouse)]
Dashboard --> Supabase[(Supabase)]
Dashboard --> CHThe gateway and dashboard are architecturally separated. Zeus connects directly to Supabase and ClickHouse — it never proxies through Olympus. A compromised gateway cannot access dashboard data, and a compromised dashboard does not affect gateway request processing.
API Key Security
Key Format
Floopy API keys use a structured format with prefix and checksum:
flo_sk_live_<32_random_chars>_<crc32_checksum>flo_sk_prefix — enables automatic detection by secret scanning tools (GitHub Advanced Security, GitGuardian, TruffleHog). If your key leaks in a public repository, these tools flag it instantly.live/testmode — distinguishes production keys from sandbox keys.- CRC-32 checksum — allows client-side validation before making a network round-trip. Catches typos and truncation immediately.
Key Storage
API keys are never stored in plaintext. When you create a key, it’s shown once — then hashed with SHA-256 before storage. Every subsequent lookup compares hashes, not plaintext values. There is no way to retrieve the original key from the database.
Auth Caching
Key lookups are cached in Redis with a configurable TTL to avoid database round-trips on every request. When you revoke or rotate a key in the dashboard, the cache is invalidated immediately — revocation takes effect within seconds, not minutes.
Provider Key Encryption
Your LLM provider API keys (OpenAI, Anthropic, Gemini, etc.) are encrypted at rest using XChaCha20-Poly1305 envelope encryption:
- Each provider key gets its own Data Encryption Key (DEK)
- The DEK is encrypted by a Key Encryption Key (KEK) managed via KMS
- 192-bit random nonces eliminate birthday-paradox collision risks that affect AES-GCM (96-bit nonces)
- Keys are decrypted only at runtime when forwarding to the provider — never cached in plaintext
- Plaintext DEK material is wiped from memory (
zeroize) immediately after use
KEK Rotation
The KEK used to wrap each DEK is rotatable with zero downtime. Multiple KEK versions coexist: new provider keys are encrypted with the current KEK, while existing ciphertexts continue to decrypt with whatever KEK originally wrapped them. Every encrypted record carries its KEK version and encryption timestamp for audit purposes, and the version byte is cryptographically bound to the ciphertext via AEAD — tampering with it fails authentication rather than coercing a different key lookup.
Rate Limiting
Floopy implements tiered rate limiting to prevent abuse while being fair to enterprise customers:
| Tier | Key | Default Limit | Purpose |
|---|---|---|---|
| Anonymous | IP address | 20 rpm | Block unauthenticated abuse |
| Authenticated | Organization ID | Plan-based (RPS / 10s / RPM) | Fair usage (not per-IP — safe behind NATs) |
| Per-Key | API Key ID | Configurable | Granular control per application |
Authenticated tiers enforce three concurrent sliding windows — 1-second (RPS), 10-second (burst), and 60-second (RPM) — evaluated atomically in a single Redis round-trip. When a request trips a window, the response returns 429 with a Floopy-RateLimit-Exceeded: rps|w10|w60 header identifying which window was exceeded. Plan defaults: Free 5/50/60, Starter 20/200/300, Pro 100/1,000/10,000, Enterprise custom.
Rate limits use atomic Redis sliding windows for consistency across horizontal scaling. Authenticated requests are limited by organization, not by IP — this prevents false limiting for enterprise customers behind corporate NATs or shared egress IPs.
LLM Firewall
Single-stage prompt security that runs before any request reaches an LLM provider.
A safety-tuned LLM (configured via FIREWALL_MODEL, defaults to meta-llama/Llama-Guard-4-12B on Together — the same notation accepts a Bedrock or weighted-distribution config) classifies every prompt as safe or unsafe. Categories considered unsafe: prompt injection, jailbreak attempts, self-harm content, sexual content involving minors, illegal-activity instructions, hate speech.
A Qdrant verdict cache sits in front of the LLM call. When the request’s embedding matches a recent unsafe verdict above FIREWALL_SEMANTIC_CACHE_THRESHOLD (default 0.95), the cached verdict short-circuits without calling the LLM. Only unsafe verdicts are cached — safe results are recomputed each request so a model upgrade can flip them without a runbook step.
- Enable per-request via
floopy-llm-security-enabled: true - Plan-gated by
has_advanced_firewall - Blocked requests return
400 PROMPT_THREAT_DETECTED - Each backend call records
backend+model_refon the request row; cache hits logmodel_ref="cache:firewall_verdicts" - Fail-open semantics: any LLM failure (network, parse) lets the request through with a loud log
SSRF Protection
The gateway validates all outbound requests against a strict provider allowlist:
- Only known provider hostnames are allowed (
api.openai.com,api.anthropic.com,generativelanguage.googleapis.com,api.groq.com,api.mistral.ai,api.deepseek.com) - DNS resolution results are checked against private/reserved IP ranges (RFC 1918, RFC 6598, link-local, loopback, CGNAT)
- Customer-supplied URLs never influence the destination — the provider is determined by model mapping
- SSRF attempts are logged as security events
Header Sanitization
Client headers are sanitized before forwarding to providers using a blocklist approach:
- Stripped:
authorization,cookie,set-cookie,proxy-authorization,x-forwarded-for,x-forwarded-host,x-forwarded-proto,x-real-ip,host - Passed through: All other headers, including provider-specific ones (
anthropic-version,OpenAI-Organization) - Provider authentication is injected after stripping — your provider keys never mix with client headers
PII Scrubbing
Request and response bodies are automatically scrubbed for PII before logging to ClickHouse:
| Pattern | Example | Replacement |
|---|---|---|
| Email addresses | user@example.com | [REDACTED:email] |
| CPF numbers | 123.456.789-00 | [REDACTED:cpf] |
| SSN | 123-45-6789 | [REDACTED:ssn] |
| Credit card numbers | 4111 1111 1111 1111 | [REDACTED:credit_card] |
| Phone numbers | +55 11 91234-5678 | [REDACTED:phone] |
| API keys | sk-abc123... | [REDACTED:api_key] |
| Bearer tokens | Bearer eyJ... | [REDACTED:bearer] |
Scrubbing runs in the async logging path — it never blocks or slows down your request.
Trace Attribute Redaction
In addition to body PII scrubbing, span attributes (the metadata attached to distributed traces) are passed through a dedicated redaction pass before shipping to ClickHouse or to any per-organization OTLP collector. Regex patterns catch Bearer ... tokens, sk-... / sk-ant-... / AIza... provider key shapes, and long opaque secret-like values; sensitive attribute keys (authorization, api_key, token, secret, password) are redacted wholesale regardless of value. Redaction happens at the worker layer so it applies uniformly across the ClickHouse sink and every external OTLP destination.
Dashboard Security
| Protection | Implementation |
|---|---|
| Authentication | Supabase Auth with cookie-based sessions (httpOnly, secure, sameSite: lax) |
| Organization access | Every server action validates user membership via requireOrgAccess() |
| Admin operations | Destructive actions require requireOrgAdmin() (owner or admin role) |
| Security headers | X-Frame-Options: DENY, X-Content-Type-Options: nosniff, HSTS (2 years), Permissions-Policy (camera/mic/geo disabled) |
| Content Security Policy | Restricts script sources, frame ancestors, and connect targets |
| Query safety | All ClickHouse queries use parameterized {name:Type} syntax — no string interpolation |
| Rate limiting | 120 requests/minute per user on all server actions |
| Body size limit | 2 MB cap on server action payloads |
Role-Based Access Control
Floopy uses a three-tier RBAC model so that access granted at a wider scope (a group of organizations) flows down to every resource inside that scope, while narrower memberships remain authoritative at their own level.
The three scopes
| Scope | Supabase table | What it gates |
|---|---|---|
| Group | organization_group_members | Every organization that belongs to the group (and, transitively, every project inside those orgs). |
| Organization | organization_members | A single organization and every project inside it. |
| Project | project_members | A single project only. |
A user may appear in any combination of these tables. The has_org_access(auth.uid(), org_id) SQL helper evaluates them together and is the single predicate used by every Row-Level Security policy in Supabase — there is no bespoke per-table logic that could drift.
Access inheritance
has_org_access returns true if any of the following is true:
- The caller is a member of the target organization directly (
organization_members). - The caller is a member of the organization’s group (
organization_group_membersjoined viaorganizations.organization_group_id). - The caller is a member of at least one project inside the target organization (
project_membersjoined viaprojects.organization_id).
This means a “group owner” automatically has access to every org and project under the group without requiring a second membership row per resource. Revoking the group membership removes access to the entire subtree in a single operation.
Roles within a scope
Each membership row carries a role. Roles are scope-local — being an admin of a group does not make you an admin of a single project inside it unless you also have a project-level role. Scopes decide reach; roles decide what you can do within that reach.
| Role | Typical capabilities |
|---|---|
owner | Full control, including transfer of ownership and deletion of the scope itself. |
admin | Manage members, keys, and settings at this scope. |
member | Read and use resources; cannot manage members or destructive settings. |
The dashboard enforces roles via requireOrgAdmin() (owner/admin) and requireOrgAccess() (any member). The gateway uses has_org_access indirectly — API keys are scoped to a single organization, so cross-org access never reaches the gateway in the first place.
Row-Level Security
Every tenant-bearing table in Supabase (prompts, routing_rules, organization_api_keys, feedback, etc.) has an RLS policy of the form:
USING (has_org_access(auth.uid(), organization_id))This is the only access check on those tables. Direct SQL access through the Supabase client is as safe as server-action access through Zeus, because both traverse the same policy. A compromised client-side token can still only read rows the underlying user is already authorised to read.
Network & Transport
| Component | Transport Security |
|---|---|
| Client → Gateway | HTTPS (TLS 1.2+) via CDN/load balancer |
| Gateway → Providers | HTTPS to all providers (OpenAI, Anthropic, etc.) |
| Gateway → Redis | rediss:// (TLS) in production |
| Gateway → ClickHouse | HTTPS in production |
| Dashboard → Supabase | HTTPS (managed by Supabase) |
Request Flow Security
Every request passes through these layers in order. Each layer can reject the request independently:
- Body size check — requests over 10MB are rejected immediately
- IP rate limit — unauthenticated requests limited to 20 rpm per IP
- API key validation — key extracted, hashed, verified against cache/database
- Org rate limit — authenticated requests limited per organization
- Subscription check — expired plans are rejected with
403 SUBSCRIPTION_EXPIRED - Usage limits — monthly request quotas enforced, returns
429 MONTHLY_LIMIT_EXCEEDED - Prompt resolution — if using managed prompts, template variables are substituted
- Cache check — if cache is enabled, checked before any security-sensitive operation reaches the provider
- LLM Firewall — verdict cache lookup, then a safety-tuned LLM classifies the prompt
- SSRF validation — outbound URL verified against provider allowlist
- Header sanitization — dangerous headers stripped before forwarding
- Provider dispatch — request forwarded with clean headers and injected provider auth
- Async logging — response logged with PII scrubbing, never blocking the response path
Rust Security Guarantees
The gateway is written in Rust, which provides compile-time guarantees against:
- Buffer overflows — bounds-checked arrays, no manual memory management
- Null pointer dereferences —
Option<T>enforced by the type system - Data races — the borrow checker prevents concurrent mutable access
- Use-after-free — ownership model eliminates dangling pointers
- Memory leaks — deterministic drop semantics (no garbage collector)
These are not runtime checks — they are caught at compile time. Code that violates memory safety simply doesn’t compile.
Vulnerability Reporting
If you discover a security vulnerability, please report it responsibly:
- Email: security@floopy.ai
- Response SLA: Acknowledge within 48 hours, triage within 7 days
- Safe harbor: Researchers acting in good faith will not face legal action
- security.txt: Available at
https://api.floopy.ai/.well-known/security.txt
Compliance
Floopy’s security architecture is designed with compliance readiness in mind:
- Data residency — logs stored in your chosen region
- Data retention — configurable per plan, enforced via automated cleanup
- Audit trail — all security events logged for incident investigation
- Encryption at rest — provider keys (XChaCha20-Poly1305), log data (ClickHouse encryption)
- Encryption in transit — TLS on all connections
- Access control — three-tier RBAC (group / organization / project) with role-based permissions (owner, admin, member) and SQL-level Row-Level Security via
has_org_access