Spring Boot-style configuration for Python applications
SprigConfig provides secure handling of sensitive configuration values through encrypted secrets with lazy decryption. This guide covers key management, encryption workflow, and security best practices.
SprigConfig uses Fernet symmetric encryption from the cryptography library. Sensitive values are stored as ENC(...) in configuration files and decrypted only when explicitly accessed.
Key security principles:
.get() is calledEncrypted values use the ENC() wrapper:
database:
username: admin
password: ENC(gAAAAABl_example_ciphertext_here...)
api:
key: ENC(gAAAAABl_another_encrypted_value...)
SprigConfig automatically detects ENC() values and wraps them as LazySecret objects.
Use the cryptography library to generate a Fernet key:
from cryptography.fernet import Fernet
print(Fernet.generate_key().decode())
Output looks like:
ZmDfcTF7_60GrrY167zsiPd67pEvs0aGOv2oasOM1Pg=
Generate separate keys for each environment:
| Environment | Key Variable |
|---|---|
| Development | DEV_SECRET_KEY or shared .env |
| Test | TEST_SECRET_KEY or in-memory |
| Production | APP_SECRET_KEY via secret manager |
Never commit keys to version control.
Store keys in:
.env files — Excluded from Git via .gitignoreSprigConfig provides multiple ways to configure the encryption key:
export APP_SECRET_KEY="your-fernet-key-here"
SprigConfig checks APP_SECRET_KEY automatically when decrypting.
from sprigconfig.lazy_secret import ensure_key_from_env
# Loads APP_SECRET_KEY and validates it
ensure_key_from_env("APP_SECRET_KEY")
from sprigconfig.lazy_secret import set_global_key
set_global_key("your-fernet-key-here")
For key rotation or vault integration:
from sprigconfig.lazy_secret import set_key_provider
def get_key_from_vault():
# Fetch from secret manager
return vault_client.get_secret("app-secret-key")
set_key_provider(get_key_from_vault)
When decrypting, SprigConfig checks for keys in this order:
LazySecretset_global_key()set_key_provider()APP_SECRET_KEYCreate a helper script for encrypting values:
# encrypt_value.py
import os
import sys
from cryptography.fernet import Fernet
key = os.environ.get("APP_SECRET_KEY")
if not key:
print("Set APP_SECRET_KEY first")
sys.exit(1)
value = sys.argv[1]
f = Fernet(key.encode())
encrypted = f.encrypt(value.encode()).decode()
print(f"ENC({encrypted})")
Usage:
export APP_SECRET_KEY="your-key"
python encrypt_value.py "my-secret-password"
# Output: ENC(gAAAAABl...)
Then add to your configuration:
database:
password: ENC(gAAAAABl...)
Encrypted values become LazySecret objects:
cfg = load_config(profile="prod")
# Returns LazySecret, not the plaintext
secret = cfg["database"]["password"]
print(type(secret)) # <class 'sprigconfig.lazy_secret.LazySecret'>
# Decrypt only when needed
plaintext = secret.get()
Decryption happens only when .get() is called:
ConfigLoadError.get() call decrypts (no caching of plaintext)For sensitive applications, use .zeroize() for best-effort memory cleanup:
secret = cfg["database"]["password"]
password = secret.get()
# Use password...
secret.zeroize() # Best-effort cleanup
Note: Python’s garbage collection makes guaranteed memory cleanup impossible.
Secrets are redacted in dumps and serialization:
cfg = load_config(profile="prod")
# Redacted output
print(cfg.to_dict())
# {'database': {'password': '<LazySecret>'}}
print(cfg.dump())
# database:
# password: <LazySecret>
For debugging, you can reveal secrets:
# In code (unsafe!)
data = cfg.to_dict(reveal_secrets=True)
yaml_str = cfg.dump(safe=False)
# Via CLI (unsafe!)
sprigconfig dump --config-dir=config --profile=prod --secrets
Warning: Only use reveal options in secure, local environments. Never in logs or CI output.
from cryptography.fernet import Fernet
new_key = Fernet.generate_key().decode()
old_f = Fernet(old_key)
new_f = Fernet(new_key)
# For each secret
plaintext = old_f.decrypt(old_ciphertext)
new_ciphertext = new_f.encrypt(plaintext)
Deploy updated configs + K2 together
Use a custom key provider for gradual rotation:
def dual_key_provider():
k1 = os.getenv("APP_SECRET_KEY_OLD")
k2 = os.getenv("APP_SECRET_KEY")
# Try new key first, fall back to old
return k2 or k1
set_key_provider(dual_key_provider)
# GitLab CI example
deploy:
script:
- pip install sprig-config
# Key is injected as protected variable
- sprigconfig dump --config-dir=config --profile=prod # Redacted
variables:
APP_SECRET_KEY: $PROD_SECRET_KEY # Protected CI variable
APP_SECRET_KEY is a protected/masked CI variable--secrets flag in CI logs| Error | Cause |
|---|---|
No Fernet key available |
Key not set before accessing secret |
Invalid Fernet key |
Key is malformed or wrong length |
InvalidToken |
Wrong key or corrupted ciphertext |
ConfigLoadError |
Wraps cryptography errors |
from sprigconfig import load_config, ConfigLoadError
try:
cfg = load_config(profile="prod")
password = cfg["database"]["password"].get()
except ConfigLoadError as e:
if "Fernet key" in str(e):
print("Missing or invalid encryption key")
elif "InvalidToken" in str(e):
print("Wrong key or corrupted secret")
else:
print(f"Config error: {e}")
ENC() wrapper.gitignore excludes .env filesreveal_secrets only used locally# Check for unencrypted sensitive values
grep -rn "password:" config/ | grep -v "ENC("
grep -rn "secret:" config/ | grep -v "ENC("
grep -rn "token:" config/ | grep -v "ENC("
# Verify all ENC values are present
grep -rn "ENC(" config/