Python Secrets Management Best Practices¶
Objective: Master senior-level Python secrets management patterns for production systems. When you need to implement secure credential handling, when you want to build robust secret rotation, when you need enterprise-grade secrets managementโthese best practices become your weapon of choice.
Core Principles¶
- Never Store Secrets in Code: Use external secret stores
- Encrypt at Rest and in Transit: Protect secrets with encryption
- Rotate Regularly: Implement automatic secret rotation
- Least Privilege: Grant minimum necessary access
- Audit Everything: Log all secret access and changes
Secret Storage Strategies¶
Environment-Based Secrets¶
# python/01-secret-storage.py
"""
Secret storage patterns and credential management
"""
from typing import List, Dict, Any, Optional, Union, Tuple
from dataclasses import dataclass, asdict
from enum import Enum
import os
import json
import base64
import hashlib
import secrets
import time
from datetime import datetime, timedelta
import logging
from pathlib import Path
import yaml
import toml
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import keyring
import hvac
import boto3
from azure.keyvault.secrets import SecretClient
from azure.identity import DefaultAzureCredential
import google.cloud.secretmanager as secretmanager
from google.cloud import secretmanager_v1
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class SecretType(Enum):
"""Secret type enumeration"""
PASSWORD = "password"
API_KEY = "api_key"
TOKEN = "token"
CERTIFICATE = "certificate"
PRIVATE_KEY = "private_key"
DATABASE_URL = "database_url"
ENCRYPTION_KEY = "encryption_key"
class SecretSource(Enum):
"""Secret source enumeration"""
ENVIRONMENT = "environment"
FILE = "file"
VAULT = "vault"
CLOUD = "cloud"
KEYRING = "keyring"
@dataclass
class Secret:
"""Secret definition"""
name: str
value: str
secret_type: SecretType
source: SecretSource
created_at: datetime
expires_at: Optional[datetime] = None
tags: Dict[str, str] = None
version: str = "1"
def __post_init__(self):
if self.tags is None:
self.tags = {}
if self.created_at is None:
self.created_at = datetime.utcnow()
class EnvironmentSecretManager:
"""Environment variable secret manager"""
def __init__(self, prefix: str = "SECRET_"):
self.prefix = prefix
self.secrets = {}
self.load_secrets()
def load_secrets(self) -> None:
"""Load secrets from environment variables"""
for key, value in os.environ.items():
if key.startswith(self.prefix):
secret_name = key[len(self.prefix):].lower()
self.secrets[secret_name] = value
logger.debug(f"Loaded secret from environment: {secret_name}")
def get_secret(self, name: str, default: Optional[str] = None) -> Optional[str]:
"""Get secret value"""
return self.secrets.get(name, default)
def set_secret(self, name: str, value: str) -> None:
"""Set secret value (for testing only)"""
self.secrets[name] = value
os.environ[f"{self.prefix}{name.upper()}"] = value
def list_secrets(self) -> List[str]:
"""List all secret names"""
return list(self.secrets.keys())
def has_secret(self, name: str) -> bool:
"""Check if secret exists"""
return name in self.secrets
class FileSecretManager:
"""File-based secret manager"""
def __init__(self, secrets_file: str, encrypted: bool = True):
self.secrets_file = Path(secrets_file)
self.encrypted = encrypted
self.encryption_key = None
self.secrets = {}
if encrypted:
self.encryption_key = self._get_or_create_encryption_key()
self.load_secrets()
def _get_or_create_encryption_key(self) -> bytes:
"""Get or create encryption key"""
key_file = self.secrets_file.parent / ".secret_key"
if key_file.exists():
with open(key_file, 'rb') as f:
return f.read()
else:
key = Fernet.generate_key()
with open(key_file, 'wb') as f:
f.write(key)
# Set restrictive permissions
key_file.chmod(0o600)
return key
def _encrypt_value(self, value: str) -> str:
"""Encrypt secret value"""
if not self.encrypted:
return value
fernet = Fernet(self.encryption_key)
encrypted_value = fernet.encrypt(value.encode())
return base64.b64encode(encrypted_value).decode()
def _decrypt_value(self, encrypted_value: str) -> str:
"""Decrypt secret value"""
if not self.encrypted:
return encrypted_value
try:
encrypted_bytes = base64.b64decode(encrypted_value.encode())
fernet = Fernet(self.encryption_key)
decrypted_value = fernet.decrypt(encrypted_bytes)
return decrypted_value.decode()
except Exception as e:
logger.error(f"Failed to decrypt secret: {e}")
raise
def load_secrets(self) -> None:
"""Load secrets from file"""
if not self.secrets_file.exists():
logger.warning(f"Secrets file not found: {self.secrets_file}")
return
try:
with open(self.secrets_file, 'r') as f:
if self.secrets_file.suffix == '.json':
data = json.load(f)
elif self.secrets_file.suffix == '.yaml' or self.secrets_file.suffix == '.yml':
data = yaml.safe_load(f)
elif self.secrets_file.suffix == '.toml':
data = toml.load(f)
else:
# Assume plain text format
data = {}
for line in f:
if '=' in line and not line.strip().startswith('#'):
key, value = line.strip().split('=', 1)
data[key] = value
for name, value in data.items():
if self.encrypted:
decrypted_value = self._decrypt_value(value)
else:
decrypted_value = value
self.secrets[name] = decrypted_value
logger.debug(f"Loaded secret from file: {name}")
except Exception as e:
logger.error(f"Failed to load secrets from file: {e}")
raise
def save_secrets(self) -> None:
"""Save secrets to file"""
try:
data = {}
for name, value in self.secrets.items():
if self.encrypted:
encrypted_value = self._encrypt_value(value)
else:
encrypted_value = value
data[name] = encrypted_value
with open(self.secrets_file, 'w') as f:
if self.secrets_file.suffix == '.json':
json.dump(data, f, indent=2)
elif self.secrets_file.suffix == '.yaml' or self.secrets_file.suffix == '.yml':
yaml.dump(data, f, default_flow_style=False)
elif self.secrets_file.suffix == '.toml':
toml.dump(data, f)
else:
# Plain text format
for name, value in data.items():
f.write(f"{name}={value}\n")
# Set restrictive permissions
self.secrets_file.chmod(0o600)
logger.info(f"Secrets saved to file: {self.secrets_file}")
except Exception as e:
logger.error(f"Failed to save secrets to file: {e}")
raise
def get_secret(self, name: str, default: Optional[str] = None) -> Optional[str]:
"""Get secret value"""
return self.secrets.get(name, default)
def set_secret(self, name: str, value: str) -> None:
"""Set secret value"""
self.secrets[name] = value
self.save_secrets()
def delete_secret(self, name: str) -> bool:
"""Delete secret"""
if name in self.secrets:
del self.secrets[name]
self.save_secrets()
return True
return False
def list_secrets(self) -> List[str]:
"""List all secret names"""
return list(self.secrets.keys())
class KeyringSecretManager:
"""Keyring-based secret manager"""
def __init__(self, service_name: str):
self.service_name = service_name
def get_secret(self, name: str, default: Optional[str] = None) -> Optional[str]:
"""Get secret from keyring"""
try:
return keyring.get_password(self.service_name, name)
except Exception as e:
logger.error(f"Failed to get secret from keyring: {e}")
return default
def set_secret(self, name: str, value: str) -> None:
"""Set secret in keyring"""
try:
keyring.set_password(self.service_name, name, value)
logger.info(f"Secret set in keyring: {name}")
except Exception as e:
logger.error(f"Failed to set secret in keyring: {e}")
raise
def delete_secret(self, name: str) -> bool:
"""Delete secret from keyring"""
try:
keyring.delete_password(self.service_name, name)
logger.info(f"Secret deleted from keyring: {name}")
return True
except Exception as e:
logger.error(f"Failed to delete secret from keyring: {e}")
return False
def list_secrets(self) -> List[str]:
"""List secrets (keyring doesn't support listing)"""
logger.warning("Keyring doesn't support listing secrets")
return []
class VaultSecretManager:
"""HashiCorp Vault secret manager"""
def __init__(self, vault_url: str, token: str, mount_point: str = "secret"):
self.vault_url = vault_url
self.token = token
self.mount_point = mount_point
self.client = hvac.Client(url=vault_url, token=token)
self.client.is_authenticated()
def get_secret(self, path: str, default: Optional[str] = None) -> Optional[str]:
"""Get secret from Vault"""
try:
response = self.client.secrets.kv.v2.read_secret_version(
path=path, mount_point=self.mount_point
)
return response['data']['data'].get('value')
except Exception as e:
logger.error(f"Failed to get secret from Vault: {e}")
return default
def set_secret(self, path: str, value: str, metadata: Optional[Dict[str, str]] = None) -> None:
"""Set secret in Vault"""
try:
secret_data = {'value': value}
if metadata:
secret_data.update(metadata)
self.client.secrets.kv.v2.create_or_update_secret(
path=path, secret=secret_data, mount_point=self.mount_point
)
logger.info(f"Secret set in Vault: {path}")
except Exception as e:
logger.error(f"Failed to set secret in Vault: {e}")
raise
def delete_secret(self, path: str) -> bool:
"""Delete secret from Vault"""
try:
self.client.secrets.kv.v2.delete_metadata_and_all_versions(
path=path, mount_point=self.mount_point
)
logger.info(f"Secret deleted from Vault: {path}")
return True
except Exception as e:
logger.error(f"Failed to delete secret from Vault: {e}")
return False
def list_secrets(self, path: str = "") -> List[str]:
"""List secrets in Vault"""
try:
response = self.client.secrets.kv.v2.list_secrets(
path=path, mount_point=self.mount_point
)
return response['data']['keys']
except Exception as e:
logger.error(f"Failed to list secrets from Vault: {e}")
return []
class AWSSecretsManager:
"""AWS Secrets Manager client"""
def __init__(self, region_name: str = "us-east-1"):
self.region_name = region_name
self.client = boto3.client('secretsmanager', region_name=region_name)
def get_secret(self, secret_name: str, default: Optional[str] = None) -> Optional[str]:
"""Get secret from AWS Secrets Manager"""
try:
response = self.client.get_secret_value(SecretId=secret_name)
return response['SecretString']
except Exception as e:
logger.error(f"Failed to get secret from AWS: {e}")
return default
def set_secret(self, secret_name: str, value: str, description: str = "") -> None:
"""Set secret in AWS Secrets Manager"""
try:
self.client.create_secret(
Name=secret_name,
SecretString=value,
Description=description
)
logger.info(f"Secret set in AWS: {secret_name}")
except self.client.exceptions.ResourceExistsException:
# Update existing secret
self.client.update_secret(
SecretId=secret_name,
SecretString=value
)
logger.info(f"Secret updated in AWS: {secret_name}")
except Exception as e:
logger.error(f"Failed to set secret in AWS: {e}")
raise
def delete_secret(self, secret_name: str) -> bool:
"""Delete secret from AWS Secrets Manager"""
try:
self.client.delete_secret(
SecretId=secret_name,
ForceDeleteWithoutRecovery=True
)
logger.info(f"Secret deleted from AWS: {secret_name}")
return True
except Exception as e:
logger.error(f"Failed to delete secret from AWS: {e}")
return False
def list_secrets(self) -> List[str]:
"""List secrets in AWS Secrets Manager"""
try:
response = self.client.list_secrets()
return [secret['Name'] for secret in response['SecretList']]
except Exception as e:
logger.error(f"Failed to list secrets from AWS: {e}")
return []
class AzureKeyVaultManager:
"""Azure Key Vault secret manager"""
def __init__(self, vault_url: str):
self.vault_url = vault_url
self.credential = DefaultAzureCredential()
self.client = SecretClient(vault_url=vault_url, credential=self.credential)
def get_secret(self, name: str, default: Optional[str] = None) -> Optional[str]:
"""Get secret from Azure Key Vault"""
try:
secret = self.client.get_secret(name)
return secret.value
except Exception as e:
logger.error(f"Failed to get secret from Azure: {e}")
return default
def set_secret(self, name: str, value: str) -> None:
"""Set secret in Azure Key Vault"""
try:
self.client.set_secret(name, value)
logger.info(f"Secret set in Azure: {name}")
except Exception as e:
logger.error(f"Failed to set secret in Azure: {e}")
raise
def delete_secret(self, name: str) -> bool:
"""Delete secret from Azure Key Vault"""
try:
self.client.begin_delete_secret(name)
logger.info(f"Secret deleted from Azure: {name}")
return True
except Exception as e:
logger.error(f"Failed to delete secret from Azure: {e}")
return False
def list_secrets(self) -> List[str]:
"""List secrets in Azure Key Vault"""
try:
secrets = []
for secret_properties in self.client.list_properties_of_secrets():
secrets.append(secret_properties.name)
return secrets
except Exception as e:
logger.error(f"Failed to list secrets from Azure: {e}")
return []
class GCPSecretManager:
"""Google Cloud Secret Manager client"""
def __init__(self, project_id: str):
self.project_id = project_id
self.client = secretmanager.SecretManagerServiceClient()
def get_secret(self, secret_id: str, version: str = "latest", default: Optional[str] = None) -> Optional[str]:
"""Get secret from GCP Secret Manager"""
try:
name = f"projects/{self.project_id}/secrets/{secret_id}/versions/{version}"
response = self.client.access_secret_version(request={"name": name})
return response.payload.data.decode("UTF-8")
except Exception as e:
logger.error(f"Failed to get secret from GCP: {e}")
return default
def set_secret(self, secret_id: str, value: str) -> None:
"""Set secret in GCP Secret Manager"""
try:
parent = f"projects/{self.project_id}"
# Create secret if it doesn't exist
try:
self.client.create_secret(
request={
"parent": parent,
"secret_id": secret_id,
"secret": {"replication": {"automatic": {}}},
}
)
except Exception:
# Secret might already exist
pass
# Add secret version
self.client.add_secret_version(
request={
"parent": f"{parent}/secrets/{secret_id}",
"payload": {"data": value.encode("UTF-8")},
}
)
logger.info(f"Secret set in GCP: {secret_id}")
except Exception as e:
logger.error(f"Failed to set secret in GCP: {e}")
raise
def delete_secret(self, secret_id: str) -> bool:
"""Delete secret from GCP Secret Manager"""
try:
name = f"projects/{self.project_id}/secrets/{secret_id}"
self.client.delete_secret(request={"name": name})
logger.info(f"Secret deleted from GCP: {secret_id}")
return True
except Exception as e:
logger.error(f"Failed to delete secret from GCP: {e}")
return False
def list_secrets(self) -> List[str]:
"""List secrets in GCP Secret Manager"""
try:
parent = f"projects/{self.project_id}"
secrets = []
for secret in self.client.list_secrets(request={"parent": parent}):
secret_id = secret.name.split("/")[-1]
secrets.append(secret_id)
return secrets
except Exception as e:
logger.error(f"Failed to list secrets from GCP: {e}")
return []
class UnifiedSecretManager:
"""Unified secret manager supporting multiple backends"""
def __init__(self, primary_backend: Any, fallback_backends: List[Any] = None):
self.primary_backend = primary_backend
self.fallback_backends = fallback_backends or []
self.all_backends = [primary_backend] + self.fallback_backends
def get_secret(self, name: str, default: Optional[str] = None) -> Optional[str]:
"""Get secret from primary backend with fallback"""
# Try primary backend first
value = self.primary_backend.get_secret(name)
if value is not None:
return value
# Try fallback backends
for backend in self.fallback_backends:
value = backend.get_secret(name)
if value is not None:
return value
return default
def set_secret(self, name: str, value: str) -> None:
"""Set secret in primary backend"""
self.primary_backend.set_secret(name, value)
def delete_secret(self, name: str) -> bool:
"""Delete secret from all backends"""
success = True
for backend in self.all_backends:
if hasattr(backend, 'delete_secret'):
if not backend.delete_secret(name):
success = False
return success
def list_secrets(self) -> List[str]:
"""List secrets from primary backend"""
return self.primary_backend.list_secrets()
# Usage examples
def example_secret_management():
"""Example secret management usage"""
# Environment-based secrets
env_manager = EnvironmentSecretManager("APP_")
env_secret = env_manager.get_secret("database_password")
print(f"Environment secret: {env_secret}")
# File-based secrets
file_manager = FileSecretManager("/tmp/secrets.json", encrypted=True)
file_manager.set_secret("api_key", "secret-api-key")
file_secret = file_manager.get_secret("api_key")
print(f"File secret: {file_secret}")
# Keyring secrets
keyring_manager = KeyringSecretManager("my-app")
keyring_manager.set_secret("database_url", "postgresql://user:pass@localhost/db")
keyring_secret = keyring_manager.get_secret("database_url")
print(f"Keyring secret: {keyring_secret}")
# AWS Secrets Manager
aws_manager = AWSSecretsManager("us-east-1")
aws_secret = aws_manager.get_secret("my-secret")
print(f"AWS secret: {aws_secret}")
# Unified secret manager
unified_manager = UnifiedSecretManager(
primary_backend=env_manager,
fallback_backends=[file_manager, keyring_manager]
)
unified_secret = unified_manager.get_secret("database_password")
print(f"Unified secret: {unified_secret}")
Secret Rotation¶
# python/02-secret-rotation.py
"""
Secret rotation patterns and automated credential management
"""
from typing import List, Dict, Any, Optional, Union, Tuple
import time
import threading
from datetime import datetime, timedelta
import logging
from dataclasses import dataclass
from enum import Enum
import asyncio
from concurrent.futures import ThreadPoolExecutor
import schedule
import croniter
logger = logging.getLogger(__name__)
class RotationStrategy(Enum):
"""Rotation strategy enumeration"""
TIME_BASED = "time_based"
USAGE_BASED = "usage_based"
MANUAL = "manual"
EVENT_BASED = "event_based"
@dataclass
class RotationPolicy:
"""Secret rotation policy"""
secret_name: str
strategy: RotationStrategy
interval_days: int = 30
max_usage_count: int = 1000
cron_schedule: str = "0 0 * * *" # Daily at midnight
auto_rotate: bool = True
notify_before_days: int = 7
class SecretRotator:
"""Secret rotation manager"""
def __init__(self, secret_manager: Any):
self.secret_manager = secret_manager
self.rotation_policies = {}
self.rotation_history = {}
self.usage_counts = {}
self.rotation_lock = threading.Lock()
self.is_running = False
self.rotation_thread = None
def add_rotation_policy(self, policy: RotationPolicy) -> None:
"""Add rotation policy for secret"""
self.rotation_policies[policy.secret_name] = policy
logger.info(f"Added rotation policy for secret: {policy.secret_name}")
def start_rotation_scheduler(self) -> None:
"""Start automatic rotation scheduler"""
if self.is_running:
logger.warning("Rotation scheduler is already running")
return
self.is_running = True
self.rotation_thread = threading.Thread(target=self._rotation_loop, daemon=True)
self.rotation_thread.start()
logger.info("Secret rotation scheduler started")
def stop_rotation_scheduler(self) -> None:
"""Stop automatic rotation scheduler"""
self.is_running = False
if self.rotation_thread:
self.rotation_thread.join()
logger.info("Secret rotation scheduler stopped")
def _rotation_loop(self) -> None:
"""Main rotation loop"""
while self.is_running:
try:
self._check_rotation_requirements()
time.sleep(60) # Check every minute
except Exception as e:
logger.error(f"Error in rotation loop: {e}")
time.sleep(60)
def _check_rotation_requirements(self) -> None:
"""Check if any secrets need rotation"""
for secret_name, policy in self.rotation_policies.items():
if not policy.auto_rotate:
continue
try:
if self._should_rotate_secret(secret_name, policy):
self._rotate_secret(secret_name, policy)
except Exception as e:
logger.error(f"Error checking rotation for {secret_name}: {e}")
def _should_rotate_secret(self, secret_name: str, policy: RotationPolicy) -> bool:
"""Check if secret should be rotated"""
if policy.strategy == RotationStrategy.TIME_BASED:
return self._check_time_based_rotation(secret_name, policy)
elif policy.strategy == RotationStrategy.USAGE_BASED:
return self._check_usage_based_rotation(secret_name, policy)
elif policy.strategy == RotationStrategy.EVENT_BASED:
return self._check_event_based_rotation(secret_name, policy)
return False
def _check_time_based_rotation(self, secret_name: str, policy: RotationPolicy) -> bool:
"""Check time-based rotation requirements"""
if secret_name not in self.rotation_history:
return True # First rotation
last_rotation = self.rotation_history[secret_name].get('last_rotation')
if not last_rotation:
return True
days_since_rotation = (datetime.utcnow() - last_rotation).days
return days_since_rotation >= policy.interval_days
def _check_usage_based_rotation(self, secret_name: str, policy: RotationPolicy) -> bool:
"""Check usage-based rotation requirements"""
usage_count = self.usage_counts.get(secret_name, 0)
return usage_count >= policy.max_usage_count
def _check_event_based_rotation(self, secret_name: str, policy: RotationPolicy) -> bool:
"""Check event-based rotation requirements"""
# This would be implemented based on specific events
# For example, security breach, compromised credentials, etc.
return False
def _rotate_secret(self, secret_name: str, policy: RotationPolicy) -> None:
"""Rotate secret"""
with self.rotation_lock:
try:
logger.info(f"Starting rotation for secret: {secret_name}")
# Generate new secret
new_secret = self._generate_new_secret(secret_name)
# Update secret in all backends
self._update_secret_in_backends(secret_name, new_secret)
# Update rotation history
self._update_rotation_history(secret_name, new_secret)
# Reset usage count
self.usage_counts[secret_name] = 0
# Notify about rotation
self._notify_rotation(secret_name, policy)
logger.info(f"Successfully rotated secret: {secret_name}")
except Exception as e:
logger.error(f"Failed to rotate secret {secret_name}: {e}")
raise
def _generate_new_secret(self, secret_name: str) -> str:
"""Generate new secret value"""
# This would be implemented based on secret type
# For example, generate new password, API key, etc.
return f"new_secret_{int(time.time())}"
def _update_secret_in_backends(self, secret_name: str, new_secret: str) -> None:
"""Update secret in all backends"""
if hasattr(self.secret_manager, 'set_secret'):
self.secret_manager.set_secret(secret_name, new_secret)
elif hasattr(self.secret_manager, 'all_backends'):
for backend in self.secret_manager.all_backends:
if hasattr(backend, 'set_secret'):
backend.set_secret(secret_name, new_secret)
def _update_rotation_history(self, secret_name: str, new_secret: str) -> None:
"""Update rotation history"""
if secret_name not in self.rotation_history:
self.rotation_history[secret_name] = {}
self.rotation_history[secret_name].update({
'last_rotation': datetime.utcnow(),
'rotation_count': self.rotation_history[secret_name].get('rotation_count', 0) + 1,
'previous_secret': self.rotation_history[secret_name].get('current_secret'),
'current_secret': new_secret
})
def _notify_rotation(self, secret_name: str, policy: RotationPolicy) -> None:
"""Notify about secret rotation"""
# This would implement actual notification logic
# For example, send email, Slack message, etc.
logger.info(f"Notification: Secret {secret_name} has been rotated")
def record_secret_usage(self, secret_name: str) -> None:
"""Record secret usage for usage-based rotation"""
if secret_name in self.usage_counts:
self.usage_counts[secret_name] += 1
def get_rotation_status(self, secret_name: str) -> Dict[str, Any]:
"""Get rotation status for secret"""
if secret_name not in self.rotation_policies:
return {"error": "No rotation policy found"}
policy = self.rotation_policies[secret_name]
history = self.rotation_history.get(secret_name, {})
return {
"secret_name": secret_name,
"policy": {
"strategy": policy.strategy.value,
"interval_days": policy.interval_days,
"auto_rotate": policy.auto_rotate
},
"history": history,
"usage_count": self.usage_counts.get(secret_name, 0),
"next_rotation": self._calculate_next_rotation(secret_name, policy)
}
def _calculate_next_rotation(self, secret_name: str, policy: RotationPolicy) -> Optional[datetime]:
"""Calculate next rotation time"""
if not policy.auto_rotate:
return None
if policy.strategy == RotationStrategy.TIME_BASED:
last_rotation = self.rotation_history.get(secret_name, {}).get('last_rotation')
if last_rotation:
return last_rotation + timedelta(days=policy.interval_days)
else:
return datetime.utcnow() + timedelta(days=policy.interval_days)
return None
def manual_rotate(self, secret_name: str) -> bool:
"""Manually rotate secret"""
if secret_name not in self.rotation_policies:
logger.error(f"No rotation policy found for secret: {secret_name}")
return False
policy = self.rotation_policies[secret_name]
try:
self._rotate_secret(secret_name, policy)
return True
except Exception as e:
logger.error(f"Manual rotation failed for {secret_name}: {e}")
return False
class SecretAuditor:
"""Secret access auditor"""
def __init__(self):
self.access_logs = []
self.audit_lock = threading.Lock()
def log_secret_access(self, secret_name: str, user: str, action: str,
success: bool, metadata: Optional[Dict[str, Any]] = None) -> None:
"""Log secret access"""
with self.audit_lock:
log_entry = {
"timestamp": datetime.utcnow(),
"secret_name": secret_name,
"user": user,
"action": action,
"success": success,
"metadata": metadata or {}
}
self.access_logs.append(log_entry)
logger.info(f"Secret access logged: {secret_name} by {user}")
def get_access_logs(self, secret_name: Optional[str] = None,
user: Optional[str] = None,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None) -> List[Dict[str, Any]]:
"""Get access logs with optional filtering"""
with self.audit_lock:
logs = self.access_logs.copy()
# Apply filters
if secret_name:
logs = [log for log in logs if log["secret_name"] == secret_name]
if user:
logs = [log for log in logs if log["user"] == user]
if start_time:
logs = [log for log in logs if log["timestamp"] >= start_time]
if end_time:
logs = [log for log in logs if log["timestamp"] <= end_time]
return logs
def get_secret_usage_stats(self, secret_name: str) -> Dict[str, Any]:
"""Get usage statistics for secret"""
logs = self.get_access_logs(secret_name=secret_name)
total_accesses = len(logs)
successful_accesses = len([log for log in logs if log["success"]])
failed_accesses = total_accesses - successful_accesses
# Group by user
user_accesses = {}
for log in logs:
user = log["user"]
if user not in user_accesses:
user_accesses[user] = 0
user_accesses[user] += 1
return {
"secret_name": secret_name,
"total_accesses": total_accesses,
"successful_accesses": successful_accesses,
"failed_accesses": failed_accesses,
"success_rate": (successful_accesses / total_accesses * 100) if total_accesses > 0 else 0,
"user_accesses": user_accesses,
"last_access": logs[-1]["timestamp"] if logs else None
}
# Usage examples
def example_secret_rotation():
"""Example secret rotation usage"""
# Create secret manager
secret_manager = EnvironmentSecretManager()
# Create rotator
rotator = SecretRotator(secret_manager)
# Add rotation policies
policy1 = RotationPolicy(
secret_name="database_password",
strategy=RotationStrategy.TIME_BASED,
interval_days=30,
auto_rotate=True
)
rotator.add_rotation_policy(policy1)
policy2 = RotationPolicy(
secret_name="api_key",
strategy=RotationStrategy.USAGE_BASED,
max_usage_count=1000,
auto_rotate=True
)
rotator.add_rotation_policy(policy2)
# Start rotation scheduler
rotator.start_rotation_scheduler()
# Record secret usage
rotator.record_secret_usage("api_key")
# Get rotation status
status = rotator.get_rotation_status("database_password")
print(f"Rotation status: {status}")
# Manual rotation
manual_success = rotator.manual_rotate("database_password")
print(f"Manual rotation: {manual_success}")
# Secret auditing
auditor = SecretAuditor()
auditor.log_secret_access("database_password", "user1", "read", True)
auditor.log_secret_access("api_key", "user2", "write", False)
# Get usage stats
stats = auditor.get_secret_usage_stats("database_password")
print(f"Usage stats: {stats}")
# Stop rotation scheduler
rotator.stop_rotation_scheduler()
TL;DR Runbook¶
Quick Start¶
# 1. Environment secrets
env_manager = EnvironmentSecretManager("APP_")
secret = env_manager.get_secret("database_password")
# 2. File-based secrets
file_manager = FileSecretManager("/tmp/secrets.json", encrypted=True)
file_manager.set_secret("api_key", "secret-value")
# 3. Cloud secrets
aws_manager = AWSSecretsManager("us-east-1")
aws_secret = aws_manager.get_secret("my-secret")
# 4. Secret rotation
rotator = SecretRotator(secret_manager)
policy = RotationPolicy("database_password", RotationStrategy.TIME_BASED, interval_days=30)
rotator.add_rotation_policy(policy)
rotator.start_rotation_scheduler()
# 5. Secret auditing
auditor = SecretAuditor()
auditor.log_secret_access("secret_name", "user", "read", True)
Essential Patterns¶
# Complete secrets management setup
def setup_secrets_management():
"""Setup complete secrets management environment"""
# Multiple secret backends
env_manager = EnvironmentSecretManager("APP_")
file_manager = FileSecretManager("/tmp/secrets.json", encrypted=True)
aws_manager = AWSSecretsManager("us-east-1")
# Unified secret manager
unified_manager = UnifiedSecretManager(
primary_backend=env_manager,
fallback_backends=[file_manager, aws_manager]
)
# Secret rotation
rotator = SecretRotator(unified_manager)
# Secret auditing
auditor = SecretAuditor()
print("Secrets management setup complete!")
This guide provides the complete machinery for Python secrets management. Each pattern includes implementation examples, rotation strategies, and real-world usage patterns for enterprise secrets management.