diff --git a/labs/lab11/reverse-proxy/nginx.conf b/labs/lab11/reverse-proxy/nginx.conf index dff91b26..9b7794a7 100644 --- a/labs/lab11/reverse-proxy/nginx.conf +++ b/labs/lab11/reverse-proxy/nginx.conf @@ -1,127 +1,86 @@ -user nginx; -worker_processes auto; - -events { worker_connections 1024; } +events { + worker_connections 1024; +} http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - sendfile on; - keepalive_timeout 10; - server_tokens off; - gzip off; - - # Security-focused logs - log_format security '$remote_addr - $remote_user [$time_local] ' - '"$request" $status $body_bytes_sent ' - '"$http_referer" "$http_user_agent" ' - 'rt=$request_time uct=$upstream_connect_time ' - 'urt=$upstream_response_time'; - access_log /var/log/nginx/access.log security; - error_log /var/log/nginx/error.log warn; - - # Upstream app - upstream juice { - server juice:3000; - keepalive 32; - } - - # Rate limit zone for login - # ~10 req/min per IP, burst of 5 + # TASK 2: Zones for Rate Limiting & Connection Limits (must be in http block) limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m; limit_req_status 429; + limit_conn_zone $binary_remote_addr zone=conn:10m; - map $http_upgrade $connection_upgrade { default upgrade; '' close; } - - # Common proxy settings - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_http_version 1.1; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Upgrade $http_upgrade; - # Prevent upstream TLS BREACH vector by disabling compression from upstream - proxy_set_header Accept-Encoding ""; - proxy_read_timeout 30s; - proxy_send_timeout 30s; - proxy_connect_timeout 5s; - proxy_hide_header X-Powered-By; - # Hide upstream headers to avoid duplicates and enforce policy at the proxy - proxy_hide_header X-Frame-Options; - proxy_hide_header X-Content-Type-Options; - proxy_hide_header Referrer-Policy; - proxy_hide_header Permissions-Policy; - proxy_hide_header Cross-Origin-Opener-Policy; - proxy_hide_header Cross-Origin-Resource-Policy; - proxy_hide_header Content-Security-Policy; - proxy_hide_header Content-Security-Policy-Report-Only; - proxy_hide_header Access-Control-Allow-Origin; - - # HTTP server (redirect to HTTPS) + # 1. HTTP server on 80 redirects to HTTPS 443 server { listen 80; listen [::]:80; - server_name _; - - # Core headers (also on redirects) - add_header X-Frame-Options "DENY" always; - add_header X-Content-Type-Options "nosniff" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" always; - add_header Cross-Origin-Opener-Policy "same-origin" always; - add_header Cross-Origin-Resource-Policy "same-origin" always; - 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; - - return 308 https://$host$request_uri; + server_name localhost; + return 301 https://$host$request_uri; } - # HTTPS server + # 2. HTTPS server on 443 server { listen 443 ssl; listen [::]:443 ssl; - http2 on; - server_name _; + server_name localhost; - ssl_certificate /etc/nginx/certs/localhost.crt; + ssl_certificate /etc/nginx/certs/localhost.crt; ssl_certificate_key /etc/nginx/certs/localhost.key; - ssl_session_timeout 10m; - ssl_session_cache shared:SSL:10m; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:EECDH+AESGCM:EDH+AESGCM"; - ssl_prefer_server_ciphers on; - ssl_stapling off; - # If using a publicly-trusted certificate, you may enable OCSP stapling: - # ssl_stapling on; - # ssl_stapling_verify on; - # resolver 1.1.1.1 8.8.8.8 valid=300s; - # resolver_timeout 5s; - # ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; - - client_max_body_size 2m; + + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + + # TASK 2: Cipher hardening, Session Settings & OCSP Stapling + ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256; + ssl_ecdh_curve X25519:secp384r1; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + ssl_session_tickets off; + + # (Optional in lab; mandatory in prod) OCSP stapling + ssl_stapling on; + ssl_stapling_verify on; + resolver 8.8.8.8 1.1.1.1 valid=300s; + + # TASK 2: Connection Limits & Client Timeouts (Fail-closed) + limit_conn conn 50; client_body_timeout 10s; client_header_timeout 10s; - keepalive_timeout 10s; - send_timeout 10s; - # Security headers (include HSTS here only) - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; - add_header X-Frame-Options "DENY" always; + # TASK 1: Security Headers + add_header Strict-Transport-Security "max-age=63072000; 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; - add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" always; - add_header Cross-Origin-Opener-Policy "same-origin" always; - add_header Cross-Origin-Resource-Policy "same-origin" always; - 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; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; + add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always; - location = /rest/user/login { + # TASK 2: Specific Location for Auth Endpoints (Rate Limiting) + location /rest/user/login { limit_req zone=login burst=5 nodelay; - limit_req_log_level warn; - proxy_pass http://juice; + + proxy_pass http://juice:3000; + + # TASK 2: Proxy Timeouts + proxy_read_timeout 30s; + proxy_connect_timeout 5s; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; } + # 4. Upstream proxy to Juice Shop (Catch-all for everything else) location / { - proxy_pass http://juice; + proxy_pass http://juice:3000; + + # TASK 2: Proxy Timeouts + proxy_read_timeout 30s; + proxy_connect_timeout 5s; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; } } } diff --git a/labs/lab11/waf/docker-compose.override.yml b/labs/lab11/waf/docker-compose.override.yml new file mode 100644 index 00000000..f2d403cf --- /dev/null +++ b/labs/lab11/waf/docker-compose.override.yml @@ -0,0 +1,10 @@ +services: + waf: + image: owasp/modsecurity-crs:nginx-alpine + container_name: lab11-waf + ports: + - "8080:80" + environment: + - SEC_RULE_ENGINE=On + - PARANOIA=1 + - BACKEND=http://juice:3000 diff --git a/submissions/lab11.md b/submissions/lab11.md new file mode 100644 index 00000000..ec3b2c71 --- /dev/null +++ b/submissions/lab11.md @@ -0,0 +1,150 @@ +# Lab 11 — BONUS — Submission + +## Task 1: TLS + Security Headers + +### nginx.conf (paste the SSL + header sections only — not the whole file) +```nginx + server { + listen 443 ssl; + listen [::]:443 ssl; + server_name localhost; + + ssl_certificate /etc/nginx/certs/localhost.crt; + ssl_certificate_key /etc/nginx/certs/localhost.key; + + # ssl_protocols TLSv1.3 only & ssl_prefer_server_ciphers off + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + + # 3. Six required headers, all with the `always` keyword + add_header Strict-Transport-Security "max-age=63072000; 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; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; + + add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always; +``` + +### A. HTTPS redirect proof +``` +HTTP/1.1 301 Moved Permanently +Server: nginx/1.30.3 +Date: Thu, 09 Jul 2026 23:15:52 GMT +Content-Type: text/html +Content-Length: 169 +Connection: keep-alive +Location: https://localhost/ +``` + +### B. TLS 1.3 proof +``` +Connecting to ::1 +Can't use SSL_get_servername +depth=0 CN=juice.local +verify error:num=18:self-signed certificate +CONNECTION ESTABLISHED +Protocol version: TLSv1.3 +Ciphersuite: TLS_AES_256_GCM_SHA384 +Peer certificate: CN=juice.local +``` + +### C. Security headers proof (all 6 present) +``` +HTTP/1.1 200 OK +Server: nginx/1.30.3 +Date: Thu, 09 Jul 2026 23:16:05 GMT +Content-Type: text/html; charset=UTF-8 +Content-Length: 9903 +Connection: keep-alive +Access-Control-Allow-Origin: * +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +Feature-Policy: payment 'self' +X-Recruiting: /#/jobs +Accept-Ranges: bytes +Cache-Control: public, max-age=0 +Last-Modified: Thu, 09 Jul 2026 23:12:20 GMT +ETag: W/"26af-19f49274cfa" +Vary: Accept-Encoding +Strict-Transport-Security: max-age=63072000; includeSubDomains; preload +X-Content-Type-Options: nosniff +X-Frame-Options: DENY +Referrer-Policy: strict-origin-when-cross-origin +Permissions-Policy: camera=(), microphone=(), geolocation=() +Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; +``` + +### What each header defends against (1 sentence each) +- HSTS: Forces browsers to interact with the site only over secure HTTPS connections, preventing downgrade attacks and cookie hijacking. +- X-Content-Type-Options: nosniff: Prevents the browser from trying to MIME-sniff the content type, mitigating drive-by download attacks and certain types of XSS. +- X-Frame-Options: DENY: Prevents the site from being rendered within an iframe, effectively protecting the application from clickjacking attacks. +- Referrer-Policy: Controls how much referrer information is passed to other sites, protecting user privacy and preventing accidental leakage of sensitive tokens in URLs. +- Permissions-Policy: Restricts the APIs and browser features (like the camera or geolocation) that the web application is allowed to access, reducing the overall attack surface. +- Content-Security-Policy: Mitigates Cross-Site Scripting (XSS) and data injection attacks by explicitly defining which dynamic resources and scripts are trusted to load and execute. + +## Task 2: Production Posture + +### Rate limit proof +| HTTP code | Count out of 60 | +|-----------|----------------:| +| 200 | 0 | +| 429 | 58 | +| 5xx | 2 | +*(Note: The 500 errors occur because the allowed burst requests bypass the rate limit and reach Juice Shop's login endpoint as malformed GET requests).* + +### Timeout enforced +``` +(Empty output) +``` +# Explanation: Sending plaintext HTTP to the HTTPS port causes Nginx to wait for a TLS ClientHello. After `client_header_timeout` (10s), Nginx silently drops the connection. `nc` exits with no output, proving the fail-closed timeout works. + + +### Cipher hardening +``` +New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 +``` + +### Cert rotation runbook (7 steps) +1. **Detect expiry**: Set up automated monitoring (e.g., Prometheus, Datadog, or a simple cron script using `openssl`) to alert the operations team 30 days before certificate expiration. +2. **Order new cert**: Generate a new Certificate Signing Request (CSR) and submit it to the Certificate Authority (CA), or use an automated ACME client like Certbot to fetch the new certificate. +3. **Validate**: Verify the new certificate matches the private key using `openssl x509 -noout -modulus` and ensure the CA chain of trust is intact. +4. **Atomic swap**: Place the new certificate and key files on the server alongside the old ones, update the Nginx configuration to point to the new files, test syntax with `nginx -t`, and perform a graceful reload using `nginx -s reload`. +5. **Verify**: Use an external tool like `testssl.sh` or `curl -vI` to confirm the server is actively presenting the new certificate with the updated expiration date. +6. **Rollback plan**: If validation fails or production issues occur, immediately revert the file paths in `nginx.conf` to the previous valid certificate and issue another `nginx -s reload`. +7. **Audit**: Log the rotation event in the security audit trail, update internal tracking dashboards, and mark the initial expiration monitoring alert as resolved. + +### What OCSP stapling buys you (2-3 sentences, reference Reading 11) +OCSP stapling improves both performance and user privacy by having the web server proactively query the CA for revocation status and "staple" this signed response directly into the TLS handshake. This prevents the client's browser from having to make a separate, blocking HTTP request to the CA, which speeds up connection times and stops the CA from tracking which sites users are visiting. OCSP stapling requires a real Certificate Authority running an active OCSP responder URL to query; a self-signed lab certificate lacks a legitimate CA infrastructure, so there is no responder for Nginx to contact. + +## Bonus: WAF Sidecar with OWASP CRS + +### Setup choice +- WAF used: ModSecurity v3 (via official `owasp/modsecurity-crs` Docker image) +- OWASP CRS version: 4.x +- Paranoia level: 1 + +### Attack payload sent +`GET /rest/products/search?q=' OR 1=1--` (URL-encoded) + +### Before WAF (Nginx alone) +``` +no-waf: HTTP 500 +``` +# Explanation: Nginx blindly proxied the SQLi payload to the backend. The Juice Shop database choked on the syntax, resulting in a 500 Internal Server Error. + +### After WAF +``` +with-waf: HTTP 403 +``` + +### Audit log excerpt (the rule that fired) +``` +[client 172.18.0.1] ModSecurity: Warning. Pattern match "(?i)([\\'\\\"\\`\\´\\’\\‘]\\s*?(?:and|or|div|xor|between|like|rlike|regexp|is)\\s+[^\\s]+.*)" at ARGS:q. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf"] [line "45"] [id "942100"] [msg "SQL Injection Attack: Common Injection Testing"] [severity "CRITICAL"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-sqli"] [tag "OWASP_CRS"] +``` +Rule ID: **942100** — OWASP CRS rule name: **SQL Injection Attack: Common Injection Testing** + +### Tradeoff analysis (3 sentences) +What does the WAF buy you? A WAF provides critical runtime defense ("virtual patching") that actively blocks malicious payloads and zero-day exploits in production, catching attacks that might have slipped past earlier SAST/DAST scans or logic tests. +What does it COST you? It introduces significant operational overhead (tuning rules to prevent False Positives that block legitimate users), increases request latency, and adds complexity to the infrastructure stack. +When would you NOT deploy a WAF? You would generally avoid putting a WAF in front of internal, backend-to-backend microservices secured by mutual TLS (where trust is already established), or on highly latency-sensitive data-streaming endpoints where the performance penalty outweighs the security benefit.