| name | production-security |
| description | Production security hardening — secrets management, CORS policy, authentication patterns, authorization models, rate limiting, security headers, dependency scanning, and OWASP Top 10 awareness. Use this skill when the user works on authentication, authorization, secrets, CORS, rate limiting, security headers, or any security-adjacent code. Also trigger when user says /production security. |
Production Security
This skill encodes the security patterns that stop your application from becoming a headline. Every recommendation here comes from real breaches, real CVE exploits, and real incident reports — not theoretical threat models. The patterns are opinionated because security is not a place for "it depends." If you ship with allow_origins=["*"], hardcoded API keys, or MD5 password hashes, you are not making a tradeoff — you are making a mistake.
1. Secrets Management
The #1 rule: secrets never touch code, ever. Not in variables, not in comments, not in "temporary" config files, not in Docker build args. If git log -p or docker history --no-trunc can reveal a secret, you have a breach waiting to happen.
Environment Variables for Local Dev
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
)
database_url: str
jwt_secret_key: str
stripe_api_key: str
redis_url: str = "redis://localhost:6379/0"
environment: str = "development"
DATABASE_URL=postgresql://user:pass@localhost:5432/myapp
JWT_SECRET_KEY=your-256-bit-secret-here
STRIPE_API_KEY=sk_test_...
Secret Managers for Production
Never use .env files in production. Use a proper secret manager:
from google.cloud import secretmanager
def get_secret(project_id: str, secret_id: str, version: str = "latest") -> str:
client = secretmanager.SecretManagerServiceClient()
name = f"projects/{project_id}/secrets/{secret_id}/versions/{version}"
response = client.access_secret_version(request={"name": name})
return response.payload.data.decode("UTF-8")
import boto3
def get_secret(secret_name: str, region: str = "us-east-1") -> str:
client = boto3.client("secretsmanager", region_name=region)
response = client.get_secret_value(SecretId=secret_name)
return response["SecretString"]
Other production-grade options: HashiCorp Vault, Doppler, 1Password Secrets Automation.
Docker: BuildKit Secrets Only
# syntax=docker/dockerfile:1
# GOOD — secret never appears in image layers
RUN --mount=type=secret,id=pip_index_url \
PIP_INDEX_URL=$(cat /run/secrets/pip_index_url) \
pip install --no-cache-dir -r requirements.txt
DOCKER_BUILDKIT=1 docker build --secret id=pip_index_url,src=.pip_credentials .
What NEVER to do:
# ALL OF THESE LEAK SECRETS INTO IMAGE LAYERS:
ARG DATABASE_URL=postgresql://... # visible in docker history
ENV API_KEY=sk_live_... # visible in docker inspect
COPY .env /app/.env # baked into a layer forever
COPY id_rsa /root/.ssh/ # private key in the image
For complete container secret patterns, see production-docker section 4.
Secret Rotation
Design for rotation from day one. Secrets will be compromised — the question is how fast you can rotate.
def verify_jwt(token: str) -> dict:
"""Try current key first, fall back to previous key."""
for key in [settings.jwt_secret_key, settings.jwt_secret_key_previous]:
if key is None:
continue
try:
return jwt.decode(token, key, algorithms=["HS256"])
except jwt.InvalidSignatureError:
continue
raise HTTPException(status_code=401, detail="Invalid token")
Pre-Commit Hooks: Catch Secrets Before They Ship
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks
detect-secrets scan > .secrets.baseline
gitleaks detect --source . --verbose
trufflehog filesystem . --only-verified
Detection: Grep for Hardcoded Secrets
rg '(password|secret|api_key|token|private_key)\s*=\s*"[^"]{8,}"' -i --type py --type js --type ts
rg '(PASSWORD|SECRET|API_KEY|TOKEN)\s*=\s*"[^"]{8,}"'
rg 'sk_(live|test)_[a-zA-Z0-9]{20,}'
rg 'AKIA[0-9A-Z]{16}'
rg 'ghp_[a-zA-Z0-9]{36}'
rg 'xox[bpas]-[a-zA-Z0-9-]+'
rg '-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----'
2. CORS Configuration
CORS (Cross-Origin Resource Sharing) controls which origins can make requests to your API from a browser. Get it wrong and you either block your own frontend or open your API to every site on the internet.
What CORS Protects Against (And What It Does Not)
CORS prevents: A malicious site (evil.com) from making authenticated requests to your API using a victim's browser cookies. Without CORS, any site could read your API responses if the user is logged in.
CORS does NOT prevent: Server-to-server attacks, API key theft from client-side code, or attacks from non-browser clients (curl, Postman, bots). CORS is a browser-only enforcement mechanism.
Python FastAPI
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://app.example.com",
"https://staging.example.com",
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
max_age=600,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Node.js Express
const cors = require('cors');
const allowedOrigins = [
'https://app.example.com',
'https://staging.example.com',
];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type'],
maxAge: 600,
}));
CORS Rules
- Never
allow_origins=["*"] when allow_credentials=True — browsers actually reject this combination, but it signals you have not thought about your CORS policy
- Whitelist specific origins — your frontend domain(s), your staging domain, nothing else
allow_methods — only the methods your API actually uses. If you do not support PATCH, do not allow PATCH
allow_headers — only Authorization and Content-Type for most APIs
max_age — set to 600 (10 min) to reduce preflight requests. Do not set to 86400 in dev (causes stale CORS debugging)
3. Authentication Patterns
JWT: Short-Lived Access, Long-Lived Refresh
Access token: 15 minutes (carried on every request)
Refresh token: 7 days (used only to get new access tokens)
Access tokens are short-lived so that if stolen, the damage window is small. Refresh tokens are long-lived but can be revoked server-side.
JWT Storage: HttpOnly Cookies, Not localStorage
from fastapi.responses import JSONResponse
@app.post("/auth/login")
async def login(credentials: LoginRequest, response: Response):
user = authenticate(credentials.email, credentials.password)
if not user:
raise HTTPException(status_code=401, detail="Invalid credentials")
access_token = create_access_token(user.id, expires_minutes=15)
refresh_token = create_refresh_token(user.id, expires_days=7)
response = JSONResponse(content={"message": "Logged in"})
response.set_cookie(
key="access_token",
value=access_token,
httponly=True,
secure=True,
samesite="lax",
max_age=900,
path="/",
)
response.set_cookie(
key="refresh_token",
value=refresh_token,
httponly=True,
secure=True,
samesite="lax",
max_age=604800,
path="/auth/refresh",
)
return response
Why not localStorage? Any XSS vulnerability gives the attacker full access to tokens stored in localStorage. HttpOnly cookies are invisible to JavaScript — XSS cannot read them.
JWT Validation
import jwt
from datetime import datetime, timezone
def validate_access_token(token: str) -> dict:
try:
payload = jwt.decode(
token,
settings.jwt_secret_key,
algorithms=["HS256"],
options={
"require": ["exp", "sub", "iat"],
"verify_exp": True,
},
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
const jwt = require('jsonwebtoken');
function validateAccessToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET_KEY, {
algorithms: ['HS256'],
complete: false,
});
} catch (err) {
if (err.name === 'TokenExpiredError') {
throw new HttpError(401, 'Token expired');
}
throw new HttpError(401, 'Invalid token');
}
}
Password Hashing: bcrypt or argon2id
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=3,
memory_cost=65536,
parallelism=4,
)
def hash_password(password: str) -> str:
return ph.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
try:
return ph.verify(hashed, plain)
except Exception:
return False
Never use: MD5, SHA1, SHA256 (unsalted), or any general-purpose hash for passwords. These are fast by design — attackers can brute-force billions of hashes per second. bcrypt and argon2id are intentionally slow.
OAuth2 Flows
- Authorization Code — for server-rendered web apps. Server exchanges code for tokens, tokens never touch the browser.
- Authorization Code + PKCE — for SPAs and mobile apps. Same flow, but with a code verifier to prevent interception.
- Client Credentials — for service-to-service. No user involved.
- Never use Implicit flow — deprecated, tokens exposed in URL fragments.
API Keys for Service-to-Service
import hashlib
import secrets
def generate_api_key() -> tuple[str, str]:
"""Returns (raw_key_for_client, hashed_key_for_storage)."""
raw_key = f"sk_{secrets.token_urlsafe(32)}"
hashed = hashlib.sha256(raw_key.encode()).hexdigest()
return raw_key, hashed
def verify_api_key(raw_key: str, stored_hash: str) -> bool:
return hashlib.sha256(raw_key.encode()).hexdigest() == stored_hash
Never roll your own crypto. Use established libraries: PyJWT, python-jose, passlib, argon2-cffi, jsonwebtoken (Node.js). If you are implementing a cryptographic primitive, you are doing it wrong.
4. Authorization Models
Authentication answers "who are you?" Authorization answers "what can you do?"
RBAC (Role-Based Access Control)
Best for most applications. Users have roles, roles have permissions.
from enum import Enum
from fastapi import Depends, HTTPException
class Role(str, Enum):
ADMIN = "admin"