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 .env files committed to git
  • [ ] Mozilla Observatory score ≥ B (≥ 70)

Cycle Complete

The security-cycle is finished. Update TODO.md and schedule re-audits quarterly.