From a988ae468ac9f20dcb970f4f2ada2600766c5186 Mon Sep 17 00:00:00 2001 From: SNeka Date: Thu, 16 Jul 2026 16:57:37 +0300 Subject: [PATCH] feat(lab11): hardened nginx + WAF sidecar --- labs/lab11/reverse-proxy/nginx.conf | 47 ++++--- submissions/lab11.md | 188 ++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+), 15 deletions(-) create mode 100644 submissions/lab11.md diff --git a/labs/lab11/reverse-proxy/nginx.conf b/labs/lab11/reverse-proxy/nginx.conf index dff91b265..8e3c2f91a 100644 --- a/labs/lab11/reverse-proxy/nginx.conf +++ b/labs/lab11/reverse-proxy/nginx.conf @@ -26,11 +26,13 @@ http { keepalive 32; } - # Rate limit zone for login - # ~10 req/min per IP, burst of 5 + # Rate limit zone for login — ~10 req/min per IP, burst of 5 limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m; limit_req_status 429; + # Connection limit — max 50 concurrent connections per IP (Task 2) + limit_conn_zone $binary_remote_addr zone=conn:10m; + map $http_upgrade $connection_upgrade { default upgrade; '' close; } # Common proxy settings @@ -85,18 +87,33 @@ http { 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; + + # --- TLS 1.3 only (Task 1) --- + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; # TLS 1.3 ignores this anyway; explicit for clarity + + # --- Cipher hardening (Task 2, Mozilla Modern) --- + # NOTE: in nginx, `ssl_ciphers` only controls TLS <=1.2 suites. TLS 1.3 cipher + # suites MUST be set via `ssl_conf_command Ciphersuites` (OpenSSL 1.1.1+), otherwise + # a TLS1.3-only server with TLS1.3 names in ssl_ciphers fails with "no cipher match". + ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256; + ssl_ecdh_curve X25519:secp384r1; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + ssl_session_tickets off; + + # --- OCSP stapling (Task 2) — documentation-only for this self-signed lab cert. + # A self-signed cert has no OCSP responder URL and no CA chain, so nginx can never + # actually fetch/staple a real OCSP response here; these directives are inert in + # this lab and only demonstrate the production config (see submission for why). + ssl_stapling on; + ssl_stapling_verify on; + resolver 8.8.8.8 1.1.1.1 valid=300s; + resolver_timeout 5s; + # ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; # needs a real CA chain + + # --- Connection limit (Task 2) --- + limit_conn conn 50; client_max_body_size 2m; client_body_timeout 10s; @@ -105,7 +122,7 @@ http { send_timeout 10s; # 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; diff --git a/submissions/lab11.md b/submissions/lab11.md new file mode 100644 index 000000000..3069cccd0 --- /dev/null +++ b/submissions/lab11.md @@ -0,0 +1,188 @@ +# Lab 11 — BONUS — Submission + +## Task 1: TLS + Security Headers + +### nginx.conf (SSL + header sections) +```nginx +# HTTP server — redirect everything to HTTPS with a 308, carrying the header set +server { + listen 80; + listen [::]:80; + server_name _; + 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; +} + +# HTTPS server — TLS 1.3 only + full header set +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name _; + + ssl_certificate /etc/nginx/certs/localhost.crt; + ssl_certificate_key /etc/nginx/certs/localhost.key; + + # TLS 1.3 only + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; # TLS 1.3 ignores this anyway; explicit for clarity + + 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; + 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; + ... +} +``` + +### A. HTTPS redirect proof +``` +HTTP/1.1 308 Permanent Redirect +Server: nginx +Content-Type: text/html +Content-Length: 164 +Connection: keep-alive +Location: https://localhost/ +X-Frame-Options: DENY +X-Content-Type-Options: nosniff +Referrer-Policy: strict-origin-when-cross-origin +Permissions-Policy: camera=(), geolocation=(), microphone=() +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Resource-Policy: same-origin +Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' +``` + +### B. TLS 1.3 proof +``` +CONNECTION ESTABLISHED +Protocol version: TLSv1.3 +Ciphersuite: TLS_AES_256_GCM_SHA384 +Peer certificate: CN=juice.local +Hash used: SHA256 +Signature type: RSA-PSS +``` +(The `verify error:num=18:self-signed certificate` line is expected — the lab cert is self-signed, so `-k`/insecure is used for all client probes.) + +### C. Security headers proof (all 6 present) +``` +HTTP/1.1 200 OK +Server: nginx +... +Strict-Transport-Security: max-age=63072000; includeSubDomains; preload +X-Frame-Options: DENY +X-Content-Type-Options: nosniff +Referrer-Policy: strict-origin-when-cross-origin +Permissions-Policy: camera=(), geolocation=(), microphone=() +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Resource-Policy: same-origin +Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' +``` +All 6 required headers land on the real 200 response: HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, and CSP (Report-Only). The upstream's own weaker headers (`Feature-Policy: payment 'self'`) are stripped/overridden at the proxy via the `proxy_hide_header` block. + +### What each header defends against +- **HSTS** (`Strict-Transport-Security`): forces the browser to use HTTPS for every future request to this host for 2 years, so an attacker can't strip TLS with an SSL-downgrade/MITM on the first plaintext hop. +- **X-Content-Type-Options: nosniff**: stops the browser from MIME-sniffing a response into an executable type, so a user-uploaded "image" that's actually JavaScript won't be run as script. +- **X-Frame-Options: DENY**: forbids the page from being embedded in any `