Security Hardening
Defense-in-depth configuration, secrets management, dependency auditing, and incident response for leaked credentials.
Core principle: Every layer of defense buys time and limits blast radius. No single control is sufficient.
When to Use
- Adding security headers to a new service
- Implementing rate limiting on auth endpoints
- Auditing or configuring secrets management
- Running dependency vulnerability scans
- Responding to a leaked credential
- Reviewing supply chain risk of dependencies
When NOT to Use
- Code-level security review (use security-review skill)
- Threat modeling (use threat-modeling skill)
- Compliance framework mapping (use compliance-and-governance skill)
Defense-in-Depth Layers
Perimeter → Network → Application → Data. Each layer assumes the one above has failed. No single control is sufficient.
Security Headers
import helmet from 'helmet';
app.use(helmet());
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], // only if needed
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
},
}));
Required Headers Checklist
| Header | Value | Purpose |
|---|---|---|
Content-Security-Policy |
See above | Prevents XSS, data injection |
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
Forces HTTPS |
X-Frame-Options |
DENY or SAMEORIGIN |
Prevents clickjacking |
X-Content-Type-Options |
nosniff |
Prevents MIME sniffing |
Referrer-Policy |
strict-origin-when-cross-origin |
Limits referrer data |
Permissions-Policy |
camera=(), microphone=(), geolocation=() |
Restricts browser APIs |
Rate Limiting
import rateLimit from 'express-rate-limit';
// Auth endpoints: strict
app.use('/api/auth/', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // 10 attempts per window
message: { error: 'Too many attempts. Try again in 15 minutes.' },
standardHeaders: true,
legacyHeaders: false,
}));
// General API: lenient
app.use('/api/', rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 100,
}));
Endpoints requiring strict rate limiting:
POST /auth/loginPOST /auth/registerPOST /auth/forgot-passwordPOST /auth/reset-password- Any endpoint that sends emails or SMS
Secrets Management
Environment Variable Rules
.env.example → Committed (template with placeholder values)
.env → NOT committed (real secrets)
.env.local → NOT committed (local overrides)
.gitignore must include:
.env
.env.*
!.env.example
*.pem
*.key
*.p12
Fail-Secure Pattern
# SAFE: App fails if secret missing
SECRET_KEY = os.environ['SECRET_KEY']
DATABASE_URL = os.environ['DATABASE_URL']
# DANGEROUS: App runs with insecure default
SECRET_KEY = os.environ.get('SECRET_KEY') or 'default'
Pre-commit Check
# Add to pre-commit hook or CI
git diff --cached | grep -iE "password|secret|api_key|token|AKIA|sk-|ghp_|Bearer"
Dependency Vulnerability Auditing
| Ecosystem | Command | Frequency |
|---|---|---|
| npm | npm audit --audit-level=high |
Every CI run |
| Python | pip-audit |
Every CI run |
| Ruby | bundle audit |
Every CI run |
| Go | govulncheck ./... |
Every CI run |
| Rust | cargo audit |
Every CI run |
Audit Triage Decision Tree
Vulnerability found
├── Severity: Critical/High
│ ├── Reachable in production? → Fix immediately
│ ├── Dev-only dependency? → Fix soon, not a blocker
│ ├── Fix available? → Update to patched version
│ └── No fix available? → Replace dependency or document risk
├── Severity: Medium → Fix in next release cycle
└── Severity: Low → Track, fix during regular maintenance
Supply Chain Risk Assessment
Flag new dependencies that have: single maintainer, >1 year inactive, <1k downloads/week, multiple CVEs, or high-risk features (FFI, eval, shell exec). Anonymous maintainers warrant extra scrutiny.
Incident Response: Leaked Secrets
If a secret (API key, password, token) is found in code or commit history:
Immediate (within 1 hour)
- Revoke the credential immediately — assume it was accessed
- Rotate — generate a new credential
- Do NOT just push a new commit that deletes it — it's in git history
History Cleanup
# Use git-filter-repo (recommended over BFG)
pip install git-filter-repo
git filter-repo --path-glob '*.env' --invert-paths
# OR replace the specific string
git filter-repo --replace-text <(echo 'OLD_SECRET_VALUE==>REDACTED')
# Force push all branches
git push --force --all
git push --force --tags
Investigation (within 24 hours)
- Audit exposure window — When was the secret committed? Was repo public at any time?
- Check for abuse — Review the credential provider's audit logs for unauthorized use
- Notify — If there's any evidence of unauthorized access, follow breach notification procedures
Prevention
- Add
git-secretsorgitleaksas pre-commit hook - Enable GitHub secret scanning on all repos
- Rotate all credentials on a schedule (90-day max for long-lived tokens)
CORS Configuration
app.use(cors({
origin: ['https://app.company.com', 'https://www.company.com'], // Explicit allowlist
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
// NEVER: cors({ origin: '*' }) — allows any origin
Container Security
FROM python:3.12-slim AS base # Minimal base, pinned version (not :latest)
FROM base AS builder # Multi-stage: isolate build deps
RUN pip install --user -r requirements.txt
FROM base AS runtime
RUN useradd -r -u 1001 appuser
USER appuser # Never run as root
COPY --from=builder /root/.local /home/appuser/.local
COPY --chown=appuser:appuser . .
Rules: no secrets in ENV or --build-arg (use --secret mount); pin image digests in CI; scan with trivy image <name> before deploy.
IaC Security (Terraform / Kubernetes)
Terraform: Block all S3 public access (block_public_acls = block_public_policy = true). Never use wildcard IAM (actions = ["*"]) — enumerate specific actions and resources.
Kubernetes — required securityContext for every pod:
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
Scan manifests with trivy config k8s/ or checkov -d . in CI.
Red Flags
Investigate immediately:
- Wildcard CORS (
*) in production - No rate limiting on authentication endpoints
- Secrets in code (even if "old" or "test")
npm auditreporting Critical/High with no tracking issue- Single-maintainer dependency with >10k downloads/week (high-value target)
verify: falseorssl: falsein any production config
Verification Checklist
Headers:
- CSP configured (no
unsafe-evalwithout justification) - HSTS with
includeSubDomains - X-Frame-Options set
- X-Content-Type-Options: nosniff
Rate Limiting:
- Auth endpoints limited (10 req/15min)
- General API limited (100 req/min)
Secrets:
- .env not committed; .env.example committed
- All secrets fail-secure (crash if missing)
- Pre-commit secret scanning hook installed
- No secrets in git history
Dependencies:
-
npm audit(or equivalent) in CI, blocking on High+ - New dependencies supply-chain-assessed before adding
- Lock file committed and up-to-date
CORS:
- Explicit allowlist, not wildcard
- Credentials flag only if needed