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\n
403 Forbidden\r\n\r\n403 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 `