Secrets Backend Abstraction
- Status: Accepted
- Date: 2026-04-03
Context and Problem Statement
NAAS v2.0 introduces several features that require cryptographic secrets: API key signing/validation (#101), credential encryption at rest in Redis (#286), and webhook HMAC signing (#277). These secrets (encryption keys, signing keys) need to be stored securely and retrieved at runtime.
Currently, NAAS reads all configuration from environment variables. This works for simple deployments but is insufficient for production environments where secrets should be managed by dedicated systems like HashiCorp Vault or AWS Secrets Manager.
How should NAAS load application secrets in a way that works for both simple Docker Compose deployments and production environments with dedicated secret stores?
Decision Drivers
- NAAS must remain easy to deploy with Docker Compose (no mandatory external dependencies)
- Production deployments need Vault/AWS Secrets Manager integration
- Multiple v2.0 features will consume secrets — the abstraction must exist before they're built
- The solution should be simple enough that adding a new backend is trivial
Considered Options
- Option 1: Environment variables only (status quo)
- Option 2: Pluggable
SecretsBackendprotocol with env default - Option 3: Adopt an existing library (keyring, cloudsecretmanager, etc.)
Decision Outcome
Chosen option: Option 2 — Pluggable SecretsBackend protocol, because it keeps the default deployment simple while enabling production secret stores without refactoring consuming code.
Consequences
- Good: Zero new dependencies for the default (env) backend
- Good: Consuming code is backend-agnostic — no refactoring when switching backends
- Good: Adding a new backend is ~20 lines implementing a Protocol
- Bad: Vault and AWS backends add optional dependencies (
hvac,boto3) - Bad: Slightly more code than raw
os.environcalls (but centralizes secret access)
Pros and Cons of the Options
Option 1: Environment variables only
- Good: Zero complexity, already works
- Bad: No path to Vault/AWS SM without refactoring every call site
- Bad: Secrets in env vars are visible in
/proc,docker inspect, CI logs - Bad: No rotation support — requires container restart
Option 2: Pluggable SecretsBackend protocol
- Good: Default backend reads env vars — no behavior change for existing deployments
- Good: Single configuration point (
SECRETS_BACKEND=env|vault|aws) to switch backends - Good: Protocol is tiny — easy to test, easy to extend
- Good: Optional deps only installed when needed
- Bad: Thin abstraction layer to maintain
- Bad: Backend-specific configuration (Vault address, AWS region) adds config surface
Option 3: Existing library
- Good: No code to write
- Bad: No existing library provides env + Vault + AWS SM with a unified interface
- Bad:
keyringis desktop-oriented (macOS Keychain, GNOME Keyring) — wrong model for server apps - Bad:
cloudsecretmanagercovers Azure + GCP only - Bad: Adding a dependency for ~50 lines of code is poor trade-off
Design
Protocol
class SecretsBackend(Protocol):
def get_secret(self, name: str) -> str:
"""Retrieve a secret by name. Raises KeyError if not found."""
...
Backends
EnvSecretsBackend (default, zero deps):
- Reads from
os.environ - Optionally reads from a mounted file path (
SECRETS_FILE_PATH) - Configuration: none required
VaultSecretsBackend (optional, requires hvac):
- Reads from HashiCorp Vault KV v2 engine
- Configuration:
VAULT_ADDR,VAULT_TOKEN(or other Vault auth methods),VAULT_SECRETS_PATH
AWSSecretsBackend (optional, requires boto3):
- Reads from AWS Secrets Manager
- Configuration:
AWS_REGION,AWS_SECRETS_PREFIX - Uses default credential chain (IAM role, env vars, etc.)
Configuration
# Default — read secrets from environment variables
SECRETS_BACKEND=env
# HashiCorp Vault
SECRETS_BACKEND=vault
VAULT_ADDR=https://vault.example.com:8200
VAULT_TOKEN=s.xxxxxxxx
VAULT_SECRETS_PATH=secret/data/naas
# AWS Secrets Manager
SECRETS_BACKEND=aws
AWS_REGION=us-east-1
AWS_SECRETS_PREFIX=naas/
Consuming Code
from naas.library.secrets import get_secrets_backend
secrets = get_secrets_backend()
encryption_key = secrets.get_secret("NAAS_ENCRYPTION_KEY")
Consumers never know which backend is active. Switching backends is a config change, not a code change.
Initialization
A get_secrets_backend() factory reads SECRETS_BACKEND and returns the appropriate implementation. Called once at app startup, stored on app.config["secrets"] for Flask request context access.
Scope for v2.0
- Ship
EnvSecretsBackendas the default - Ship
VaultSecretsBackendas optional (documentpip install naas[vault]) - Defer
AWSSecretsBackendto v2.1 unless there's demand - All v2.0 features (#101, #286, #277) consume secrets through this interface