Skip to main content

Secrets Management

Qarion supports pluggable secrets backends for secure storage of application secrets. By default, secrets are read from environment variables (.env file), but production deployments can use HashiCorp Vault, AWS Secrets Manager, or GCP Secret Manager.

Quick Start

Set the SECRETS_BACKEND environment variable to select a backend:

# Default — reads from environment variables
SECRETS_BACKEND=env

# HashiCorp Vault KV v2
SECRETS_BACKEND=vault

# AWS Secrets Manager
SECRETS_BACKEND=aws_sm

# Google Cloud Secret Manager
SECRETS_BACKEND=gcp_sm

Backend Configuration

Environment Variables (default)

No additional configuration needed. All secrets are read from os.environ:

SECRETS_BACKEND=env
SECRET_KEY=your-jwt-signing-key
POSTGRES_PASSWORD=your-db-password
INSTANCE_ENCRYPTION_KEY=your-fernet-key

HashiCorp Vault

Reads from a Vault KV v2 engine. Requires the hvac package:

pip install hvac
SECRETS_BACKEND=vault
VAULT_ADDR=https://vault.example.com
VAULT_TOKEN=hvs.your-vault-token
VAULT_SECRET_PATH=secret/data/qarion
SECRETS_CACHE_TTL=300 # Cache TTL in seconds (default: 300)

The secret at VAULT_SECRET_PATH should be a JSON object:

{
"SECRET_KEY": "your-jwt-key",
"POSTGRES_PASSWORD": "db-password",
"INSTANCE_ENCRYPTION_KEY": "fernet-key"
}

AWS Secrets Manager

Reads from an AWS Secrets Manager secret. Requires boto3:

pip install boto3
SECRETS_BACKEND=aws_sm
AWS_SECRET_ARN=arn:aws:secretsmanager:eu-west-1:123456789:secret:qarion-prod
AWS_REGION=eu-west-1 # optional, falls back to boto3 defaults
SECRETS_CACHE_TTL=300

The secret should contain a JSON string with key-value pairs.

Google Cloud Secret Manager

Reads from a GCP Secret Manager secret. Requires google-cloud-secret-manager:

pip install google-cloud-secret-manager
SECRETS_BACKEND=gcp_sm
GCP_SECRET_PROJECT=my-gcp-project
GCP_SECRET_NAME=qarion-secrets # default
GCP_SECRET_VERSION=latest # default
SECRETS_CACHE_TTL=300

Managed Secret Fields

The following settings are resolved from the secrets backend:

FieldPurpose
SECRET_KEYJWT signing key
SECRET_KEY_PREVIOUSPrevious JWT key (dual-key rotation)
POSTGRES_PASSWORDDatabase password
INSTANCE_ENCRYPTION_KEYFernet key for field-level encryption
CONTROL_PLANE_SECRET_KEYMulti-instance control plane key
SENDGRID_API_KEYSendGrid email API key
SCIM_BEARER_TOKENSCIM provisioning token
SMTP_PASSWORDSMTP email password
STORAGE_S3_SECRET_KEYS3 storage secret key
STORAGE_GCS_CREDENTIALS_JSONGCS credentials
STORAGE_AZURE_CONNECTION_STRINGAzure storage connection string

Key Rotation

JWT Signing Key

Qarion supports dual-key JWT rotation for zero-downtime key changes:

  1. Set SECRET_KEY_PREVIOUS to the current SECRET_KEY value.
  2. Update SECRET_KEY to the new key.
  3. Deploy — both old and new tokens will be accepted.
  4. After the token TTL expires (default 30 min), remove SECRET_KEY_PREVIOUS.

Encryption Key Rotation

To rotate INSTANCE_ENCRYPTION_KEY, use the built-in migration utility:

import asyncio
from app.core.secrets.key_rotation import rotate_encryption_key

asyncio.run(rotate_encryption_key(
old_key="<current-key>",
new_key="<new-key>",
))

This re-encrypts all Fernet-encrypted data (instance DB URLs and export config credentials) in a single database transaction.

Log Scrubbing

All secret values are automatically scrubbed from application logs. Any log event containing a known secret value will have it replaced with ***.

Architecture

┌──────────────────────┐
│ SecretProvider │ ← Strategy pattern
│ (abstract base) │
└──────────┬───────────┘

┌──────┼──────┬─────────────┐
▼ ▼ ▼ ▼
.env Vault AWS SM GCP SM

The SECRETS_BACKEND setting selects which provider is used. Only one backend is active at a time. The selected provider's SDK is loaded lazily, so unused providers don't add import overhead.