Security model
Technical deep dive: encryption, fail-closed Shield, canary tokens, and audit findings - for teams evaluating Prune.
Quick answer
Prune protects provider API keys with AES-256-GCM encryption for vaulted provider API keys and key separation in the vault. Shield spend caps and rate limits run before every upstream call and fail closed - if policy cannot be verified, the request is blocked, not passed through.
AES-256-GCM vault encryption with key separation
When you store an OpenAI or Anthropic key in the Prune vault, we encrypt it with AES-256-GCM before writing to the database.
Key separation means the key that encrypts your provider credentials is not stored next to the ciphertext. The master key lives in a separate secrets manager; the database holds only encrypted blobs and metadata. A database-only breach does not expose plaintext API keys.
Prune API keys (prune_…) are never stored in recoverable form - only keyed HMAC-SHA256 hashes, so a leak cannot be replayed.
Fail-closed Shield
Shield enforces rate limits, daily spend caps, model allowlists, and IP restrictions on every proxied request. Daily spend is read from your usage records in the database; per-minute rate limits use Redis counters.
Fail closed means: if Shield cannot load your policy or today's spend (database error or timeout), the request returns 503 with X-Prune-Shield-Code: shield_unavailable. It does not bypass caps and call the provider. Spend caps checked before the provider is called.
Rejected requests include X-Prune-Shield-Code so your app can distinguish spend caps from auth failures. See error codes.
What fails open and what fails closed
A gateway has to pick, for each dependency, whether an outage blocks your traffic or lets it through. Prune blocks when letting the call through could cost you money or skip a limit you set, and keeps serving when the only loss is a saving.
| If this is down or slow | What happens |
|---|---|
| Database (policy or spend lookup) | Fails closed: 503 shield_unavailable. After repeated timeouts, new requests get 503 with Retry-After instead of queueing. |
| Redis (rate-limit counters) | Limits keep working from in-process counters. Spike detection pauses until Redis is back. |
| Response cache | Fails open: treated as a cache miss and sent to the provider, so you lose the saving, not the answer. |
| Your provider | The provider's error is passed through. With a fallback model set, Prune retries on it first. |
Canary token injection (OWASP LLM07:2025)
Optional per-request canary tokens are embedded in system context. If the model echoes the canary in its output, Prune flags a possible system-prompt leakage event. Enable with X-Prune-Shield-Canary: true; block responses with X-Prune-Shield-Canary-Block: true.
Receipt fields: shield_canary_injected, shield_canary_leaked.
PII tokenization
When enabled, emails, phone numbers, and names are replaced with reversible placeholders before the request leaves Prune for the upstream provider. The model never sees raw PII; your app can detokenize responses if needed.
Spend cap race condition fix
Daily spend tracking uses atomic Redis INCRBYFLOAT operations so concurrent requests cannot overshoot a cap between read and write. Caps are checked before the upstream LLM call fires.
Internal security audit
Before opening signups, we ran an adversarial security pass across proxy routes, vault handling, and Shield enforcement. Findings included:
- Internal security audit: 4 of 6 routes had Shield gaps. All fixed and covered by tests
- Invalid Prune keys rejected before any upstream LLM call
- Monthly plan limits fail closed in production on database errors
- Constant-time API key hash verification
- Provider keys excluded from logs and error responses
All identified Shield gaps were closed before the first users were onboarded. Report vulnerabilities to hello@withprune.com.