diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 000000000..063f7c13c --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,5 @@ +[allowlist] + description = "Allow documentation examples in submissions" + paths = [ + '''submissions/lab3\.md''', + ] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..0bd663214 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,12 @@ +repos: + - repo: https://github.com/gitleaks/gitleaks + rev: v8.27.2 + hooks: + - id: gitleaks + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: detect-private-key + - id: check-added-large-files + args: ["--maxkb=500"] diff --git a/labs/lab11/docker-compose.yml b/labs/lab11/docker-compose.yml index edb9ca06b..2f5388be5 100644 --- a/labs/lab11/docker-compose.yml +++ b/labs/lab11/docker-compose.yml @@ -11,8 +11,8 @@ services: depends_on: - juice ports: - - "8080:8080" # HTTP (will redirect to HTTPS) - - "8443:8443" # HTTPS + - "80:80" # HTTP (redirects to HTTPS) + - "443:443" # HTTPS volumes: - ./reverse-proxy/nginx.conf:/etc/nginx/nginx.conf:ro - ./reverse-proxy/certs:/etc/nginx/certs:ro diff --git a/labs/lab11/reverse-proxy/nginx.conf b/labs/lab11/reverse-proxy/nginx.conf index b90f6c476..8b09b97a2 100644 --- a/labs/lab11/reverse-proxy/nginx.conf +++ b/labs/lab11/reverse-proxy/nginx.conf @@ -7,47 +7,49 @@ http { include /etc/nginx/mime.types; default_type application/octet-stream; sendfile on; - keepalive_timeout 10; server_tokens off; - gzip 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" ' - 'rt=$request_time uct=$upstream_connect_time ' - 'urt=$upstream_response_time'; + '"$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent" ' + 'rt=$request_time uct=$upstream_connect_time ' + 'urt=$upstream_response_time'; 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: 10 req/min per IP on the login endpoint, burst of 5 limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m; limit_req_status 429; - map $http_upgrade $connection_upgrade { default upgrade; '' close; } + # Connection limit: max 50 concurrent connections per IP + limit_conn_zone $binary_remote_addr zone=conn:10m; - # 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; + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; 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_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + # Strip Accept-Encoding to prevent BREACH compression oracle against upstream + proxy_set_header Accept-Encoding ""; + proxy_read_timeout 30s; + proxy_send_timeout 30s; + proxy_connect_timeout 5s; + + # Strip headers upstream sets so proxy-level values are authoritative 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,62 +60,60 @@ http { proxy_hide_header Content-Security-Policy-Report-Only; proxy_hide_header Access-Control-Allow-Origin; - # HTTP server (redirect to HTTPS) + # ── HTTP → HTTPS redirect ──────────────────────────────────────────── server { - listen 8080; - listen [::]:8080; + 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; - 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:8443$request_uri; + return 308 https://$host$request_uri; } - # HTTPS server + # ── HTTPS server ───────────────────────────────────────────────────── server { - listen 8443 ssl; - listen [::]:8443 ssl; - http2 on; + 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_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; - - 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 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; + + # TLS 1.3 only — eliminates all legacy handshake attack surface + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + # TLS 1.3 cipher suites are not configurable via ssl_ciphers (OpenSSL manages them) + ssl_ecdh_curve X25519:secp384r1; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + ssl_session_tickets off; + + # OCSP stapling — no-op with self-signed cert; enable in production with CA-signed cert + # ssl_stapling on; ssl_stapling_verify on; + # resolver 1.1.1.1 8.8.8.8 valid=300s; resolver_timeout 5s; + + limit_conn conn 50; + + client_max_body_size 2m; + client_body_timeout 10s; + client_header_timeout 10s; + keepalive_timeout 10s; + send_timeout 10s; + + # ── Six required security headers ────────────────────────────────── + 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; + + # Additional hardening headers (beyond the required six) + 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; + # Rate-limited login endpoint (10 req/min, burst 5, fail immediately) location = /rest/user/login { limit_req zone=login burst=5 nodelay; limit_req_log_level warn; diff --git a/labs/lab11/waf/docker-compose.override.yml b/labs/lab11/waf/docker-compose.override.yml new file mode 100644 index 000000000..3c76b789a --- /dev/null +++ b/labs/lab11/waf/docker-compose.override.yml @@ -0,0 +1,20 @@ +services: + # ModSecurity v3 + OWASP CRS (nginx connector) — blocks attacks before they reach Juice Shop + waf: + image: owasp/modsecurity-crs:nginx-alpine + restart: unless-stopped + depends_on: + - juice + environment: + BACKEND: "http://juice:3000" + PORT: "8080" + MODSEC_RULE_ENGINE: "On" + PARANOIA: "1" + ANOMALY_INBOUND: "5" + ANOMALY_OUTBOUND: "4" + MODSEC_AUDIT_LOG: "/var/log/modsec/audit.log" + MODSEC_AUDIT_LOG_FORMAT: "JSON" + ports: + - "8080:8080" # WAF HTTP endpoint (separate from Nginx HTTPS on 443) + volumes: + - ./waf/logs:/var/log/modsec:rw diff --git a/submissions/lab11.md b/submissions/lab11.md new file mode 100644 index 000000000..3b3242bbf --- /dev/null +++ b/submissions/lab11.md @@ -0,0 +1,203 @@ +# Lab 11 — BONUS — Submission + +## Task 1: TLS + Security Headers + +### nginx.conf — SSL + header sections + +```nginx + # ── HTTP → HTTPS redirect ──────────────────────────────────────────── + server { + listen 80; + listen [::]:80; + server_name _; + + return 308 https://$host$request_uri; + } + + # ── HTTPS server ───────────────────────────────────────────────────── + 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 — eliminates all legacy handshake attack surface + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + # TLS 1.3 cipher suites are not configurable via ssl_ciphers (OpenSSL manages them) + ssl_ecdh_curve X25519:secp384r1; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + ssl_session_tickets off; + + # ── Six required security headers ────────────────────────────────── + 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; + } +``` + +### A. HTTPS redirect proof + +``` +HTTP/1.1 308 Permanent Redirect +Server: nginx +Date: Sat, 04 Jul 2026 14:11:27 GMT +Content-Type: text/html +Content-Length: 164 +Connection: keep-alive +Location: https://localhost/ +``` + +### B. TLS 1.3 proof + +``` +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 +Hash used: SHA256 +Signature type: rsa_pss_rsae_sha256 +``` + +### C. Security headers proof (all 6 present) + +``` +HTTP/2 200 +server: nginx +date: Sat, 04 Jul 2026 14:11:27 GMT +content-type: text/html; charset=UTF-8 +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' +cross-origin-opener-policy: same-origin +cross-origin-resource-policy: same-origin +``` + +### What each header defends against (1 sentence each) + +- **HSTS**: Forces all future browser connections to use HTTPS for 2 years, preventing protocol-downgrade and SSL-stripping attacks by a network-level adversary. +- **X-Content-Type-Options: nosniff**: Stops browsers from MIME-sniffing a response away from the declared Content-Type, blocking attacks where an attacker uploads a polyglot file (e.g., a PNG that is also valid JavaScript) that the browser would otherwise execute. +- **X-Frame-Options: DENY**: Prevents any site from embedding this page in an `