diff --git a/.gitignore b/.gitignore index c6b279fa..92ae0e5e 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ node_modules/ .venv/ .DS_Store *.swp +labs/lab11/waf/waf-logs/ diff --git a/labs/lab11/results/cipher.txt b/labs/lab11/results/cipher.txt new file mode 100644 index 00000000..4c46aafd --- /dev/null +++ b/labs/lab11/results/cipher.txt @@ -0,0 +1,3 @@ +New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 + Cipher : TLS_AES_256_GCM_SHA384 + Cipher : TLS_AES_256_GCM_SHA384 diff --git a/labs/lab11/results/headers.txt b/labs/lab11/results/headers.txt new file mode 100644 index 00000000..424d519e --- /dev/null +++ b/labs/lab11/results/headers.txt @@ -0,0 +1,22 @@ +HTTP/1.1 200 OK +Server: nginx +Date: Fri, 10 Jul 2026 08:42:04 GMT +Content-Type: text/html; charset=UTF-8 +Content-Length: 9903 +Connection: keep-alive +Feature-Policy: payment 'self' +X-Recruiting: /#/jobs +Accept-Ranges: bytes +Cache-Control: public, max-age=0 +Last-Modified: Fri, 10 Jul 2026 08:38:56 GMT +ETag: W/"26af-19f4b2e096b" +Vary: Accept-Encoding +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' + diff --git a/labs/lab11/results/http-redirect.txt b/labs/lab11/results/http-redirect.txt new file mode 100644 index 00000000..e9fb8590 --- /dev/null +++ b/labs/lab11/results/http-redirect.txt @@ -0,0 +1,15 @@ +HTTP/1.1 308 Permanent Redirect +Server: nginx +Date: Fri, 10 Jul 2026 08:41:37 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' + diff --git a/labs/lab11/results/ratelimit.txt b/labs/lab11/results/ratelimit.txt new file mode 100644 index 00000000..48f12840 --- /dev/null +++ b/labs/lab11/results/ratelimit.txt @@ -0,0 +1,2 @@ + 54 429 + 6 500 diff --git a/labs/lab11/results/timeout.txt b/labs/lab11/results/timeout.txt new file mode 100644 index 00000000..bd8ea16e --- /dev/null +++ b/labs/lab11/results/timeout.txt @@ -0,0 +1,8 @@ +Connecting to ::1 +Can't use SSL_get_servername +depth=0 CN=juice.local +verify error:num=18:self-signed certificate +verify return:1 +depth=0 CN=juice.local +verify return:1 +A8330000:error:0A000126:SSL routines::unexpected eof while reading:../openssl-3.5.5/ssl/record/rec_layer_s3.c:698: diff --git a/labs/lab11/results/tls13.txt b/labs/lab11/results/tls13.txt new file mode 100644 index 00000000..3d0b7eb0 --- /dev/null +++ b/labs/lab11/results/tls13.txt @@ -0,0 +1,8 @@ +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 diff --git a/labs/lab11/results/waf-audit.txt b/labs/lab11/results/waf-audit.txt new file mode 100644 index 00000000..e8980f0b --- /dev/null +++ b/labs/lab11/results/waf-audit.txt @@ -0,0 +1,9 @@ +{"transaction":{"client_ip":"127.0.0.1","time_stamp":"Fri Jul 10 09:00:26 2026","server_id":"c96835f14eb6f49b91a82df9e69c29ce2a1206a4","client_port":47222,"host_ip":"127.0.0.1","host_port":8443,"unique_id":"178367402620.995940","is_interrupted":false,"request":{"method":"GET","http_version":"2.0","hostname":"localhost","uri":"/healthz","headers":{"user-agent":"healthcheck","accept":"*/*","host":"localhost:8443"}},"response":{"body":"OK","http_code":200,"headers":{"Server":"nginx\u0000","Date":"Fri, 10 Jul 2026 09:00:26 GMT","Content-Length":"2","Content-Type":"application/octet-stream","Content-Type":"text/plain","Connection":"close"}},"producer":{"modsecurity":"ModSecurity v3.0.16 (Linux)","connector":"ModSecurity-nginx v1.0.4","secrules_engine":"Enabled","components":["OWASP_CRS/3.3.10\""]},"messages":[]}} +{"transaction":{"client_ip":"127.0.0.1","time_stamp":"Fri Jul 10 09:00:56 2026","server_id":"c96835f14eb6f49b91a82df9e69c29ce2a1206a4","client_port":36772,"host_ip":"127.0.0.1","host_port":8443,"unique_id":"178367405662.529850","is_interrupted":false,"request":{"method":"GET","http_version":"2.0","hostname":"localhost","uri":"/healthz","headers":{"user-agent":"healthcheck","accept":"*/*","host":"localhost:8443"}},"response":{"body":"OK","http_code":200,"headers":{"Server":"nginx\u0000","Date":"Fri, 10 Jul 2026 09:00:56 GMT","Content-Length":"2","Content-Type":"application/octet-stream","Content-Type":"text/plain","Connection":"close"}},"producer":{"modsecurity":"ModSecurity v3.0.16 (Linux)","connector":"ModSecurity-nginx v1.0.4","secrules_engine":"Enabled","components":["OWASP_CRS/3.3.10\""]},"messages":[]}} +{"transaction":{"client_ip":"127.0.0.1","time_stamp":"Fri Jul 10 09:01:27 2026","server_id":"c96835f14eb6f49b91a82df9e69c29ce2a1206a4","client_port":47686,"host_ip":"127.0.0.1","host_port":8443,"unique_id":"178367408768.607175","is_interrupted":false,"request":{"method":"GET","http_version":"2.0","hostname":"localhost","uri":"/healthz","headers":{"user-agent":"healthcheck","accept":"*/*","host":"localhost:8443"}},"response":{"body":"OK","http_code":200,"headers":{"Server":"nginx\u0000","Date":"Fri, 10 Jul 2026 09:01:27 GMT","Content-Length":"2","Content-Type":"application/octet-stream","Content-Type":"text/plain","Connection":"close"}},"producer":{"modsecurity":"ModSecurity v3.0.16 (Linux)","connector":"ModSecurity-nginx v1.0.4","secrules_engine":"Enabled","components":["OWASP_CRS/3.3.10\""]},"messages":[]}} +{"transaction":{"client_ip":"172.20.0.1","time_stamp":"Fri Jul 10 09:01:43 2026","server_id":"c96835f14eb6f49b91a82df9e69c29ce2a1206a4","client_port":46280,"host_ip":"172.20.0.4","host_port":8080,"unique_id":"178367410380.402071","is_interrupted":true,"request":{"method":"GET","http_version":"1.1","hostname":"localhost","uri":"/rest/products/search?q='%20OR%201=1--","headers":{"Host":"localhost:8081","User-Agent":"curl/8.18.0","Accept":"*/*"}},"response":{"body":"\r\n403 Forbidden\r\n\r\n

403 Forbidden

\r\n
nginx
\r\n\r\n\r\n","http_code":403,"headers":{"Server":"nginx\u0000","Date":"Fri, 10 Jul 2026 09:01:43 GMT","Content-Length":"146","Content-Type":"text/plain","Access-Control-Allow-Origin":"*","Connection":"keep-alive","Access-Control-Max-Age":"3600","Access-Control-Allow-Methods":"GET, POST, PUT, DELETE, OPTIONS","Access-Control-Allow-Headers":"*"}},"producer":{"modsecurity":"ModSecurity v3.0.16 (Linux)","connector":"ModSecurity-nginx v1.0.4","secrules_engine":"Enabled","components":["OWASP_CRS/3.3.10\""]},"messages":[{"message":"SQL Injection Attack Detected via libinjection","details":{"match":"detected SQLi using libinjection.","reference":"v28,10","ruleId":"942100","file":"/etc/modsecurity.d/owasp-crs/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf","lineNumber":"46","data":"Matched Data: s&1c found within ARGS:q: ' OR 1=1--","severity":"2","ver":"OWASP_CRS/3.3.10","rev":"","tags":["application-multi","language-multi","platform-multi","attack-sqli","paranoia-level/1","OWASP_CRS","capec/1000/152/248/66","PCI/6.5.2"],"maturity":"0","accuracy":"0"}},{"message":"Inbound Anomaly Score Exceeded (Total Score: 5)","details":{"match":"Matched \"Operator `Ge' with parameter `5' against variable `TX:ANOMALY_SCORE' (Value: `5' )","reference":"","ruleId":"949110","file":"/etc/modsecurity.d/owasp-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf","lineNumber":"81","data":"","severity":"2","ver":"OWASP_CRS/3.3.10","rev":"","tags":["modsecurity","application-multi","language-multi","platform-multi","attack-generic"],"maturity":"0","accuracy":"0"}}]}} +{"transaction":{"client_ip":"127.0.0.1","time_stamp":"Fri Jul 10 09:01:57 2026","server_id":"c96835f14eb6f49b91a82df9e69c29ce2a1206a4","client_port":50166,"host_ip":"127.0.0.1","host_port":8443,"unique_id":"178367411746.611460","is_interrupted":false,"request":{"method":"GET","http_version":"2.0","hostname":"localhost","uri":"/healthz","headers":{"user-agent":"healthcheck","accept":"*/*","host":"localhost:8443"}},"response":{"body":"OK","http_code":200,"headers":{"Server":"nginx\u0000","Date":"Fri, 10 Jul 2026 09:01:57 GMT","Content-Length":"2","Content-Type":"application/octet-stream","Content-Type":"text/plain","Connection":"close"}},"producer":{"modsecurity":"ModSecurity v3.0.16 (Linux)","connector":"ModSecurity-nginx v1.0.4","secrules_engine":"Enabled","components":["OWASP_CRS/3.3.10\""]},"messages":[]}} +{"transaction":{"client_ip":"127.0.0.1","time_stamp":"Fri Jul 10 09:02:27 2026","server_id":"c96835f14eb6f49b91a82df9e69c29ce2a1206a4","client_port":42676,"host_ip":"127.0.0.1","host_port":8443,"unique_id":"178367414721.647263","is_interrupted":false,"request":{"method":"GET","http_version":"2.0","hostname":"localhost","uri":"/healthz","headers":{"user-agent":"healthcheck","accept":"*/*","host":"localhost:8443"}},"response":{"body":"OK","http_code":200,"headers":{"Server":"nginx\u0000","Date":"Fri, 10 Jul 2026 09:02:27 GMT","Content-Length":"2","Content-Type":"application/octet-stream","Content-Type":"text/plain","Connection":"close"}},"producer":{"modsecurity":"ModSecurity v3.0.16 (Linux)","connector":"ModSecurity-nginx v1.0.4","secrules_engine":"Enabled","components":["OWASP_CRS/3.3.10\""]},"messages":[]}} +{"transaction":{"client_ip":"127.0.0.1","time_stamp":"Fri Jul 10 09:02:57 2026","server_id":"c96835f14eb6f49b91a82df9e69c29ce2a1206a4","client_port":46892,"host_ip":"127.0.0.1","host_port":8443,"unique_id":"178367417732.279455","is_interrupted":false,"request":{"method":"GET","http_version":"2.0","hostname":"localhost","uri":"/healthz","headers":{"user-agent":"healthcheck","accept":"*/*","host":"localhost:8443"}},"response":{"body":"OK","http_code":200,"headers":{"Server":"nginx\u0000","Date":"Fri, 10 Jul 2026 09:02:57 GMT","Content-Length":"2","Content-Type":"application/octet-stream","Content-Type":"text/plain","Connection":"close"}},"producer":{"modsecurity":"ModSecurity v3.0.16 (Linux)","connector":"ModSecurity-nginx v1.0.4","secrules_engine":"Enabled","components":["OWASP_CRS/3.3.10\""]},"messages":[]}} +{"transaction":{"client_ip":"127.0.0.1","time_stamp":"Fri Jul 10 09:03:27 2026","server_id":"c96835f14eb6f49b91a82df9e69c29ce2a1206a4","client_port":46066,"host_ip":"127.0.0.1","host_port":8443,"unique_id":"17836742074.571046","is_interrupted":false,"request":{"method":"GET","http_version":"2.0","hostname":"localhost","uri":"/healthz","headers":{"user-agent":"healthcheck","accept":"*/*","host":"localhost:8443"}},"response":{"body":"OK","http_code":200,"headers":{"Server":"nginx\u0000","Date":"Fri, 10 Jul 2026 09:03:27 GMT","Content-Length":"2","Content-Type":"application/octet-stream","Content-Type":"text/plain","Connection":"close"}},"producer":{"modsecurity":"ModSecurity v3.0.16 (Linux)","connector":"ModSecurity-nginx v1.0.4","secrules_engine":"Enabled","components":["OWASP_CRS/3.3.10\""]},"messages":[]}} +{"transaction":{"client_ip":"127.0.0.1","time_stamp":"Fri Jul 10 09:03:57 2026","server_id":"c96835f14eb6f49b91a82df9e69c29ce2a1206a4","client_port":39670,"host_ip":"127.0.0.1","host_port":8443,"unique_id":"178367423788.827231","is_interrupted":false,"request":{"method":"GET","http_version":"2.0","hostname":"localhost","uri":"/healthz","headers":{"user-agent":"healthcheck","accept":"*/*","host":"localhost:8443"}},"response":{"body":"OK","http_code":200,"headers":{"Server":"nginx\u0000","Date":"Fri, 10 Jul 2026 09:03:57 GMT","Content-Length":"2","Content-Type":"application/octet-stream","Content-Type":"text/plain","Connection":"close"}},"producer":{"modsecurity":"ModSecurity v3.0.16 (Linux)","connector":"ModSecurity-nginx v1.0.4","secrules_engine":"Enabled","components":["OWASP_CRS/3.3.10\""]},"messages":[]}} diff --git a/labs/lab11/reverse-proxy/nginx.conf b/labs/lab11/reverse-proxy/nginx.conf index dff91b26..a9275749 100644 --- a/labs/lab11/reverse-proxy/nginx.conf +++ b/labs/lab11/reverse-proxy/nginx.conf @@ -31,6 +31,9 @@ http { limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m; limit_req_status 429; + # Connection limit zone (Task 2) + limit_conn_zone $binary_remote_addr zone=conn:10m; + map $http_upgrade $connection_upgrade { default upgrade; '' close; } # Common proxy settings @@ -83,15 +86,30 @@ http { http2 on; server_name _; + # Connection limit (Task 2) + limit_conn conn 50; + + # Fail-closed timeouts (client-side, Task 2) + 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; + + # TLS 1.3 only, per Task 1 requirement + 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; + # TLS 1.3 negotiates ciphersuites independently of this directive, but Task 1 + # explicitly asks for it to be off (TLS 1.2 server-preference logic doesn't apply here). + ssl_prefer_server_ciphers off; + ssl_stapling off; - # If using a publicly-trusted certificate, you may enable OCSP stapling: + # OCSP stapling requires a publicly-trusted CA-issued cert; our self-signed + # lab cert has no OCSP responder to query, so stapling is documentation-only here. # ssl_stapling on; # ssl_stapling_verify on; # resolver 1.1.1.1 8.8.8.8 valid=300s; @@ -99,13 +117,11 @@ http { # 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; # 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; @@ -118,10 +134,14 @@ http { limit_req zone=login burst=5 nodelay; limit_req_log_level warn; proxy_pass http://juice; + proxy_connect_timeout 5s; + proxy_read_timeout 30s; } location / { proxy_pass http://juice; + proxy_connect_timeout 5s; + proxy_read_timeout 30s; } } } diff --git a/labs/lab11/waf/docker-compose.override.yml b/labs/lab11/waf/docker-compose.override.yml new file mode 100644 index 00000000..57e5463d --- /dev/null +++ b/labs/lab11/waf/docker-compose.override.yml @@ -0,0 +1,16 @@ +services: + waf: + image: owasp/modsecurity-crs:nginx-alpine + restart: unless-stopped + depends_on: + - juice + environment: + BACKEND: "http://juice:3000" + PARANOIA: "1" + MODSEC_RULE_ENGINE: "On" + MODSEC_AUDIT_ENGINE: "On" + MODSEC_AUDIT_LOG: "/var/log/modsecurity/audit.log" + ports: + - "8081:8080" + volumes: + - ./waf-logs:/var/log/modsecurity:rw diff --git a/submissions/lab11.md b/submissions/lab11.md new file mode 100644 index 00000000..534f2e45 --- /dev/null +++ b/submissions/lab11.md @@ -0,0 +1,158 @@ +# Lab 11 — BONUS — Submission + +## Task 1: TLS + Security Headers + +### nginx.conf (SSL + header sections) +```nginx +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 1d; + ssl_session_cache shared:SSL:10m; + 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; + + 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 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 +Location: https://localhost/ +``` + +### B. TLS 1.3 proof +``` +CONNECTION ESTABLISHED +Protocol version: TLSv1.3 +Ciphersuite: TLS_AES_256_GCM_SHA384 +Peer certificate: CN=juice.local +``` + +### C. Security headers proof (all 6 present) +``` +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=() +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 always use HTTPS for this domain, even if the user types `http://` or clicks an old HTTP link, preventing SSL-stripping downgrade attacks. +- **X-Content-Type-Options: nosniff**: stops the browser from guessing ("sniffing") a file's MIME type, blocking attacks where a malicious file disguised as an image is executed as a script. +- **X-Frame-Options: DENY**: prevents the page from being embedded in an `