Security

How GnamiAI is put together.

GnamiAI is a multi-tenant AI workspace. Several of the design choices below exist specifically because running an agent for strangers is a different threat model than running one for yourself. This page lists them honestly, including the limits.

Authentication

Key material at rest

Every provider API key you connect (OpenAI, Anthropic, OpenRouter, Ollama, Mem0) is encrypted in the database with AES-256-GCM using a 32-byte key encryption key held as a server environment variable. Each key gets a fresh 12-byte IV and a 16-byte authentication tag. Changing the KEK without access to the previous one makes every stored key unrecoverable ciphertext — the operator backs it up.

The per-request HMAC key that signs your session cookies is also a 32-byte env var, separate from the KEK. Rotating it invalidates every outstanding session by design.

Tenant isolation

Every tenant-scoped table carries a tenant_id column. Every non-admin API handler begins with requireTenant(), which re-verifies your session against the DB and returns the tenant record. Every subsequent query filters by that tenant id. Admin endpoints are additionally gated by requireAdmin() which re-reads the is_admin flag from your DB row on every call — never from the session cookie.

A tampered client that submits another tenant's id cannot read or mutate that tenant's data. Server handlers ignore caller-supplied ids in the paths where it matters; the filter is always the session's tenant.

Multi-tenant guardrails

SSRF protection

Two features accept URLs you supply: the Ollama integration (base URL) and skill-install-from-URL. Both run the URL through a guard that rejects:

DNS-based rebinding is a residual risk; we document it rather than pretend otherwise. For the maximum-security deployment, operators run the fetches through an egress proxy that resolves and pins IPs.

What GnamiAI explicitly cannot do

Content that reaches Postgres

Every text field that gets inserted into Postgres passes through a sanitiser that strips \x00 bytes (which Postgres refuses in TEXT columns) and normalises Windows line endings. This is belt-and-braces: the guard above rejects obvious non-markdown content at fetch time, and the sanitiser catches anything that slipped.

Scheduled runs

The endpoint that fires scheduled tasks (/api/cron/tick) is protected by a server-only CRON_SECRET via an Authorization: Bearer header. Without the correct secret, the endpoint refuses to run. A public request cannot cause scheduled work to execute on anyone's account.

Transport

The deployment runs on Vercel's edge with TLS 1.3. HSTS headers are served so browsers refuse to downgrade to HTTP on subsequent visits.

Responsible disclosure

Found something? Email the operator. Please don't exploit the finding against other tenants — that's a rule, not a suggestion. Reports that include a concrete reproduction get a response; vague speculation does not.