| name | security-secrets-management |
| description | Secrets management patterns including vaults, environment variables, key rotation, and secure credential handling for applications |
Security: Secrets Management
Scope: Secrets vaults, credential management, key rotation, secure storage
Lines: ~380
Last Updated: 2025-10-27
When to Use This Skill
Activate this skill when:
- Storing API keys, passwords, or credentials
- Implementing secrets management for applications
- Integrating with HashiCorp Vault, AWS Secrets Manager, or similar
- Rotating encryption keys or credentials
- Managing secrets in CI/CD pipelines
- Preventing credential exposure in code or logs
- Designing multi-environment secret strategies
Secrets Management Fundamentals
What Are Secrets?
"""
Secrets are sensitive data that must be protected:
- API keys and tokens
- Database passwords
- Encryption keys
- TLS/SSL certificates
- OAuth client secrets
- Service account credentials
- Signing keys
"""
API_KEY = "sk_live_abc123def456"
DB_PASSWORD = "MyP@ssw0rd123"
import os
API_KEY = os.environ.get('API_KEY')
DB_PASSWORD = vault.get_secret('database/password')
Environment Variables
Using Environment Variables
import os
from typing import Optional
DATABASE_URL = os.environ['DATABASE_URL']
API_KEY = os.environ.get('API_KEY')
API_KEY = os.environ.get('API_KEY', 'default-value')
def get_required_env(key: str) -> str:
"""Get required environment variable or raise error"""
value = os.environ.get(key)
if not value:
raise ValueError(f"Missing required environment variable: {key}")
return value
PORT = int(os.environ.get('PORT', '8000'))
DEBUG = os.environ.get('DEBUG', 'false').lower() == 'true'
import json
AWS_CREDENTIALS = json.loads(os.environ.get('AWS_CREDENTIALS', '{}'))
.env Files (Development Only)
DATABASE_URL=postgresql://user:password@localhost/db
API_KEY=sk_test_abc123
STRIPE_SECRET=sk_test_def456
DEBUG=true
from dotenv import load_dotenv
import os
if os.environ.get('ENV') != 'production':
load_dotenv()
DATABASE_URL = os.environ['DATABASE_URL']
.gitignore:
# Never commit secrets
.env
.env.local
.env.*.local
secrets.json
credentials.json
*.pem
*.key
HashiCorp Vault
Vault Integration (Python)
import hvac
from typing import Dict, Any
class VaultClient:
"""HashiCorp Vault client wrapper"""
def __init__(self, url: str = None, token: str = None):
self.url = url or os.environ['VAULT_ADDR']
self.token = token or os.environ['VAULT_TOKEN']
self.client = hvac.Client(url=self.url, token=self.token)
if not self.client.is_authenticated():
raise Exception("Vault authentication failed")
def get_secret(self, path: str) -> Dict[str, Any]:
"""Read secret from Vault"""
response = self.client.secrets.kv.v2.read_secret_version(
path=path,
mount_point='secret'
)
return response['data']['data']
def write_secret(self, path: str, data: Dict[str, Any]):
.client.secrets.kv.v2.create_or_update_secret(
path=path,
secret=data,
mount_point=
)
():
.client.secrets.kv.v2.delete_metadata_and_all_versions(
path=path,
mount_point=
)
vault = VaultClient()
db_creds = vault.get_secret()
DATABASE_URL = (
)
vault.write_secret(, {
: ,
:
})
stripe_keys = vault.get_secret()
STRIPE_SECRET = stripe_keys[]
Vault AppRole Authentication
class VaultAppRoleClient:
"""Vault client using AppRole authentication"""
def __init__(self, role_id: str, secret_id: str, url: str = None):
self.url = url or os.environ['VAULT_ADDR']
self.client = hvac.Client(url=self.url)
response = self.client.auth.approle.login(
role_id=role_id,
secret_id=secret_id
)
self.client.token = response['auth']['client_token']
self._start_token_renewal()
def _start_token_renewal(self):
"""Automatically renew token before expiration"""
import threading
def renew():
while True:
time.sleep(3600)
try:
self.client.auth.token.renew_self()
except Exception as e:
logger.error(f"Token renewal failed: {e}")
thread = threading.Thread(target=renew, daemon=True)
thread.start()
vault = VaultAppRoleClient(
role_id=os.environ[],
secret_id=os.environ[]
)
Dynamic Database Credentials
def get_dynamic_db_credentials(vault_client, role: str):
"""Get short-lived database credentials from Vault"""
response = vault_client.client.secrets.database.generate_credentials(
name=role,
mount_point='database'
)
return {
'username': response['data']['username'],
'password': response['data']['password'],
'ttl': response['lease_duration']
}
creds = get_dynamic_db_credentials(vault, 'readonly-role')
conn = psycopg2.connect(
host='localhost',
database='mydb',
user=creds['username'],
password=creds['password']
)
AWS Secrets Manager
AWS Secrets Manager Integration
import boto3
import json
from botocore.exceptions import ClientError
class AWSSecretsManager:
"""AWS Secrets Manager client"""
def __init__(self, region_name: str = 'us-east-1'):
self.client = boto3.client(
'secretsmanager',
region_name=region_name
)
def get_secret(self, secret_name: str) -> dict:
"""Retrieve secret from AWS Secrets Manager"""
try:
response = self.client.get_secret_value(SecretId=secret_name)
if 'SecretString' in response:
return json.loads(response['SecretString'])
else:
import base64
return base64.b64decode(response['SecretBinary'])
except ClientError as e:
if e.response['Error']['Code'] == 'ResourceNotFoundException':
raise ValueError(f"Secret not found: {secret_name}")
raise
def create_secret(self, secret_name: , secret_value: ):
.client.create_secret(
Name=secret_name,
SecretString=json.dumps(secret_value)
)
():
.client.update_secret(
SecretId=secret_name,
SecretString=json.dumps(secret_value)
)
():
.client.rotate_secret(
SecretId=secret_name,
RotationLambdaARN=lambda_arn,
RotationRules={: }
)
secrets = AWSSecretsManager(region_name=)
db_secret = secrets.get_secret()
DATABASE_URL = (
)
secrets.create_secret(, {
: ,
:
})
Caching Secrets
from functools import lru_cache
import time
class CachedSecretsManager:
"""Cache secrets to reduce API calls"""
def __init__(self, secrets_manager):
self.secrets_manager = secrets_manager
self.cache = {}
self.cache_ttl = 300
def get_secret(self, secret_name: str) -> dict:
"""Get secret with caching"""
now = time.time()
if secret_name in self.cache:
cached_value, cached_time = self.cache[secret_name]
if now - cached_time < self.cache_ttl:
return cached_value
value = self.secrets_manager.get_secret(secret_name)
self.cache[secret_name] = (value, now)
return value
def invalidate_cache(self, secret_name: str = None):
"""Clear cache for secret or all secrets"""
if secret_name:
self.cache.pop(secret_name, None)
else:
.cache.clear()
GCP Secret Manager
Google Cloud Secret Manager
from google.cloud import secretmanager
class GCPSecretsManager:
"""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") -> str:
"""Access secret version"""
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')
def create_secret(self, secret_id: str, secret_value: str):
"""Create new secret"""
parent = f"projects/{self.project_id}"
secret = self.client.create_secret(
request={
"parent": parent,
"secret_id": secret_id,
"secret": {"replication": {"automatic": {}}},
}
)
self.client.add_secret_version(
request={
: secret.name,
: {: secret_value.encode()},
}
)
():
parent =
.client.add_secret_version(
request={
: parent,
: {: secret_value.encode()},
}
)
secrets = GCPSecretsManager(project_id=)
api_key = secrets.get_secret()
secrets.create_secret(, )
Key Rotation
Automated Key Rotation
from datetime import datetime, timedelta
from typing import Callable
class KeyRotationService:
"""Automated key rotation service"""
def __init__(self, secrets_manager, notification_service=None):
self.secrets_manager = secrets_manager
self.notification_service = notification_service
def rotate_key(self, secret_name: str, generator: Callable[[], str]):
"""Rotate a secret key"""
new_key = generator()
old_key = self.secrets_manager.get_secret(secret_name)
try:
self.secrets_manager.update_secret(secret_name, {'value': new_key})
logger.info(f"Rotated secret: {secret_name}")
if self.notification_service:
self.notification_service.send_alert(
f"Secret rotated: {secret_name}"
)
return new_key
except Exception as e:
logger.error(f"Key rotation failed: ")
.secrets_manager.update_secret(secret_name, old_key)
():
metadata = .secrets_manager.get_secret_metadata(secret_name)
last_rotated = metadata[]
age = datetime.now() - last_rotated
age > timedelta(days=max_age_days):
logger.warning()
rotation_service = KeyRotationService(secrets_manager)
secrets
():
secrets.token_urlsafe()
new_key = rotation_service.rotate_key(, generate_api_key)
rotation_service.check_expiration(, max_age_days=):
rotation_service.rotate_key(, generate_strong_password)
Zero-Downtime Rotation
class ZeroDowntimeRotation:
"""Rotate secrets without downtime"""
def __init__(self, secrets_manager):
self.secrets_manager = secrets_manager
def rotate_with_overlap(self, secret_name: str, new_value: str):
"""
Dual-key rotation:
1. Add new key (both keys valid)
2. Update services to use new key
3. Remove old key after grace period
"""
current = self.secrets_manager.get_secret(secret_name)
dual_secret = {
'primary': new_value,
'secondary': current['value'],
'rotated_at': datetime.utcnow().isoformat()
}
self.secrets_manager.update_secret(secret_name, dual_secret)
logger.info(f"New key active, old key valid for grace period")
def complete_rotation(self, secret_name: str):
"""Remove old key after grace period"""
secret = self.secrets_manager.get_secret(secret_name)
updated = {'value': secret['primary']}
self.secrets_manager.update_secret(secret_name, updated)
logger.info(f"Rotation completed, old key removed")
rotator = ZeroDowntimeRotation(secrets_manager)
rotator.rotate_with_overlap('database/password', )
rotator.complete_rotation()
CI/CD Secrets Management
GitHub Actions Secrets
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Deploy to production
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
./deploy.sh
Vault Integration in CI/CD
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Import Secrets from Vault
uses: hashicorp/vault-action@v2
with:
url: https://vault.example.com
token: ${{ secrets.VAULT_TOKEN }}
secrets: |
secret/data/database username | DATABASE_USER ;
secret/data/database password | DATABASE_PASSWORD ;
secret/data/api stripe_key | STRIPE_KEY
- name: Deploy
run: |
echo "Using secrets from Vault"
./deploy.sh
Security Best Practices
Secrets Management Checklist
Storage:
Rotation:
Access:
Development:
Detection:
Common Vulnerabilities
Secrets in Code
API_KEY = "sk_live_abc123def456"
try:
connect(password="secret123")
except Exception as e:
logger.error(f"Connection failed with password: {password}")
API_KEY = os.environ['API_KEY']
API_KEY = vault.get_secret('api/stripe')['key']
try:
connect(password=password)
except Exception as e:
logger.error("Connection failed (credentials redacted)")
Secrets Detection Tools
git secrets --install
git secrets --register-aws
git secrets --scan
trufflehog git https://github.com/user/repo
detect-secrets scan > .secrets.baseline
detect-secrets audit .secrets.baseline
gitleaks detect --source . --verbose
Related Skills
security-authentication.md - Password and credential management
security-authorization.md - Access control for secrets
cryptography-encryption.md - Encrypting secrets
cicd-github-actions.md - CI/CD secrets management
Last Updated: 2025-10-27
Format Version: 1.0 (Atomic)