Initial commit
This commit is contained in:
702
secrets-patterns.md
Normal file
702
secrets-patterns.md
Normal file
@@ -0,0 +1,702 @@
|
||||
# Secrets Management Patterns
|
||||
|
||||
This document outlines the secure patterns for managing secrets in GitLab stack projects.
|
||||
|
||||
## Table of Contents
|
||||
1. [Core Security Principles](#core-security-principles)
|
||||
2. [Directory and File Structure](#directory-and-file-structure)
|
||||
3. [Docker Secrets Integration](#docker-secrets-integration)
|
||||
4. [Secret Detection Patterns](#secret-detection-patterns)
|
||||
5. [Migration Patterns](#migration-patterns)
|
||||
6. [Common Secret Types](#common-secret-types)
|
||||
|
||||
---
|
||||
|
||||
## Core Security Principles
|
||||
|
||||
### The Golden Rules
|
||||
|
||||
**NEVER**:
|
||||
- ❌ Put secrets in .env files
|
||||
- ❌ Put secrets in docker-compose.yml environment variables
|
||||
- ❌ Commit secrets to git
|
||||
- ❌ Use world-readable permissions
|
||||
- ❌ Store secrets as root-owned files
|
||||
- ❌ Hardcode secrets in application code
|
||||
- ❌ Log secret values
|
||||
- ❌ Pass secrets via command-line arguments
|
||||
|
||||
**ALWAYS**:
|
||||
- ✅ Use Docker secrets mechanism
|
||||
- ✅ Store secret files in ./secrets directory
|
||||
- ✅ Set 700 permissions on ./secrets directory
|
||||
- ✅ Set 600 permissions on secret files
|
||||
- ✅ Add ./secrets/* to .gitignore
|
||||
- ✅ Use cryptographically secure random generation
|
||||
- ✅ Rotate secrets regularly
|
||||
- ✅ Audit secret usage
|
||||
|
||||
---
|
||||
|
||||
## Directory and File Structure
|
||||
|
||||
### Standard ./secrets Directory Layout
|
||||
|
||||
```
|
||||
./secrets/
|
||||
├── .gitkeep # Only file tracked by git
|
||||
├── db_password # Database password
|
||||
├── db_root_password # Database root password
|
||||
├── api_key # External API key
|
||||
├── jwt_secret # JWT signing secret
|
||||
├── oauth_client_secret # OAuth secret
|
||||
├── smtp_password # Email password
|
||||
├── encryption_key # Application encryption key
|
||||
└── ssl/
|
||||
├── cert.pem # SSL certificate
|
||||
└── key.pem # SSL private key
|
||||
```
|
||||
|
||||
### Permissions Reference
|
||||
|
||||
```bash
|
||||
# Directory permissions
|
||||
drwx------ ./secrets/ # 700 (owner only)
|
||||
|
||||
# File permissions
|
||||
-rw------- db_password # 600 (owner read/write)
|
||||
-rw------- api_key # 600
|
||||
-rw------- jwt_secret # 600
|
||||
|
||||
# Ownership
|
||||
user:user all files and directories # NOT root
|
||||
```
|
||||
|
||||
### Setting Up Proper Permissions
|
||||
|
||||
```bash
|
||||
# Create secrets directory
|
||||
mkdir -p ./secrets
|
||||
chmod 700 ./secrets
|
||||
|
||||
# Create secret file
|
||||
echo -n "secret-value" > ./secrets/db_password
|
||||
chmod 600 ./secrets/db_password
|
||||
|
||||
# Verify permissions
|
||||
ls -la ./secrets/
|
||||
# Expected: drwx------ ... ./secrets/
|
||||
# Expected: -rw------- ... db_password
|
||||
|
||||
# Fix ownership if root-owned
|
||||
sudo chown -R $(id -u):$(id -g) ./secrets/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker Secrets Integration
|
||||
|
||||
### Top-Level Secrets Definition
|
||||
|
||||
**File-based secrets** (preferred for development/single-host):
|
||||
|
||||
```yaml
|
||||
secrets:
|
||||
db_password:
|
||||
file: ./secrets/db_password
|
||||
|
||||
api_key:
|
||||
file: ./secrets/api_key
|
||||
|
||||
jwt_secret:
|
||||
file: ./secrets/jwt_secret
|
||||
|
||||
# SSL certificates
|
||||
ssl_cert:
|
||||
file: ./secrets/ssl/cert.pem
|
||||
|
||||
ssl_key:
|
||||
file: ./secrets/ssl/key.pem
|
||||
```
|
||||
|
||||
**External secrets** (for production/swarm):
|
||||
|
||||
```yaml
|
||||
secrets:
|
||||
db_password:
|
||||
external: true
|
||||
name: prod_db_password
|
||||
|
||||
api_key:
|
||||
external: true
|
||||
name: prod_api_key_v2
|
||||
```
|
||||
|
||||
### Service Secret Usage
|
||||
|
||||
**Basic usage**:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
image: myapp:latest
|
||||
secrets:
|
||||
- db_password
|
||||
- api_key
|
||||
- jwt_secret
|
||||
# Secrets mounted at /run/secrets/secret_name
|
||||
```
|
||||
|
||||
**Containers with native Docker secrets support**:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
secrets:
|
||||
- db_password
|
||||
environment:
|
||||
POSTGRES_DB: myapp
|
||||
POSTGRES_USER: appuser
|
||||
# Use _FILE suffix to point to secret
|
||||
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
|
||||
```
|
||||
|
||||
**Supported containers with _FILE suffix**:
|
||||
- PostgreSQL: `POSTGRES_PASSWORD_FILE`
|
||||
- MySQL/MariaDB: `MYSQL_ROOT_PASSWORD_FILE`, `MYSQL_PASSWORD_FILE`
|
||||
- MongoDB: Various `_FILE` variables
|
||||
- Redis: Configuration file can read from secret path
|
||||
|
||||
**Containers requiring docker-entrypoint.sh**:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
custom_app:
|
||||
image: myapp:latest
|
||||
entrypoint: /docker-entrypoint.sh
|
||||
command: ["npm", "start"]
|
||||
volumes:
|
||||
- ./docker-entrypoint.sh:/docker-entrypoint.sh:ro
|
||||
secrets:
|
||||
- api_key
|
||||
- jwt_secret
|
||||
# docker-entrypoint.sh loads secrets into environment
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Secret Detection Patterns
|
||||
|
||||
### Patterns That Indicate Secrets
|
||||
|
||||
**Variable Name Patterns** (case-insensitive):
|
||||
|
||||
```regex
|
||||
.*PASSWORD.*
|
||||
.*SECRET.*
|
||||
.*KEY.*
|
||||
.*TOKEN.*
|
||||
.*API.*
|
||||
.*AUTH.*
|
||||
.*CREDENTIAL.*
|
||||
.*PRIVATE.*
|
||||
.*CERT.*
|
||||
```
|
||||
|
||||
**Value Patterns**:
|
||||
|
||||
```regex
|
||||
# Base64-encoded (long strings)
|
||||
^[A-Za-z0-9+/]{40,}={0,2}$
|
||||
|
||||
# Hex strings (64+ chars)
|
||||
^[a-f0-9]{64,}$
|
||||
|
||||
# JWT tokens
|
||||
^eyJ[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$
|
||||
|
||||
# API keys (common formats)
|
||||
^sk_live_[A-Za-z0-9]{24,}$
|
||||
^pk_live_[A-Za-z0-9]{24,}$
|
||||
|
||||
# UUID format
|
||||
^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
|
||||
```
|
||||
|
||||
### Scanning .env for Secrets
|
||||
|
||||
**Examples of secrets in .env** (BAD):
|
||||
|
||||
```bash
|
||||
# ❌ BAD - These are secrets and should NOT be in .env
|
||||
DB_PASSWORD=supersecret123
|
||||
API_KEY=sk_live_abc123xyz789
|
||||
JWT_SECRET=my-super-secret-jwt-key
|
||||
STRIPE_SECRET_KEY=sk_test_abc123
|
||||
OAUTH_CLIENT_SECRET=oauth-secret-123
|
||||
ENCRYPTION_KEY=aes256-key-here
|
||||
ADMIN_PASSWORD=admin123
|
||||
SMTP_PASSWORD=email-password
|
||||
PRIVATE_KEY=-----BEGIN PRIVATE KEY-----
|
||||
```
|
||||
|
||||
**What SHOULD be in .env** (GOOD):
|
||||
|
||||
```bash
|
||||
# ✅ GOOD - Non-secret configuration
|
||||
APP_NAME=myapp
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_URL=https://example.com
|
||||
|
||||
# Database connection (NOT credentials)
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
DB_NAME=myapp_production
|
||||
DB_USER=appuser
|
||||
# DB_PASSWORD is in ./secrets/db_password
|
||||
|
||||
# Redis
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
|
||||
# Ports
|
||||
WEB_PORT=80
|
||||
API_PORT=8080
|
||||
|
||||
# Feature flags
|
||||
ENABLE_CACHING=true
|
||||
ENABLE_LOGGING=true
|
||||
```
|
||||
|
||||
### Scanning docker-compose.yml for Secrets
|
||||
|
||||
**Bad patterns** (secrets in environment):
|
||||
|
||||
```yaml
|
||||
# ❌ BAD - Secrets in environment variables
|
||||
services:
|
||||
app:
|
||||
environment:
|
||||
DB_PASSWORD: supersecret123 # ❌ CRITICAL
|
||||
API_KEY: sk_live_abc123 # ❌ CRITICAL
|
||||
JWT_SECRET: my-jwt-secret # ❌ CRITICAL
|
||||
|
||||
postgres:
|
||||
environment:
|
||||
POSTGRES_PASSWORD: dbpassword123 # ❌ CRITICAL
|
||||
```
|
||||
|
||||
**Good patterns** (using Docker secrets):
|
||||
|
||||
```yaml
|
||||
# ✅ GOOD - Using Docker secrets
|
||||
services:
|
||||
app:
|
||||
secrets:
|
||||
- db_password
|
||||
- api_key
|
||||
- jwt_secret
|
||||
environment:
|
||||
# Only non-secret config in environment
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
DB_NAME: myapp
|
||||
|
||||
postgres:
|
||||
secrets:
|
||||
- db_password
|
||||
environment:
|
||||
POSTGRES_DB: myapp
|
||||
POSTGRES_USER: appuser
|
||||
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
|
||||
|
||||
secrets:
|
||||
db_password:
|
||||
file: ./secrets/db_password
|
||||
api_key:
|
||||
file: ./secrets/api_key
|
||||
jwt_secret:
|
||||
file: ./secrets/jwt_secret
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Patterns
|
||||
|
||||
### Pattern 1: Migrate from .env to Docker Secrets
|
||||
|
||||
**Before** (.env file):
|
||||
|
||||
```bash
|
||||
DB_PASSWORD=mysecretpass
|
||||
API_KEY=sk_live_abc123xyz
|
||||
JWT_SECRET=my-jwt-secret-key
|
||||
```
|
||||
|
||||
**Migration steps**:
|
||||
|
||||
```bash
|
||||
# 1. Create secret files
|
||||
echo -n "mysecretpass" > ./secrets/db_password
|
||||
echo -n "sk_live_abc123xyz" > ./secrets/api_key
|
||||
echo -n "my-jwt-secret-key" > ./secrets/jwt_secret
|
||||
|
||||
# 2. Set permissions
|
||||
chmod 600 ./secrets/*
|
||||
|
||||
# 3. Remove from .env
|
||||
sed -i '/DB_PASSWORD=/d' .env
|
||||
sed -i '/API_KEY=/d' .env
|
||||
sed -i '/JWT_SECRET=/d' .env
|
||||
```
|
||||
|
||||
**After** (.env file):
|
||||
|
||||
```bash
|
||||
# Secrets moved to Docker secrets in ./secrets/
|
||||
# DB_PASSWORD: ./secrets/db_password
|
||||
# API_KEY: ./secrets/api_key
|
||||
# JWT_SECRET: ./secrets/jwt_secret
|
||||
```
|
||||
|
||||
### Pattern 2: Migrate from docker-compose.yml environment to Secrets
|
||||
|
||||
**Before**:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
environment:
|
||||
DB_HOST: postgres
|
||||
DB_PASSWORD: supersecret123 # ❌ Secret in compose
|
||||
API_KEY: sk_live_abc123 # ❌ Secret in compose
|
||||
```
|
||||
|
||||
**Migration**:
|
||||
|
||||
```bash
|
||||
# Extract values and create secret files
|
||||
echo -n "supersecret123" > ./secrets/db_password
|
||||
echo -n "sk_live_abc123" > ./secrets/api_key
|
||||
chmod 600 ./secrets/*
|
||||
```
|
||||
|
||||
**After**:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
secrets:
|
||||
- db_password
|
||||
- api_key
|
||||
environment:
|
||||
DB_HOST: postgres
|
||||
# Secrets loaded from /run/secrets/
|
||||
|
||||
secrets:
|
||||
db_password:
|
||||
file: ./secrets/db_password
|
||||
api_key:
|
||||
file: ./secrets/api_key
|
||||
```
|
||||
|
||||
### Pattern 3: Create docker-entrypoint.sh for Legacy Containers
|
||||
|
||||
When container doesn't support `_FILE` variables:
|
||||
|
||||
**docker-entrypoint.sh**:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Function to load secret into environment variable
|
||||
load_secret() {
|
||||
local secret_name=$1
|
||||
local env_var=$2
|
||||
local secret_file="/run/secrets/${secret_name}"
|
||||
|
||||
if [ -f "$secret_file" ]; then
|
||||
export "${env_var}=$(cat "$secret_file")"
|
||||
echo "✓ Loaded secret: $secret_name -> $env_var"
|
||||
else
|
||||
echo "✗ ERROR: Secret file not found: $secret_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Load all required secrets
|
||||
load_secret "db_password" "DB_PASSWORD"
|
||||
load_secret "api_key" "API_KEY"
|
||||
load_secret "jwt_secret" "JWT_SECRET"
|
||||
|
||||
# Execute the original command
|
||||
exec "$@"
|
||||
```
|
||||
|
||||
**docker-compose.yml**:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
legacy_app:
|
||||
image: legacy-app:latest
|
||||
entrypoint: /docker-entrypoint.sh
|
||||
command: ["node", "server.js"]
|
||||
volumes:
|
||||
- ./docker-entrypoint.sh:/docker-entrypoint.sh:ro
|
||||
secrets:
|
||||
- db_password
|
||||
- api_key
|
||||
- jwt_secret
|
||||
```
|
||||
|
||||
**Set permissions**:
|
||||
|
||||
```bash
|
||||
chmod +x docker-entrypoint.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Secret Types
|
||||
|
||||
### 1. Database Passwords
|
||||
|
||||
**Generate**:
|
||||
```bash
|
||||
openssl rand -base64 32 | tr -d '/+=' | head -c 32 > ./secrets/db_password
|
||||
chmod 600 ./secrets/db_password
|
||||
```
|
||||
|
||||
**Use with PostgreSQL**:
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
secrets:
|
||||
- db_password
|
||||
environment:
|
||||
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
|
||||
```
|
||||
|
||||
### 2. API Keys
|
||||
|
||||
**Generate**:
|
||||
```bash
|
||||
openssl rand -hex 32 > ./secrets/api_key
|
||||
chmod 600 ./secrets/api_key
|
||||
```
|
||||
|
||||
**Format**: 64 hex characters
|
||||
|
||||
### 3. JWT Secrets
|
||||
|
||||
**Generate**:
|
||||
```bash
|
||||
openssl rand -base64 64 > ./secrets/jwt_secret
|
||||
chmod 600 ./secrets/jwt_secret
|
||||
```
|
||||
|
||||
**Format**: Base64-encoded, 64+ characters
|
||||
|
||||
### 4. Encryption Keys
|
||||
|
||||
**Generate AES-256 key**:
|
||||
```bash
|
||||
openssl rand -hex 32 > ./secrets/encryption_key
|
||||
chmod 600 ./secrets/encryption_key
|
||||
```
|
||||
|
||||
**Format**: 32 bytes hex (256-bit)
|
||||
|
||||
### 5. Session Secrets
|
||||
|
||||
**Generate**:
|
||||
```bash
|
||||
openssl rand -base64 32 > ./secrets/session_secret
|
||||
chmod 600 ./secrets/session_secret
|
||||
```
|
||||
|
||||
### 6. OAuth Client Secrets
|
||||
|
||||
**Usually provided by OAuth provider**, store securely:
|
||||
```bash
|
||||
echo -n "provider-given-secret" > ./secrets/oauth_client_secret
|
||||
chmod 600 ./secrets/oauth_client_secret
|
||||
```
|
||||
|
||||
### 7. SSL/TLS Certificates and Keys
|
||||
|
||||
**Store certificate and key separately**:
|
||||
|
||||
```bash
|
||||
# Certificate (can be less restrictive)
|
||||
cp cert.pem ./secrets/ssl/cert.pem
|
||||
chmod 644 ./secrets/ssl/cert.pem
|
||||
|
||||
# Private key (must be restrictive)
|
||||
cp key.pem ./secrets/ssl/key.pem
|
||||
chmod 600 ./secrets/ssl/key.pem
|
||||
```
|
||||
|
||||
**Use in compose**:
|
||||
```yaml
|
||||
secrets:
|
||||
ssl_cert:
|
||||
file: ./secrets/ssl/cert.pem
|
||||
ssl_key:
|
||||
file: ./secrets/ssl/key.pem
|
||||
|
||||
services:
|
||||
nginx:
|
||||
secrets:
|
||||
- ssl_cert
|
||||
- ssl_key
|
||||
# Mounted at /run/secrets/ssl_cert and /run/secrets/ssl_key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Git Protection Patterns
|
||||
|
||||
### .gitignore Configuration
|
||||
|
||||
**Comprehensive .gitignore**:
|
||||
|
||||
```gitignore
|
||||
# Secrets directory - NEVER commit
|
||||
/secrets/
|
||||
/secrets/*
|
||||
|
||||
# Allow only .gitkeep
|
||||
!secrets/.gitkeep
|
||||
|
||||
# Backup files
|
||||
*.old
|
||||
*.backup
|
||||
*.bak
|
||||
*~
|
||||
|
||||
# Environment files (may contain secrets)
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.production
|
||||
|
||||
# Common secret file patterns
|
||||
*password*.txt
|
||||
*secret*.txt
|
||||
*key*.txt
|
||||
*token*.txt
|
||||
*credential*.txt
|
||||
|
||||
# SSL/TLS
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
*.p12
|
||||
*.pfx
|
||||
|
||||
# SSH keys
|
||||
id_rsa
|
||||
id_ed25519
|
||||
*.ppk
|
||||
```
|
||||
|
||||
### Checking Git Status
|
||||
|
||||
**Verify secrets aren't staged**:
|
||||
```bash
|
||||
# Check for secrets in staging
|
||||
git status | grep secrets/
|
||||
|
||||
# Should only show .gitkeep if anything
|
||||
# If other files shown, they're staged (BAD!)
|
||||
```
|
||||
|
||||
**Check git history**:
|
||||
```bash
|
||||
# Search for secrets in history
|
||||
git log --all --full-history -- ./secrets/
|
||||
|
||||
# Search for specific patterns
|
||||
git log -p --all -S "password"
|
||||
git log -p --all -S "secret"
|
||||
```
|
||||
|
||||
**Remove secrets from git history** (if committed):
|
||||
```bash
|
||||
# Using git-filter-repo (recommended)
|
||||
git filter-repo --path secrets/ --invert-paths
|
||||
|
||||
# Or BFG Repo-Cleaner
|
||||
bfg --delete-folders secrets
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Secret Rotation Patterns
|
||||
|
||||
### Safe Rotation Procedure
|
||||
|
||||
```bash
|
||||
# 1. Backup current secret
|
||||
cp ./secrets/api_key ./secrets/api_key.$(date +%Y%m%d).old
|
||||
|
||||
# 2. Generate new secret
|
||||
openssl rand -hex 32 > ./secrets/api_key
|
||||
|
||||
# 3. Test with new secret
|
||||
docker compose up -d
|
||||
docker compose logs app # Check for errors
|
||||
|
||||
# 4. If successful, remove old backup after grace period
|
||||
# rm ./secrets/api_key.*.old
|
||||
```
|
||||
|
||||
### Rotation Tracking
|
||||
|
||||
**Create .secrets/metadata.yml** (not tracked):
|
||||
|
||||
```yaml
|
||||
db_password:
|
||||
created: 2025-01-15
|
||||
rotated: 2025-10-20
|
||||
rotation_interval_days: 90
|
||||
|
||||
api_key:
|
||||
created: 2025-01-15
|
||||
rotated: 2025-10-20
|
||||
rotation_interval_days: 90
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Checklist
|
||||
|
||||
### Pre-Deployment
|
||||
|
||||
- [ ] All secrets in ./secrets directory
|
||||
- [ ] Directory permissions: 700
|
||||
- [ ] File permissions: 600
|
||||
- [ ] No root-owned files
|
||||
- [ ] ./secrets/* in .gitignore
|
||||
- [ ] No secrets in .env
|
||||
- [ ] No secrets in docker-compose.yml environment
|
||||
- [ ] All referenced secrets exist
|
||||
- [ ] docker-entrypoint.sh only when necessary
|
||||
- [ ] No secrets in git history
|
||||
|
||||
### Post-Deployment
|
||||
|
||||
- [ ] Services can access secrets
|
||||
- [ ] No secrets in container logs
|
||||
- [ ] Secrets mounted at /run/secrets/
|
||||
- [ ] No permission errors
|
||||
- [ ] Rotation schedule established
|
||||
|
||||
---
|
||||
|
||||
*These patterns ensure secrets are managed securely throughout the stack lifecycle.*
|
||||
Reference in New Issue
Block a user