|
| 1 | +# Lab 11 — BONUS — Submission |
| 2 | + |
| 3 | +## Task 1: TLS + Security Headers |
| 4 | + |
| 5 | +### nginx.conf (paste the SSL + header sections only — not the whole file) |
| 6 | +```nginx |
| 7 | + # HTTP server (redirect to HTTPS) |
| 8 | + server { |
| 9 | + listen 8080; |
| 10 | + listen [::]:8080; |
| 11 | + server_name _; |
| 12 | +
|
| 13 | + # Core headers (also on redirects) |
| 14 | + add_header X-Frame-Options "DENY" always; |
| 15 | + add_header X-Content-Type-Options "nosniff" always; |
| 16 | + add_header Referrer-Policy "strict-origin-when-cross-origin" always; |
| 17 | + add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" always; |
| 18 | + add_header Cross-Origin-Opener-Policy "same-origin" always; |
| 19 | + add_header Cross-Origin-Resource-Policy "same-origin" always; |
| 20 | + add_header Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'" always; |
| 21 | +
|
| 22 | + return 308 https://$host:8443$request_uri; |
| 23 | + } |
| 24 | +
|
| 25 | + # HTTPS server |
| 26 | + server { |
| 27 | + listen 8443 ssl; |
| 28 | + listen [::]:8443 ssl; |
| 29 | + http2 on; |
| 30 | + server_name _; |
| 31 | +
|
| 32 | + ssl_certificate /etc/nginx/certs/localhost.crt; |
| 33 | + ssl_certificate_key /etc/nginx/certs/localhost.key; |
| 34 | + ssl_session_timeout 1d; |
| 35 | + ssl_session_cache shared:SSL:10m; |
| 36 | + ssl_session_tickets off; |
| 37 | + ssl_protocols TLSv1.3; |
| 38 | + ssl_ciphers "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-RSA-AES256-GCM-SHA384"; |
| 39 | + ssl_ecdh_curve X25519:secp384r1; |
| 40 | + ssl_prefer_server_ciphers on; |
| 41 | + ssl_stapling off; |
| 42 | +``` |
| 43 | + |
| 44 | +### A. HTTPS redirect proof |
| 45 | +``` |
| 46 | +HTTP/1.1 308 Permanent Redirect |
| 47 | +Server: nginx |
| 48 | +Date: Tue, 30 Jun 2026 19:48:07 GMT |
| 49 | +Content-Type: text/html |
| 50 | +Content-Length: 164 |
| 51 | +Connection: keep-alive |
| 52 | +Location: https://localhost:8443/ |
| 53 | +X-Frame-Options: DENY |
| 54 | +X-Content-Type-Options: nosniff |
| 55 | +Referrer-Policy: strict-origin-when-cross-origin |
| 56 | +Permissions-Policy: camera=(), geolocation=(), microphone=() |
| 57 | +Cross-Origin-Opener-Policy: same-origin |
| 58 | +Cross-Origin-Resource-Policy: same-origin |
| 59 | +Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' |
| 60 | +``` |
| 61 | + |
| 62 | +### B. TLS 1.3 proof |
| 63 | +``` |
| 64 | +depth=0 CN = juice.local |
| 65 | +verify error:num=18:self signed certificate |
| 66 | +verify return:1 |
| 67 | +depth=0 CN = juice.local |
| 68 | +verify return:1 |
| 69 | +CONNECTED(00000006) |
| 70 | +write W BLOCK |
| 71 | +``` |
| 72 | + |
| 73 | +### C. Security headers proof (all 6 present) |
| 74 | +``` |
| 75 | +HTTP/2 200 |
| 76 | +server: nginx |
| 77 | +date: Tue, 30 Jun 2026 19:48:07 GMT |
| 78 | +content-type: text/html; charset=UTF-8 |
| 79 | +content-length: 9903 |
| 80 | +strict-transport-security: max-age=31536000; includeSubDomains; preload |
| 81 | +x-frame-options: DENY |
| 82 | +x-content-type-options: nosniff |
| 83 | +referrer-policy: strict-origin-when-cross-origin |
| 84 | +permissions-policy: camera=(), geolocation=(), microphone=() |
| 85 | +cross-origin-opener-policy: same-origin |
| 86 | +cross-origin-resource-policy: same-origin |
| 87 | +content-security-policy-report-only: default-src 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' |
| 88 | +``` |
| 89 | + |
| 90 | +### What each header defends against (1 sentence each) |
| 91 | +- HSTS: Forces the browser to strictly connect via HTTPS, mitigating man-in-the-middle downgrade attacks. |
| 92 | +- X-Content-Type-Options: nosniff: Prevents the browser from MIME-sniffing the response away from the declared content-type, mitigating drive-by download attacks. |
| 93 | +- X-Frame-Options: DENY: Prevents the site from being embedded in iframes on other domains, defending against clickjacking attacks. |
| 94 | +- Referrer-Policy: Controls how much referrer information is passed to external sites, preventing leakage of sensitive URL tokens or parameters. |
| 95 | +- Permissions-Policy: Restricts the application's ability to use powerful browser features like the camera or microphone, minimizing the impact of XSS. |
| 96 | +- Content-Security-Policy: Enforces an allowlist of trusted domains from which scripts, styles, and images can be loaded, fundamentally mitigating Cross-Site Scripting (XSS). |
| 97 | + |
| 98 | +## Task 2: Production Posture |
| 99 | + |
| 100 | +### Rate limit proof |
| 101 | +| HTTP code | Count out of 60 | |
| 102 | +|-----------|----------------:| |
| 103 | +| 200 | 0 | |
| 104 | +| 429 | 54 | |
| 105 | +| 5xx | 6 | |
| 106 | + |
| 107 | +### Timeout enforced |
| 108 | +``` |
| 109 | +# Sent a partial HTTP request and waited 15 seconds; nginx forcefully closes the connection |
| 110 | +``` |
| 111 | + |
| 112 | +### Cipher hardening |
| 113 | +``` |
| 114 | +Server Temp Key: ECDH, X25519, 253 bits |
| 115 | +New, TLSv1/SSLv3, Cipher is AEAD-AES256-GCM-SHA384 |
| 116 | + Cipher : AEAD-AES256-GCM-SHA384 |
| 117 | +``` |
| 118 | + |
| 119 | +### Cert rotation runbook (7 steps) |
| 120 | +1. **Detect expiry**: Monitor the current certificate using an automated tool like Prometheus or Datadog, triggering an alert when it's < 30 days from expiration. |
| 121 | +2. **Order new cert**: Generate a new CSR and private key, and request a fresh signed certificate from the CA (e.g., Let's Encrypt or an internal PKI). |
| 122 | +3. **Validate**: Verify the newly received certificate chain using `openssl verify -CAfile` against the root CA to ensure it was properly signed and is cryptographically sound. |
| 123 | +4. **Atomic swap**: Stage the new certificate alongside the old one in the file system and perform a zero-downtime hot reload of the reverse proxy (e.g., `nginx -s reload`). |
| 124 | +5. **Verify**: Use an external testing tool (like `testssl.sh` or `openssl s_client`) to confirm the server is successfully serving the new certificate in production. |
| 125 | +6. **Rollback plan**: If the new certificate is rejected or broken, immediately run a script to swap the symlinks back to the old certificate paths and execute a hot reload to restore service. |
| 126 | +7. **Audit**: Log the successful rotation event in the centralized audit system (or Slack/Jira) and destroy the old private key securely to prevent future compromise. |
| 127 | + |
| 128 | +### What OCSP stapling buys you (2-3 sentences, reference Reading 11) |
| 129 | +OCSP stapling allows the web server (Nginx) to proactively fetch and cache the certificate revocation status from the CA, securely "stapling" it directly to the TLS handshake. This prevents clients from having to make a slow, privacy-leaking DNS/HTTP call to the CA to check if the cert was revoked. However, it requires a publicly trusted certificate signed by a real CA; for a self-signed lab certificate, there is no CA endpoint to query for revocation status, meaning stapling will simply fail. |
| 130 | + |
| 131 | +## Bonus: WAF Sidecar with OWASP CRS |
| 132 | + |
| 133 | +### Setup choice |
| 134 | +- WAF used: ModSecurity v3 (owasp/modsecurity-crs:nginx-alpine image) |
| 135 | +- OWASP CRS version: 4.x |
| 136 | +- Paranoia level: 1 |
| 137 | + |
| 138 | +### Attack payload sent |
| 139 | +`GET /rest/products/search?q=' OR 1=1--` (URL-encoded) |
| 140 | + |
| 141 | +### Before WAF (Nginx alone) |
| 142 | +``` |
| 143 | +no-waf: HTTP 500 |
| 144 | +``` |
| 145 | +*(Juice Shop returns a 500 error because the SQL injection crashes the backend DB query, but Nginx happily proxies the attack).* |
| 146 | + |
| 147 | +### After WAF |
| 148 | +``` |
| 149 | +with-waf: HTTP 403 |
| 150 | +``` |
| 151 | +*(The WAF intercepts the SQL injection before it reaches Juice Shop and immediately returns a 403 Forbidden).* |
| 152 | + |
| 153 | +### Audit log excerpt (the rule that fired) |
| 154 | +```json |
| 155 | +{ |
| 156 | + "message":"SQL Injection Attack Detected via libinjection", |
| 157 | + "details":{ |
| 158 | + "match":"detected SQLi using libinjection.", |
| 159 | + "reference":"v28,10", |
| 160 | + "ruleId":"942100", |
| 161 | + "file":"/etc/modsecurity.d/owasp-crs/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf", |
| 162 | + "lineNumber":"46", |
| 163 | + "data":"Matched Data: s&1c found within ARGS:q: ' OR 1=1--", |
| 164 | + "severity":"2", |
| 165 | + "ver":"OWASP_CRS/4.27.0" |
| 166 | + } |
| 167 | +} |
| 168 | +``` |
| 169 | +Rule ID: **942100** — OWASP CRS rule name: **SQL Injection Attack Detected via libinjection** |
| 170 | + |
| 171 | +### Tradeoff analysis (3 sentences) |
| 172 | +Deploying a WAF with OWASP CRS gives us immediate, zero-code protection against zero-day exploits (like Log4Shell) and standard OWASP Top 10 attacks that might slip past our SAST/DAST pipeline or Conftest gates. However, it costs significant operational overhead: higher paranoia levels will inevitably block legitimate user traffic (False Positives), requiring constant tuning of rule exclusions and performance monitoring. You should NOT deploy a WAF in front of internal, highly-trusted backend-to-backend microservice communication where the performance penalty of deep packet inspection outweighs the negligible risk of inbound attacks. |
0 commit comments