Tenant Secrets
Secure storage and management of tenant-specific credentials and API keys.
Overview
Tenant secrets allow each tenant to securely store their own API keys and credentials, completely isolated from other tenants. Secrets can override base configuration values at runtime.
Security Architecture
Encryption
All secrets are protected with double encryption:
- First layer: AES-256 encryption with tenant-specific derived key
- Second layer: Additional encryption with tenant-secrets-specific key
Secret Value → Encrypt(TenantKey) → Encrypt(TenantSecretsKey) → DatabaseKey Derivation
Each tenant gets a unique encryption key derived via HKDF:
DerivedKey = HKDF-SHA256(
MasterKey,
Salt: "tenant:{tenantId}",
Info: "backflow-tenant-config",
Length: 32 bytes
)This ensures:
- Complete tenant isolation
- No shared encryption keys between tenants
- Database compromise doesn't expose secrets across tenants
Write-Only API
Secret values are never returned via API endpoints. You can:
- Store secrets (write)
- List secret metadata (keys, timestamps, access counts)
- Delete secrets
- Rotate secrets
You cannot retrieve the actual secret value via API.
Secret References
Use {{secret:key_name}} syntax to reference secrets in your configuration:
{
"routes": [{
"path": "/ai/generate",
"method": "post",
"integrations": [{
"type": "llm",
"config": {
"provider": "openai",
"apiKey": "{{secret:openai_key}}"
}
}]
}]
}At runtime, {{secret:openai_key}} is replaced with the actual stored value.
Overriding Base Configuration
Secrets can override any configuration value, including those set via environment variables:
Base Config (config.json)
{
"agent": {
"llm": {
"apiKey": "{{env.ANTHROPIC_API_KEY}}"
}
}
}Tenant Override
{
"agent": {
"llm": {
"apiKey": "{{secret:my_anthropic_key}}"
}
}
}The tenant's stored secret takes precedence when their config is loaded.
API Endpoints
Store a Secret
POST /tenant/secrets
Authorization: Bearer <token>
Content-Type: application/json
{
"key": "openai_key",
"value": "sk-proj-xxxxx",
"metadata": {
"type": "api_key",
"provider": "openai",
"description": "Production OpenAI key"
},
"expiresAt": "2025-12-31T23:59:59Z"
}Response:
{
"success": true,
"key": "openai_key"
}List Secrets
Returns metadata only, never values:
GET /tenant/secrets
Authorization: Bearer <token>Response:
{
"secrets": [
{
"key": "openai_key",
"createdAt": "2024-01-15T10:30:00Z",
"lastAccessedAt": "2024-01-20T14:22:00Z",
"accessCount": 47,
"expiresAt": "2025-12-31T23:59:59Z",
"metadata": {
"type": "api_key",
"provider": "openai"
}
}
]
}Delete a Secret
Soft delete (marked inactive):
DELETE /tenant/secrets/openai_key
Authorization: Bearer <token>Rotate a Secret
Replace with new value while keeping old one valid during grace period:
POST /tenant/secrets/openai_key/rotate
Authorization: Bearer <token>
Content-Type: application/json
{
"newValue": "sk-proj-new-key",
"gracePeriodDays": 7
}Response:
{
"success": true,
"oldKeyValidUntil": "2024-01-27T10:30:00Z"
}During the grace period, both old and new keys work. After expiration, only the new key is valid.
SDK Usage
import { Backflow } from '@backflow/sdk';
const client = new Backflow({ token: 'your-jwt-token' });
// Store a secret
await client.secrets.create({
key: 'stripe_key',
value: 'sk_live_xxx',
metadata: { provider: 'stripe' }
});
// List secrets (metadata only)
const secrets = await client.secrets.list();
// Rotate a secret
await client.secrets.rotate('stripe_key', {
newValue: 'sk_live_new',
gracePeriodDays: 14
});
// Delete a secret
await client.secrets.delete('stripe_key');Secret Detection
When saving tenant config, Backflow automatically detects {{secret:*}} references:
POST /tenant/configResponse includes detected references:
{
"version": 5,
"secretsDetected": ["openai_key", "stripe_secret"],
"message": "Config saved. Ensure secrets are stored."
}Audit Trail
All secret operations are logged:
| Action | Description |
|---|---|
create | New secret stored |
update | Existing secret value changed |
access | Secret value retrieved (internal) |
delete | Secret soft-deleted |
rotate | Secret rotated with grace period |
expire | Secret expired |
Access logs include timestamp, action, and user ID.
Expiration
Secrets can have optional expiration dates:
{
"key": "temp_api_key",
"value": "xxx",
"expiresAt": "2024-06-30T00:00:00Z"
}Expired secrets return null at runtime with a warning log.
Best Practices
- Use descriptive key names:
stripe_live_keynotkey1 - Add metadata: Include provider, type, and description
- Set expiration: For temporary credentials
- Rotate regularly: Use the rotation API with grace periods
- Monitor access: Review access counts for unusual patterns
- Avoid hardcoding: Never put actual values in config, use references
Error Handling
Secret Not Found
If a {{secret:key}} reference can't be resolved:
[warn] Secret reference 'missing_key' not found for tenant abc-123The placeholder remains as-is. Integrations may fail with auth errors.
Expired Secret
[warn] Secret 'old_key' for tenant abc-123 has expiredReturns null, integration will likely fail.
Plan Tier Limits
| Feature | Free | Pro | Enterprise |
|---|---|---|---|
| Max secrets | 5 | 50 | Unlimited |
| Rotation | No | Yes | Yes |
| Audit retention | 7 days | 90 days | Unlimited |