Skip to content

Feature/lab11 - #7

Open
raaller wants to merge 5 commits into
mainfrom
feature/lab11
Open

Feature/lab11#7
raaller wants to merge 5 commits into
mainfrom
feature/lab11

Conversation

@raaller

@raaller raaller commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Goal

Deliver Lab 11 with a hardened Nginx reverse proxy, TLS 1.3, production edge controls, and a ModSecurity v3 + OWASP CRS WAF sidecar.

Changes

  • Hardened labs/lab11/reverse-proxy/nginx.conf: HTTP-to-HTTPS redirect, TLS 1.3 only, approved TLS 1.3 cipher suites, X25519, disabled session tickets and early data, and hardened session settings.
  • Added HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, CSP Report-Only, COOP, and CORP response headers.
  • Added login rate limiting, connection/request timeouts, upstream timeouts, and a documented certificate-rotation and rollback runbook.
  • Added labs/lab11/waf/Dockerfile with the pinned owasp/modsecurity-crs:4.25.1-nginx-alpine-lts image.
  • Added labs/lab11/waf/docker-compose.override.yml exposing the WAF on port 8443 and proxying accepted requests to the hardened Nginx endpoint over TLS.
  • Added submissions/lab11.md with configuration excerpts, runtime evidence, security-header explanations, OCSP-stapling analysis, WAF evidence, and deployment tradeoffs.

Testing

Nginx configuration validation

docker compose -f labs/lab11/docker-compose.yml exec -T nginx nginx -t
# nginx: configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful

HTTP-to-HTTPS redirect

curl -sSI http://localhost | grep -Ei '^(HTTP/|Location:)'
# HTTP/1.1 308 Permanent Redirect
# Location: https://localhost/

TLS 1.3, cipher suite, and key exchange

openssl s_client -connect localhost:443 -servername localhost -tls1_3 -brief </dev/null
# Protocol version: TLSv1.3
# Ciphersuite: TLS_AES_256_GCM_SHA384
# Server Temp Key: X25519, 253 bits

Security response headers

curl -skI https://localhost | grep -Ei '^(strict-transport-security|x-content-type-options|x-frame-options|referrer-policy|permissions-policy|content-security-policy-report-only):'
# 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=(), microphone=(), geolocation=()
# content-security-policy-report-only: default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' wss:; upgrade-insecure-requests

Login rate limiting

seq 1 60 | xargs -P 30 -I{} curl -sk --max-time 10 -o /dev/null -w '%{http_code}\n' -H 'Content-Type: application/json' --data '{"email":"rate-limit-test@example.invalid","password":"invalid-password"}' https://localhost/rest/user/login | sort | uniq -c
#       6 401
#      54 429

The six HTTP 401 responses reached Juice Shop and the remaining 54 requests were rejected by Nginx with HTTP 429.

Slow-header timeout

The TLS socket test sent an incomplete HTTP request header and waited for the server to close the connection.

python3 - <<'PY'
import socket, ssl, time

context = ssl._create_unverified_context()
started = time.monotonic()
with socket.create_connection(("127.0.0.1", 443), timeout=5) as raw:
    with context.wrap_socket(raw, server_hostname="localhost") as tls:
        tls.settimeout(15)
        tls.sendall(b"GET / HTTP/1.1\r\nHost: localhost\r\nX-Slow: ")
        response = tls.recv(4096)

print("Outcome:", "HTTP response received" if response else "connection closed")
print(f"Elapsed: {time.monotonic() - started:.2f} seconds")
PY
# Outcome: connection closed
# Elapsed: 10.01 seconds

WAF configuration validation

docker compose -f labs/lab11/docker-compose.yml -f labs/lab11/waf/docker-compose.override.yml exec -T waf nginx -t
# nginx: configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful

SQL-injection probe without and with the WAF

curl -sk -o /dev/null -w 'no-waf: HTTP %{http_code}\n' --get --data-urlencode "q=' OR 1=1--" https://localhost/rest/products/search
# no-waf: HTTP 500

curl -sk -o /dev/null -w 'with-waf: HTTP %{http_code}\n' --get --data-urlencode "q=' OR 1=1--" https://localhost:8443/rest/products/search
# with-waf: HTTP 403

The baseline Nginx endpoint forwarded the payload to Juice Shop, while the WAF rejected the same request before it reached the application.

OWASP CRS audit evidence

docker compose -f labs/lab11/docker-compose.yml -f labs/lab11/waf/docker-compose.override.yml exec -T waf sh -c 'grep -E -m2 "\[id \"(942100|949110)\"\]" /var/log/modsec/audit.log'
# [id "942100"] [msg "SQL Injection Attack Detected via libinjection"] [ver "OWASP_CRS/4.25.1"]
# [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [ver "OWASP_CRS/4.25.1"]

Rule 942100 detected the SQL-injection payload at paranoia level 1, and rule 949110 enforced the HTTP 403 response.

Artifacts & Screenshots

  • labs/lab11/reverse-proxy/nginx.conf — hardened reverse-proxy configuration.
  • labs/lab11/waf/Dockerfile — pinned ModSecurity v3 and OWASP CRS 4.25.1 image.
  • labs/lab11/waf/docker-compose.override.yml — WAF sidecar deployment and TLS upstream configuration.
  • submissions/lab11.md — complete submission report with runtime proof and security analysis.

Checklist

  • Title is clear (feat(lab11): hardened nginx + WAF sidecar)
  • No secrets/large temp files committed
  • Submission file at submissions/lab11.md exists
  • Task 1 — TLS 1.3 + 6 security headers (with proof)
  • Task 2 — Rate limit + timeouts + cipher hardening + cert-rotation runbook
  • Bonus — Coraza/ModSec WAF + OWASP CRS catching a payload Nginx-alone passes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant