Step 3 · Harden
Goal
Apply systemic security hardening so that the application is defensively strong even when individual bugs exist — covering HTTP headers, rate limiting, least-privilege configs, and automated security gates in CI.
Instructions
You are in workflow step 3 of the security-cycle. Hardening reduces the blast radius of future vulnerabilities. These controls are configured once and protect continuously.
Tasks to Perform
1. Add HTTP Security Headers
// PHP — add to a front-controller or middleware
header("Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none'");
header("Strict-Transport-Security: max-age=31536000; includeSubDomains; preload");
header("X-Content-Type-Options: nosniff");
header("X-Frame-Options: DENY");
header("Referrer-Policy: strict-origin-when-cross-origin");
header("Permissions-Policy: geolocation=(), microphone=(), camera=()");
# Nginx
add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'none';" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
2. Enforce HTTPS and Redirect
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
}
3. Rate Limiting & Throttling
# Nginx — global rate limit
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m;
limit_req zone=api burst=20 nodelay;
# Tighter for auth endpoints
limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m;
location /login { limit_req zone=auth burst=3 nodelay; }
location /register { limit_req zone=auth burst=3 nodelay; }
// Application-layer rate limiting (Laravel example)
Route::middleware(['throttle:10,1'])->group(function () {
Route::post('/login', [AuthController::class, 'login']);
Route::post('/forgot-password', [AuthController::class, 'forgot']);
});
4. Least-Privilege Database User
-- Create a restricted DB user for the app
CREATE USER 'appuser'@'%' IDENTIFIED BY '<strong-password>';
-- Grant only the tables/operations the app actually needs
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.users TO 'appuser'@'%';
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.sessions TO 'appuser'@'%';
GRANT SELECT ON appdb.products TO 'appuser'@'%';
-- Never GRANT ALL, never use root user for the app
REVOKE ALL PRIVILEGES ON *.* FROM 'appuser'@'%';
FLUSH PRIVILEGES;
5. Content Security Policy Tuning
# Start in report-only mode to catch violations without blocking
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
# Monitor violations for 1 week, then switch to enforcement
Content-Security-Policy: default-src 'self'; \
script-src 'self' cdn.jsdelivr.net; \
style-src 'self' 'unsafe-inline'; \
img-src 'self' data: https:; \
font-src 'self'; \
frame-ancestors 'none'; \
form-action 'self'; \
base-uri 'self'
6. Add Security Scanning to CI
# .github/workflows/security.yml
name: Security
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Secret scanning
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Dependency audit (Node)
run: npm audit --audit-level=high
- name: SAST (Semgrep)
uses: returntocorp/semgrep-action@v1
with:
config: >-
p/owasp-top-ten
p/default
- name: Container scan
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
severity: CRITICAL,HIGH
exit-code: '1'
7. Secrets Management
# Ensure ALL secrets come from environment variables or a vault — no hard-coded values
# .env.example should list keys with placeholder values only
# Validate at startup
APP_KEY= # required, no default
DB_PASSWORD= # required, no default
STRIPE_SECRET= # required, no default
# Use a library that validates required vars at boot time
# PHP: vlucas/phpdotenv → Dotenv::required(['APP_KEY', 'DB_PASSWORD'])
# Node: envalid → makeValidator({ APP_KEY: str(), DB_PASSWORD: str() })
8. Verify Hardening
# Check all headers are present
curl -sI https://yourapp.com | grep -E \
"Strict-Transport|Content-Security|X-Frame|X-Content|Referrer|Permissions"
# Run Mozilla Observatory
# https://observatory.mozilla.org/
# OWASP ZAP quickscan
docker run -t owasp/zap2docker-stable zap-baseline.py -t https://yourapp.com
# Ensure CI blocks on HIGH/CRIT
git push origin main # should run the security workflow
Exit Criteria
- [ ] All HTTP security headers present and verified (
curl -I) - [ ] HTTPS enforced; HTTP redirects to HTTPS
- [ ] Rate limiting applies to auth and sensitive API endpoints
- [ ] Database user has least-privilege grants only
- [ ] CSP tuned and enforced (not report-only)
- [ ] Security scanning job in CI — blocks merges on CRITICAL findings
- [ ] No secrets in source code or
.envfiles committed to git - [ ] Mozilla Observatory score ≥ B (≥ 70)
Cycle Complete
The security-cycle is finished. Update TODO.md and schedule re-audits quarterly.