Skip to content

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:

  1. First layer: AES-256 encryption with tenant-specific derived key
  2. Second layer: Additional encryption with tenant-secrets-specific key
Secret Value → Encrypt(TenantKey) → Encrypt(TenantSecretsKey) → Database

Key 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:

json
{
  "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)

json
{
  "agent": {
    "llm": {
      "apiKey": "{{env.ANTHROPIC_API_KEY}}"
    }
  }
}

Tenant Override

json
{
  "agent": {
    "llm": {
      "apiKey": "{{secret:my_anthropic_key}}"
    }
  }
}

The tenant's stored secret takes precedence when their config is loaded.

API Endpoints

Store a Secret

bash
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:

json
{
  "success": true,
  "key": "openai_key"
}

List Secrets

Returns metadata only, never values:

bash
GET /tenant/secrets
Authorization: Bearer <token>

Response:

json
{
  "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):

bash
DELETE /tenant/secrets/openai_key
Authorization: Bearer <token>

Rotate a Secret

Replace with new value while keeping old one valid during grace period:

bash
POST /tenant/secrets/openai_key/rotate
Authorization: Bearer <token>
Content-Type: application/json

{
  "newValue": "sk-proj-new-key",
  "gracePeriodDays": 7
}

Response:

json
{
  "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

typescript
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 &#123;&#123;secret:*&#125;&#125; references:

bash
POST /tenant/config

Response includes detected references:

json
{
  "version": 5,
  "secretsDetected": ["openai_key", "stripe_secret"],
  "message": "Config saved. Ensure secrets are stored."
}

Audit Trail

All secret operations are logged:

ActionDescription
createNew secret stored
updateExisting secret value changed
accessSecret value retrieved (internal)
deleteSecret soft-deleted
rotateSecret rotated with grace period
expireSecret expired

Access logs include timestamp, action, and user ID.

Expiration

Secrets can have optional expiration dates:

json
{
  "key": "temp_api_key",
  "value": "xxx",
  "expiresAt": "2024-06-30T00:00:00Z"
}

Expired secrets return null at runtime with a warning log.

Best Practices

  1. Use descriptive key names: stripe_live_key not key1
  2. Add metadata: Include provider, type, and description
  3. Set expiration: For temporary credentials
  4. Rotate regularly: Use the rotation API with grace periods
  5. Monitor access: Review access counts for unusual patterns
  6. Avoid hardcoding: Never put actual values in config, use references

Error Handling

Secret Not Found

If a &#123;&#123;secret:key&#125;&#125; reference can't be resolved:

[warn] Secret reference 'missing_key' not found for tenant abc-123

The placeholder remains as-is. Integrations may fail with auth errors.

Expired Secret

[warn] Secret 'old_key' for tenant abc-123 has expired

Returns null, integration will likely fail.

Plan Tier Limits

FeatureFreeProEnterprise
Max secrets550Unlimited
RotationNoYesYes
Audit retention7 days90 daysUnlimited

Released under the ISC License.