Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[allowlist]
description = "Allow documentation examples in submissions"
paths = [
'''submissions/lab3\.md''',
]
12 changes: 12 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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"]
4 changes: 2 additions & 2 deletions labs/lab11/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
134 changes: 67 additions & 67 deletions labs/lab11/reverse-proxy/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
20 changes: 20 additions & 0 deletions labs/lab11/waf/docker-compose.override.yml
Original file line number Diff line number Diff line change
@@ -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
Loading