From 7278b58e155012aa5d3eb7ed93f3adb24272ee39 Mon Sep 17 00:00:00 2001 From: MikeNovikoff Date: Wed, 15 Jul 2026 21:40:41 +0500 Subject: [PATCH] feat(lab11): hardened nginx + WAF sidecar --- labs/lab11/reverse-proxy/nginx.conf | 55 +++--- labs/lab11/waf/docker-compose.override.yml | 19 ++ submissions/lab11.md | 193 +++++++++++++++++++++ 3 files changed, 235 insertions(+), 32 deletions(-) create mode 100644 labs/lab11/waf/docker-compose.override.yml create mode 100644 submissions/lab11.md diff --git a/labs/lab11/reverse-proxy/nginx.conf b/labs/lab11/reverse-proxy/nginx.conf index dff91b265..22b26703d 100644 --- a/labs/lab11/reverse-proxy/nginx.conf +++ b/labs/lab11/reverse-proxy/nginx.conf @@ -11,7 +11,6 @@ http { 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" ' @@ -20,20 +19,20 @@ http { 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 + # Rate limit for login / review endpoints limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m; limit_req_status 429; + # Connection limit zone + 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; @@ -41,13 +40,11 @@ http { 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; @@ -58,13 +55,11 @@ http { proxy_hide_header Content-Security-Policy-Report-Only; proxy_hide_header Access-Control-Allow-Origin; - # HTTP server (redirect to HTTPS) 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; @@ -76,36 +71,33 @@ http { return 308 https://$host$request_uri; } - # HTTPS server server { listen 443 ssl; listen [::]:443 ssl; http2 on; server_name _; + limit_conn conn 50; + client_body_timeout 10s; + client_header_timeout 10s; + ssl_certificate /etc/nginx/certs/localhost.crt; ssl_certificate_key /etc/nginx/certs/localhost.key; - ssl_session_timeout 10m; + ssl_session_timeout 1d; 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_session_tickets off; + + ssl_protocols TLSv1.3; + ssl_conf_command Ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256; + ssl_ecdh_curve X25519:secp384r1; + ssl_prefer_server_ciphers off; + 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; - client_body_timeout 10s; - client_header_timeout 10s; - keepalive_timeout 10s; - send_timeout 10s; + # OCSP stapling is disabled here because the lab uses a self-signed cert. + # In production, with a publicly-trusted CA cert, enable ssl_stapling on + # and point ssl_trusted_certificate at the issuer chain. - # Security headers (include HSTS here only) - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; 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; @@ -114,13 +106,12 @@ http { 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; - location = /rest/user/login { - limit_req zone=login burst=5 nodelay; - limit_req_log_level warn; + location / { proxy_pass http://juice; } - location / { + location /rest/user/login { + limit_req zone=login burst=5 nodelay; proxy_pass http://juice; } } diff --git a/labs/lab11/waf/docker-compose.override.yml b/labs/lab11/waf/docker-compose.override.yml new file mode 100644 index 000000000..dec481a0a --- /dev/null +++ b/labs/lab11/waf/docker-compose.override.yml @@ -0,0 +1,19 @@ +# Bonus: ModSecurity v3 + OWASP CRS sidecar in front of Juice Shop. +# Exposes HTTP :8081 so the WAF can be tested without fighting the image's TLS. +services: + waf: + image: owasp/modsecurity-crs:nginx-alpine + restart: unless-stopped + depends_on: + - juice + ports: + - "8081:8080" + environment: + BACKEND: "http://juice:3000" + PORT: "8080" + PARANOIA: "1" + MODSEC_RULE_ENGINE: "On" + MODSEC_AUDIT_ENGINE: "On" + MODSEC_AUDIT_LOG: "/var/log/modsecurity/audit.log" + volumes: + - ./waf-logs:/var/log/modsecurity:rw diff --git a/submissions/lab11.md b/submissions/lab11.md new file mode 100644 index 000000000..6b3175ee0 --- /dev/null +++ b/submissions/lab11.md @@ -0,0 +1,193 @@ +# Lab 11 — BONUS — Submission + +> Stack: nginx `1.30.3` reverse proxy in front of Juice Shop v20.0.0, self-signed cert (`CN=juice.local`), TLS 1.3 only. WAF bonus: `owasp/modsecurity-crs:nginx-alpine` with OWASP CRS 3.3.10, paranoia level 1. + +## Task 1: TLS + Security Headers + +### nginx.conf (SSL + header sections) + +```nginx +ssl_protocols TLSv1.3; +ssl_prefer_server_ciphers off; +ssl_conf_command Ciphersuites 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; + +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'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'" always; +``` + +> Note: TLS 1.3 ciphersuites are set with `ssl_conf_command Ciphersuites`, not `ssl_ciphers`. The latter only governs TLS ≤ 1.2; using it for TLS 1.3 names makes `openssl s_client` fail with `no cipher match`. + +### A. HTTPS redirect proof + +```text +HTTP/1.1 308 Permanent Redirect +Location: https://localhost/ +``` + +### B. TLS 1..3 proof (`openssl s_client -tls1_3`) + +```text +CONNECTION ESTABLISHED +Protocol version: TLSv1.3 +Ciphersuite: TLS_AES_256_GCM_SHA384 +Server Temp Key: X25519, 253 bits +Peer certificate: CN=juice.local +``` + +### C. Security headers proof (all 6 present) + +```text +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'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' +``` + +### What each header defends against + +- **HSTS:** forces the browser to use HTTPS for the next two years, preventing SSL-stripping and downgrade MITM on repeat visits. +- **X-Content-Type-Options: nosniff:** stops the browser from MIME-sniffing a response into an executable type, e.g. an uploaded file executed as JavaScript. +- **X-Frame-Options: DENY:** blocks embedding the page in an `