From 3432eef3fdc7adf89b328c234dd226bd727cdcf0 Mon Sep 17 00:00:00 2001 From: Islam Date: Mon, 6 Jul 2026 21:15:23 +0300 Subject: [PATCH] lab11: hardened nginx reverse proxy and waf --- .gitignore | 2 + labs/lab11/reverse-proxy/nginx.conf | 18 +- labs/lab11/waf/docker-compose.override.yml | 27 +++ submissions/lab11.md | 196 +++++++++++++++++++++ 4 files changed, 237 insertions(+), 6 deletions(-) create mode 100644 labs/lab11/waf/docker-compose.override.yml create mode 100644 submissions/lab11.md diff --git a/.gitignore b/.gitignore index c6b279fa..4c9b434b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,9 @@ labs/lab9/falco/logs/ labs/lab10/work/dd/ labs/lab10/imports/import-*.json labs/lab11/logs/ +labs/lab11/results/ labs/lab11/reverse-proxy/certs/ +labs/lab11/waf/logs/ labs/lab12/results/ # Standard diff --git a/labs/lab11/reverse-proxy/nginx.conf b/labs/lab11/reverse-proxy/nginx.conf index dff91b26..e1a9bb66 100644 --- a/labs/lab11/reverse-proxy/nginx.conf +++ b/labs/lab11/reverse-proxy/nginx.conf @@ -29,6 +29,7 @@ http { # 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_conn_zone $binary_remote_addr zone=conn:10m; limit_req_status 429; map $http_upgrade $connection_upgrade { default upgrade; '' close; } @@ -85,11 +86,15 @@ 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_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + # Nginx applies ssl_ciphers only to TLS <= 1.2; TLS 1.3 suites use OpenSSL conf commands. + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256; + ssl_ecdh_curve X25519:secp384r1; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + ssl_session_tickets off; ssl_stapling off; # If using a publicly-trusted certificate, you may enable OCSP stapling: # ssl_stapling on; @@ -103,9 +108,10 @@ http { client_header_timeout 10s; keepalive_timeout 10s; send_timeout 10s; + limit_conn conn 50; # 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/labs/lab11/waf/docker-compose.override.yml b/labs/lab11/waf/docker-compose.override.yml new file mode 100644 index 00000000..fe985cff --- /dev/null +++ b/labs/lab11/waf/docker-compose.override.yml @@ -0,0 +1,27 @@ +services: + waf: + image: owasp/modsecurity-crs:4.28.0-nginx-alpine-202607051007 + restart: unless-stopped + depends_on: + - nginx + ports: + - "8080:8080" + - "8443:8443" + environment: + SERVER_NAME: localhost + BACKEND: "http://nginx:80" + REQ_HEADER_FORWARDED_PROTO: "https" + NGINX_ALWAYS_TLS_REDIRECT: "off" + MODSEC_RULE_ENGINE: "On" + MODSEC_AUDIT_ENGINE: "RelevantOnly" + MODSEC_AUDIT_LOG: "/var/log/modsec/audit.log" + MODSEC_AUDIT_LOG_FORMAT: "Native" + MODSEC_AUDIT_LOG_PARTS: "ABIJDEFHZ" + BLOCKING_PARANOIA: "1" + DETECTION_PARANOIA: "1" + ANOMALY_INBOUND: "5" + ANOMALY_OUTBOUND: "4" + ALLOWED_METHODS: "GET HEAD POST OPTIONS" + ALLOWED_REQUEST_CONTENT_TYPE: "|application/x-www-form-urlencoded| |multipart/form-data| |application/json| |text/plain|" + volumes: + - ./waf/logs:/var/log/modsec:rw diff --git a/submissions/lab11.md b/submissions/lab11.md new file mode 100644 index 00000000..e6be82ef --- /dev/null +++ b/submissions/lab11.md @@ -0,0 +1,196 @@ +# Lab 11 - BONUS - Submission + +## Task 1: TLS + Security Headers + +### nginx.conf (SSL + header sections) + +```nginx +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; +} + +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; + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256: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-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; +} +``` + +Note: Nginx applies `ssl_ciphers` to TLS 1.2 and below. For TLS 1.3, the working configuration uses OpenSSL's `ssl_conf_command Ciphersuites ...`; the live proof below shows the negotiated TLS 1.3 suite. + +### A. HTTPS redirect proof + +```text +HTTP/1.1 308 Permanent Redirect +Server: nginx +Date: Mon, 06 Jul 2026 18:08:06 GMT +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 + +```text +CONNECTION ESTABLISHED +Protocol version: TLSv1.3 +Ciphersuite: TLS_AES_256_GCM_SHA384 +Peer certificate: CN=juice.local +Hash used: SHA256 +Signature type: rsa_pss_rsae_sha256 +Verification error: self-signed certificate +Peer Temp Key: X25519, 253 bits +DONE +``` + +### C. Security headers proof + +```text +HTTP/2 200 +server: nginx +date: Mon, 06 Jul 2026 18:08:06 GMT +content-type: text/html; charset=UTF-8 +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' +``` + +### What each header defends against + +- HSTS: forces future browser requests to use HTTPS, reducing SSL stripping and accidental plaintext downgrade risk. +- X-Content-Type-Options: nosniff: prevents browsers from interpreting a response as a different content type than the server declared. +- X-Frame-Options: DENY: blocks framing of the application and reduces clickjacking exposure. +- Referrer-Policy: limits how much URL context leaks to other origins through the `Referer` header. +- Permissions-Policy: denies browser access to camera, microphone, and geolocation APIs unless explicitly re-enabled. +- Content-Security-Policy: reports script, style, image, and origin violations before enforcing a strict policy that might break Juice Shop. + +## Task 2: Production Posture + +### Rate limit proof + +| HTTP code | Count out of 60 | +|-----------|----------------:| +| 401 | 6 | +| 429 | 54 | +| 5xx | 0 | + +### Timeout enforced + +```text +Partial HTTP request closed after 12 seconds; response bytes: 0 +``` + +### Cipher hardening + +```text +Protocol version: TLSv1.3 +Ciphersuite: TLS_AES_256_GCM_SHA384 +Peer Temp Key: X25519, 253 bits +``` + +### Cert rotation runbook (7 steps) + +1. **Detect expiry**: monitor certificate expiry with `openssl x509 -checkend`, synthetic HTTPS checks, and alert at 30/14/7 days before expiry. +2. **Order new cert**: issue the replacement through ACME or the production CA using the existing SAN list and key policy. +3. **Validate**: verify chain, SANs, key type, expiry, and private-key match in staging before touching production. +4. **Atomic swap**: write the new cert and key to versioned files, update the symlink atomically, and run `nginx -t`. +5. **Verify**: reload Nginx, then confirm TLS 1.3, chain validity, HSTS, and expected cert fingerprint from an external client. +6. **Rollback plan**: keep the previous cert/key pair and symlink target available so a bad deploy can be reverted and reloaded quickly. +7. **Audit**: record issuer, serial, fingerprint, operator, time, validation output, and rollback status in the change record. + +### What OCSP stapling buys you + +OCSP stapling lets Nginx fetch and staple the CA's revocation status so clients can validate revocation without calling the CA directly on every connection. That improves privacy, latency, and reliability for public certificates. It does not help this lab's self-signed certificate because there is no trusted issuer OCSP responder or real revocation chain to staple. + +## Bonus: WAF Sidecar with OWASP CRS + +### Setup choice + +- WAF used: ModSecurity v3 with the official OWASP CRS Nginx image +- Image: `owasp/modsecurity-crs:4.28.0-nginx-alpine-202607051007` +- OWASP CRS version: `4.28.0` +- Paranoia level: 1 +- Audit log: `/var/log/modsec/audit.log` + +### Attack payload sent + +`GET /rest/products/search?q=' OR 1=1--` + +### Before WAF (Nginx alone) + +```text +no-waf: HTTP 500 +``` + +This reached Juice Shop through Nginx and was not blocked by the reverse proxy. The application returned an error for the deliberately hostile query. + +### After WAF + +```text +with-waf: HTTP 403 +``` + +### Audit log excerpt + +```text +GET /rest/products/search?q='%20OR%201=1-- HTTP/2.0 +host: localhost:8443 + +HTTP/2.0 403 +Server: nginx + +ModSecurity: Warning. detected SQLi using libinjection. [file "/etc/modsecurity.d/owasp-crs/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf"] [line "46"] [id "942100"] [msg "SQL Injection Attack Detected via libinjection"] [data "Matched Data: s&1c found within ARGS:q: ' OR 1=1--"] [severity "2"] [ver "OWASP_CRS/4.28.0"] [tag "attack-sqli"] [tag "paranoia-level/1"] [tag "OWASP_CRS/ATTACK-SQLI"] +ModSecurity: Access denied with code 403 (phase 2). [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [ver "OWASP_CRS/4.28.0"] +``` + +Rule ID: **942100** - OWASP CRS rule name: **SQL Injection Attack Detected via libinjection**. + +### Tradeoff analysis + +The WAF buys a runtime compensating control: it can block whole attack classes such as SQLi probes even when SAST, DAST, or policy gates missed a specific route or payload. The cost is operational complexity: another TLS endpoint, more logs to tune, possible false positives, and rule updates that need change control. I would avoid putting this WAF in front of a small internal single-tenant service with low exposure and strong authenticated inputs; the operational cost would likely exceed the risk reduction.