Agent Studio
Adr

ADR-005: BYOK Encryption

Secure storage for tenant-provided API keys

ADR-005: BYOK Encryption

Status

Accepted

Context

Agent Studio supports BYOK (Bring Your Own Keys) - tenants provide their own API keys for providers like Deepgram, OpenAI, etc. These keys must be:

  • Encrypted at rest in the database
  • Decrypted only at runtime when needed
  • Rotatable without service interruption
  • Never logged or exposed in error messages

Options Considered

  1. Vault/Secrets Manager - External service (AWS Secrets Manager, HashiCorp Vault)
  2. Application-level encryption - Encrypt in Python before storing
  3. Database-level encryption (TDE) - PostgreSQL native encryption
  4. KMS envelope encryption - Use cloud KMS for key management

Decision

We will use application-level encryption with Fernet (symmetric encryption):

from cryptography.fernet import Fernet
from pydantic import SecretStr

class ProviderKeyService:
    def __init__(self, encryption_key: SecretStr):
        self.fernet = Fernet(encryption_key.get_secret_value().encode())
    
    def encrypt(self, api_key: str) -> str:
        """Encrypt API key for storage."""
        return self.fernet.encrypt(api_key.encode()).decode()
    
    def decrypt(self, encrypted_key: str) -> str:
        """Decrypt API key for use."""
        return self.fernet.decrypt(encrypted_key.encode()).decode()

Key Management

# settings.py
class Settings(BaseSettings):
    encryption_key: SecretStr  # Fernet key from environment
    
    # For key rotation, support multiple keys
    encryption_key_previous: SecretStr | None = None

Database Schema

class ProviderKey(Base):
    __tablename__ = "provider_keys"
    
    id: Mapped[UUID]
    tenant_id: Mapped[UUID]
    provider_type: Mapped[str]      # stt, tts, llm
    provider_name: Mapped[str]      # deepgram, openai
    api_key_encrypted: Mapped[str]  # Fernet encrypted
    key_version: Mapped[int]        # Encryption key version
    is_default: Mapped[bool]

Key Rotation Strategy

  1. Generate new Fernet key
  2. Set as encryption_key, move old to encryption_key_previous
  3. Background job re-encrypts all keys with new key
  4. Remove encryption_key_previous after migration

Security Considerations

  • Encryption key stored in environment (not database)
  • Keys decrypted only when creating provider instance
  • Decrypted keys never logged
  • API responses never include decrypted keys
# Safe logging
logger.info("Using provider", provider=provider_name, tenant=tenant_id)
# Never: logger.info("API key", key=api_key)

Consequences

Positive

  • Simple implementation with standard library
  • No external service dependencies
  • Keys protected at rest
  • Easy key rotation

Negative

  • Encryption key in environment is a single point of failure
  • Not hardware-backed (vs HSM/KMS)
  • Application has access to all decrypted keys

Future Considerations

  • Could migrate to AWS KMS or HashiCorp Vault for enterprise tier
  • Envelope encryption for additional security layer

On this page