Skip to content

Commit 1ec3c3c

Browse files
authored
Merge pull request #11 from RII6/feature/lab11
feat(lab11): hardened nginx + WAF sidecar
2 parents 35a33bd + b9eaf20 commit 1ec3c3c

3 files changed

Lines changed: 191 additions & 3 deletions

File tree

labs/lab11/reverse-proxy/nginx.conf

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ http {
3030
# ~10 req/min per IP, burst of 5
3131
limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;
3232
limit_req_status 429;
33+
limit_conn_zone $binary_remote_addr zone=conn:10m;
3334

3435
map $http_upgrade $connection_upgrade { default upgrade; '' close; }
3536

@@ -85,10 +86,12 @@ http {
8586

8687
ssl_certificate /etc/nginx/certs/localhost.crt;
8788
ssl_certificate_key /etc/nginx/certs/localhost.key;
88-
ssl_session_timeout 10m;
89+
ssl_session_timeout 1d;
8990
ssl_session_cache shared:SSL:10m;
90-
ssl_protocols TLSv1.2 TLSv1.3;
91-
ssl_ciphers "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:EECDH+AESGCM:EDH+AESGCM";
91+
ssl_session_tickets off;
92+
ssl_protocols TLSv1.3;
93+
ssl_ciphers "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-RSA-AES256-GCM-SHA384";
94+
ssl_ecdh_curve X25519:secp384r1;
9295
ssl_prefer_server_ciphers on;
9396
ssl_stapling off;
9497
# If using a publicly-trusted certificate, you may enable OCSP stapling:
@@ -98,6 +101,7 @@ http {
98101
# resolver_timeout 5s;
99102
# ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
100103

104+
limit_conn conn 50;
101105
client_max_body_size 2m;
102106
client_body_timeout 10s;
103107
client_header_timeout 10s;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
services:
2+
waf:
3+
image: owasp/modsecurity-crs:nginx-alpine
4+
restart: unless-stopped
5+
ports:
6+
- "8444:8080"
7+
environment:
8+
- PARANOIA=1
9+
- PROXY_SSL=0
10+
- BACKEND=http://juice:3000
11+
depends_on:
12+
- juice

submissions/lab11.md

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Lab 11 — BONUS — Submission
2+
3+
## Task 1: TLS + Security Headers
4+
5+
### nginx.conf (paste the SSL + header sections only — not the whole file)
6+
```nginx
7+
# HTTP server (redirect to HTTPS)
8+
server {
9+
listen 8080;
10+
listen [::]:8080;
11+
server_name _;
12+
13+
# Core headers (also on redirects)
14+
add_header X-Frame-Options "DENY" always;
15+
add_header X-Content-Type-Options "nosniff" always;
16+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
17+
add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" always;
18+
add_header Cross-Origin-Opener-Policy "same-origin" always;
19+
add_header Cross-Origin-Resource-Policy "same-origin" always;
20+
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;
21+
22+
return 308 https://$host:8443$request_uri;
23+
}
24+
25+
# HTTPS server
26+
server {
27+
listen 8443 ssl;
28+
listen [::]:8443 ssl;
29+
http2 on;
30+
server_name _;
31+
32+
ssl_certificate /etc/nginx/certs/localhost.crt;
33+
ssl_certificate_key /etc/nginx/certs/localhost.key;
34+
ssl_session_timeout 1d;
35+
ssl_session_cache shared:SSL:10m;
36+
ssl_session_tickets off;
37+
ssl_protocols TLSv1.3;
38+
ssl_ciphers "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-RSA-AES256-GCM-SHA384";
39+
ssl_ecdh_curve X25519:secp384r1;
40+
ssl_prefer_server_ciphers on;
41+
ssl_stapling off;
42+
```
43+
44+
### A. HTTPS redirect proof
45+
```
46+
HTTP/1.1 308 Permanent Redirect
47+
Server: nginx
48+
Date: Tue, 30 Jun 2026 19:48:07 GMT
49+
Content-Type: text/html
50+
Content-Length: 164
51+
Connection: keep-alive
52+
Location: https://localhost:8443/
53+
X-Frame-Options: DENY
54+
X-Content-Type-Options: nosniff
55+
Referrer-Policy: strict-origin-when-cross-origin
56+
Permissions-Policy: camera=(), geolocation=(), microphone=()
57+
Cross-Origin-Opener-Policy: same-origin
58+
Cross-Origin-Resource-Policy: same-origin
59+
Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'
60+
```
61+
62+
### B. TLS 1.3 proof
63+
```
64+
depth=0 CN = juice.local
65+
verify error:num=18:self signed certificate
66+
verify return:1
67+
depth=0 CN = juice.local
68+
verify return:1
69+
CONNECTED(00000006)
70+
write W BLOCK
71+
```
72+
73+
### C. Security headers proof (all 6 present)
74+
```
75+
HTTP/2 200
76+
server: nginx
77+
date: Tue, 30 Jun 2026 19:48:07 GMT
78+
content-type: text/html; charset=UTF-8
79+
content-length: 9903
80+
strict-transport-security: max-age=31536000; includeSubDomains; preload
81+
x-frame-options: DENY
82+
x-content-type-options: nosniff
83+
referrer-policy: strict-origin-when-cross-origin
84+
permissions-policy: camera=(), geolocation=(), microphone=()
85+
cross-origin-opener-policy: same-origin
86+
cross-origin-resource-policy: same-origin
87+
content-security-policy-report-only: default-src 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'
88+
```
89+
90+
### What each header defends against (1 sentence each)
91+
- HSTS: Forces the browser to strictly connect via HTTPS, mitigating man-in-the-middle downgrade attacks.
92+
- X-Content-Type-Options: nosniff: Prevents the browser from MIME-sniffing the response away from the declared content-type, mitigating drive-by download attacks.
93+
- X-Frame-Options: DENY: Prevents the site from being embedded in iframes on other domains, defending against clickjacking attacks.
94+
- Referrer-Policy: Controls how much referrer information is passed to external sites, preventing leakage of sensitive URL tokens or parameters.
95+
- Permissions-Policy: Restricts the application's ability to use powerful browser features like the camera or microphone, minimizing the impact of XSS.
96+
- Content-Security-Policy: Enforces an allowlist of trusted domains from which scripts, styles, and images can be loaded, fundamentally mitigating Cross-Site Scripting (XSS).
97+
98+
## Task 2: Production Posture
99+
100+
### Rate limit proof
101+
| HTTP code | Count out of 60 |
102+
|-----------|----------------:|
103+
| 200 | 0 |
104+
| 429 | 54 |
105+
| 5xx | 6 |
106+
107+
### Timeout enforced
108+
```
109+
# Sent a partial HTTP request and waited 15 seconds; nginx forcefully closes the connection
110+
```
111+
112+
### Cipher hardening
113+
```
114+
Server Temp Key: ECDH, X25519, 253 bits
115+
New, TLSv1/SSLv3, Cipher is AEAD-AES256-GCM-SHA384
116+
Cipher : AEAD-AES256-GCM-SHA384
117+
```
118+
119+
### Cert rotation runbook (7 steps)
120+
1. **Detect expiry**: Monitor the current certificate using an automated tool like Prometheus or Datadog, triggering an alert when it's < 30 days from expiration.
121+
2. **Order new cert**: Generate a new CSR and private key, and request a fresh signed certificate from the CA (e.g., Let's Encrypt or an internal PKI).
122+
3. **Validate**: Verify the newly received certificate chain using `openssl verify -CAfile` against the root CA to ensure it was properly signed and is cryptographically sound.
123+
4. **Atomic swap**: Stage the new certificate alongside the old one in the file system and perform a zero-downtime hot reload of the reverse proxy (e.g., `nginx -s reload`).
124+
5. **Verify**: Use an external testing tool (like `testssl.sh` or `openssl s_client`) to confirm the server is successfully serving the new certificate in production.
125+
6. **Rollback plan**: If the new certificate is rejected or broken, immediately run a script to swap the symlinks back to the old certificate paths and execute a hot reload to restore service.
126+
7. **Audit**: Log the successful rotation event in the centralized audit system (or Slack/Jira) and destroy the old private key securely to prevent future compromise.
127+
128+
### What OCSP stapling buys you (2-3 sentences, reference Reading 11)
129+
OCSP stapling allows the web server (Nginx) to proactively fetch and cache the certificate revocation status from the CA, securely "stapling" it directly to the TLS handshake. This prevents clients from having to make a slow, privacy-leaking DNS/HTTP call to the CA to check if the cert was revoked. However, it requires a publicly trusted certificate signed by a real CA; for a self-signed lab certificate, there is no CA endpoint to query for revocation status, meaning stapling will simply fail.
130+
131+
## Bonus: WAF Sidecar with OWASP CRS
132+
133+
### Setup choice
134+
- WAF used: ModSecurity v3 (owasp/modsecurity-crs:nginx-alpine image)
135+
- OWASP CRS version: 4.x
136+
- Paranoia level: 1
137+
138+
### Attack payload sent
139+
`GET /rest/products/search?q=' OR 1=1--` (URL-encoded)
140+
141+
### Before WAF (Nginx alone)
142+
```
143+
no-waf: HTTP 500
144+
```
145+
*(Juice Shop returns a 500 error because the SQL injection crashes the backend DB query, but Nginx happily proxies the attack).*
146+
147+
### After WAF
148+
```
149+
with-waf: HTTP 403
150+
```
151+
*(The WAF intercepts the SQL injection before it reaches Juice Shop and immediately returns a 403 Forbidden).*
152+
153+
### Audit log excerpt (the rule that fired)
154+
```json
155+
{
156+
"message":"SQL Injection Attack Detected via libinjection",
157+
"details":{
158+
"match":"detected SQLi using libinjection.",
159+
"reference":"v28,10",
160+
"ruleId":"942100",
161+
"file":"/etc/modsecurity.d/owasp-crs/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf",
162+
"lineNumber":"46",
163+
"data":"Matched Data: s&1c found within ARGS:q: ' OR 1=1--",
164+
"severity":"2",
165+
"ver":"OWASP_CRS/4.27.0"
166+
}
167+
}
168+
```
169+
Rule ID: **942100** — OWASP CRS rule name: **SQL Injection Attack Detected via libinjection**
170+
171+
### Tradeoff analysis (3 sentences)
172+
Deploying a WAF with OWASP CRS gives us immediate, zero-code protection against zero-day exploits (like Log4Shell) and standard OWASP Top 10 attacks that might slip past our SAST/DAST pipeline or Conftest gates. However, it costs significant operational overhead: higher paranoia levels will inevitably block legitimate user traffic (False Positives), requiring constant tuning of rule exclusions and performance monitoring. You should NOT deploy a WAF in front of internal, highly-trusted backend-to-backend microservice communication where the performance penalty of deep packet inspection outweighs the negligible risk of inbound attacks.

0 commit comments

Comments
 (0)