From 407cc6e26e7c6baf005ba0394de5a7f85bc1ae24 Mon Sep 17 00:00:00 2001 From: Philip Idiare Date: Fri, 17 Jul 2026 22:30:37 +0300 Subject: [PATCH 1/3] feat(lab11): hardened nginx TLS 1.3 + security headers + rate limiting --- labs/lab11/reverse-proxy/nginx.conf | 44 ++----- submissions/lab11.md | 194 ++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+), 30 deletions(-) create mode 100644 submissions/lab11.md diff --git a/labs/lab11/reverse-proxy/nginx.conf b/labs/lab11/reverse-proxy/nginx.conf index b90f6c476..2eba0d447 100644 --- a/labs/lab11/reverse-proxy/nginx.conf +++ b/labs/lab11/reverse-proxy/nginx.conf @@ -11,7 +11,6 @@ http { server_tokens 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" ' @@ -20,20 +19,17 @@ http { 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 limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m; limit_req_status 429; + limit_conn_zone $binary_remote_addr zone=conn:10m; map $http_upgrade $connection_upgrade { default upgrade; '' close; } - # 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; @@ -41,13 +37,11 @@ http { 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_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,25 +52,18 @@ http { proxy_hide_header Content-Security-Policy-Report-Only; proxy_hide_header Access-Control-Allow-Origin; - # HTTP server (redirect to HTTPS) server { listen 8080; listen [::]:8080; 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 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; - return 308 https://$host:8443$request_uri; } - # HTTPS server server { listen 8443 ssl; listen [::]:8443 ssl; @@ -85,18 +72,16 @@ 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; + 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; - # 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; + + limit_conn conn 50; client_max_body_size 2m; client_body_timeout 10s; @@ -104,12 +89,11 @@ http { 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 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=(), geolocation=(), microphone=()" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" 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; diff --git a/submissions/lab11.md b/submissions/lab11.md new file mode 100644 index 000000000..ce7cda91f --- /dev/null +++ b/submissions/lab11.md @@ -0,0 +1,194 @@ +# Lab 11 — BONUS — Reverse Proxy Hardening: Nginx + WAF + +## Task 1: TLS + Security Headers + +### nginx.conf (SSL + header sections) + +```nginx + server { + listen 8080; + listen [::]:8080; + 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=(), 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; + return 308 https://$host:8443$request_uri; + } + + server { + listen 8443 ssl; + listen [::]:8443 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_ecdh_curve X25519:secp384r1; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + ssl_session_tickets off; + ssl_stapling off; + + 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 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 +Date: Fri, 17 Jul 2026 19:18:16 GMT +Content-Type: text/html +Content-Length: 164 +Connection: keep-alive +Location: https://localhost:8443/ +X-Frame-Options: DENY +X-Content-Type-Options: nosniff +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' +``` + +### 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 +``` + +### C. Security headers proof (all present) +``` +HTTP/2 200 +server: nginx +date: Fri, 17 Jul 2026 19:18: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=() +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 (`max-age=63072000; includeSubDomains; preload`):** Forces browsers to use HTTPS for all future requests to this domain for 2 years, preventing protocol downgrade attacks where an attacker intercepts the initial HTTP request before the redirect fires. +- **X-Content-Type-Options: nosniff:** Prevents browsers from MIME-sniffing a response away from its declared Content-Type, stopping attacks where a malicious file uploaded as an image is executed as JavaScript because the browser guessed its type. +- **X-Frame-Options: DENY:** Prevents any page from embedding this site in an `