From 2311db4b779aac87491b82f3bca53cc079e15cda Mon Sep 17 00:00:00 2001 From: Lisoon22 Date: Fri, 12 Jun 2026 14:12:16 +0300 Subject: [PATCH 1/6] feat(lab1): juice shop deploy + PR template + triage report --- submissions/lab1.md | 107 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 submissions/lab1.md diff --git a/submissions/lab1.md b/submissions/lab1.md new file mode 100644 index 000000000..a4b77b61f --- /dev/null +++ b/submissions/lab1.md @@ -0,0 +1,107 @@ +# Lab 1 — Submission + +## Triage Report: OWASP Juice Shop + +### Scope & Asset +- Asset: OWASP Juice Shop (local lab instance) +- Image: `bkimminich/juice-shop:v20.0.0` +- Image digest / image ID: `sha256:fd58bdc9745416afce8184ee0666278a436574633ea7880365153a63bfd418b0` +- Host OS: Arch Linux +- Docker version: `Docker version 29.5.1, build 2518b52d94` + +### Deployment Details +- Run command used: `docker run -d --name juice-shop -p 127.0.0.1:3000:3000 bkimminich/juice-shop:v20.0.0` +- Access URL: http://127.0.0.1:3000 +- Network exposure: 127.0.0.1 only? [x] Yes [ ] No +- Container restart policy: `no` + +### Health Check +- HTTP code on `/`: 200 +- Product count from `/api/Products`: 46 +- Version check from `/rest/admin/application-version`: `{"version":"20.0.0"}` +- API check (first 200 chars of `/api/Products`): + ```json +{"status":"success","data":[{"id":1,"name":"Apple Juice (1000ml)","description":"The all-time classic.","price":1.99,"deluxePrice":0.99,"image":"apple_juice.jpg","createdAt":"2026-06-12T11:00:57.053Z" + ``` +- Container uptime: + ``` +NAMES STATUS PORTS +juice-shop Up 10 minutes 127.0.0.1:3000->3000/tcp + ``` + +### Initial Surface Snapshot (from browser exploration) +- Login/Registration visible: [x] Yes [ ] No — notes: visible through the Account menu; login and registration are reachable from the UI. +- Product listing/search present: [x] Yes [ ] No — notes: product cards are listed on the landing page; product data is also available through `/api/Products`. +- Admin or account area discoverable: [x] Yes [ ] No — notes: account functionality is visible from the top navigation; admin-related backend endpoint `/rest/admin/application-version` is reachable and exposes the application version. +- Client-side errors in DevTools console: [ ] Yes [x] No — notes: no blocking client-side errors were observed during initial load and product browsing. +- Pre-populated local storage / cookies: browser state contains normal Juice Shop client/UI state such as language or welcome/cookie-banner preferences; no user authentication token was present before login. +- Product detail network behavior: opening a product detail triggers API calls such as `/api/Products//reviews`; reading product/review data does not require authentication during initial browsing. + +### Security Headers (Quick Look) +Run: `curl -I http://127.0.0.1:3000 2>&1 | head -20`. Output: +``` + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed + 0 0 0 0 0 0 0 0 0 0 9903 0 0 0 0 0 0 0 0 9903 0 0 0 0 0 0 0 0 9903 0 0 0 0 0 0 0 +HTTP/1.1 200 OK +Access-Control-Allow-Origin: * +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +Feature-Policy: payment 'self' +X-Recruiting: /#/jobs +Accept-Ranges: bytes +Cache-Control: public, max-age=0 +Last-Modified: Fri, 12 Jun 2026 11:00:57 GMT +ETag: W/"26af-19ebb7e0182" +Content-Type: text/html; charset=UTF-8 +Content-Length: 9903 +Vary: Accept-Encoding +Date: Fri, 12 Jun 2026 11:11:54 GMT +Connection: keep-alive +Keep-Alive: timeout=5 + +``` + +Which of these are MISSING? +- [x] `Content-Security-Policy` +- [x] `Strict-Transport-Security` +- [ ] `X-Content-Type-Options: nosniff` +- [ ] `X-Frame-Options` + +### Top 3 Risks Observed +1. **Missing or incomplete browser hardening headers** — The initial header check shows that at least some defensive headers are absent or not explicitly configured. This matters because headers such as CSP, HSTS, X-Frame-Options, and X-Content-Type-Options reduce the impact of common browser-side attacks; this maps to OWASP Top 10:2025 A06 Security Misconfiguration. +2. **Discoverable unauthenticated API surface** — Product data and review-related endpoints are easy to discover from DevTools and can be queried directly. Even when public reads are intended, this increases the reconnaissance surface and should be reviewed for excessive data exposure or missing authorization boundaries; this maps to OWASP Top 10:2025 A01 Broken Access Control. +3. **Input-heavy application surface around login, registration, search, and product interaction** — The first exploration immediately exposes forms, account flows, and API-backed product functionality. These areas are common entry points for injection, authentication abuse, and validation mistakes, so they should be prioritized in later DAST/manual testing; this maps to OWASP Top 10:2025 A03 Injection and A07 Identification & Authentication Failures. + +## PR Template Setup + +- File: `.github/PULL_REQUEST_TEMPLATE.md` +- Sections included: Goal / Changes / Testing / Artifacts & Screenshots +- Checklist items: + - Title is clear (`feat(labN): ` style) + - No secrets/large temp files committed + - Submission file at `submissions/labN.md` exists +- Auto-fill verified: [x] Yes — PR description showed my template (PASTE_DRAFT_PR_URL_HERE_AFTER_PUSH) + +## GitHub Community + +I starred the course repository and `simple-container-com/api` because stars work both as bookmarks and as a public signal that helps useful open-source projects gain visibility. I also followed the professor, TAs, and classmates because following developers makes it easier to track course activity, discover related projects, and build professional connections for future teamwork. + +Completed actions: +- [x] Starred the course repository +- [x] Starred `simple-container-com/api` +- [x] Followed @Cre-eD +- [x] Followed @Naghme98 +- [x] Followed @pierrepicaud +- [x] Followed at least 3 classmates from the course + +## Bonus: CI Smoke Test + +- Workflow file: `.github/workflows/lab1-smoke.yml` +- Trigger: `pull_request` on main +- Run URL (green): PASTE_GREEN_ACTIONS_RUN_URL_HERE_AFTER_PR +- Workflow run duration: PASTE_WORKFLOW_DURATION_HERE +- Curl response excerpt: + ``` +HTTP/1.1 200 OK / Homepage HTTP status: 200 + ``` From 6efd4a5ce4bcf32e7cdc47cc22545cd34b723833 Mon Sep 17 00:00:00 2001 From: Lisoon22 Date: Fri, 12 Jun 2026 14:15:18 +0300 Subject: [PATCH 2/6] chore(lab1): add pull request template --- .github/PULL_REQUEST_TEMPLATE.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..2014a9f77 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,23 @@ +## Goal + +This PR delivers Lab 1: OWASP Juice Shop deployment, triage report, and course PR workflow setup. + +## Changes + +- Added `submissions/lab1.md` with Juice Shop deployment and triage report. +- Added `.github/PULL_REQUEST_TEMPLATE.md` for future lab submissions. +- Documented testing commands, observations, security headers, risks, and GitHub community engagement. + +## Testing + +Commands used: + +```bash +docker run -d --name juice-shop -p 127.0.0.1:3000:3000 bkimminich/juice-shop:v20.0.0 +docker ps --filter name=juice-shop +curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:3000 +curl -s http://127.0.0.1:3000/api/Products | jq '.data | length' +curl -s http://127.0.0.1:3000/rest/admin/application-version | jq +curl -I http://127.0.0.1:3000 2>&1 | head -20 +''' + From 2009dd387655d162c9050efc3e55824b52cfaa93 Mon Sep 17 00:00:00 2001 From: Lisoon22 Date: Fri, 12 Jun 2026 14:20:40 +0300 Subject: [PATCH 3/6] update PE TEMPLATE --- .github/PULL_REQUEST_TEMPLATE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2014a9f77..d8318bb8b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -19,5 +19,6 @@ curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:3000 curl -s http://127.0.0.1:3000/api/Products | jq '.data | length' curl -s http://127.0.0.1:3000/rest/admin/application-version | jq curl -I http://127.0.0.1:3000 2>&1 | head -20 -''' +``` +Observed result: Juice Shop started successfully, homepage returned HTTP 200, and API/version endpoints responded. From 40f06496e41d76b47785cdffcb6349919fa309be Mon Sep 17 00:00:00 2001 From: Lisoon22 Date: Fri, 12 Jun 2026 14:32:01 +0300 Subject: [PATCH 4/6] update PR template --- .github/PULL_REQUEST_TEMPLATE.md | 33 +++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d8318bb8b..2454de86e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,24 +1,35 @@ ## Goal -This PR delivers Lab 1: OWASP Juice Shop deployment, triage report, and course PR workflow setup. +This PR delivers Lab N work: . ## Changes -- Added `submissions/lab1.md` with Juice Shop deployment and triage report. -- Added `.github/PULL_REQUEST_TEMPLATE.md` for future lab submissions. -- Documented testing commands, observations, security headers, risks, and GitHub community engagement. +* Added/updated `submissions/labN.md`. +* Added/updated lab-related configuration files. +* Documented testing steps, observed output, and required artifacts. ## Testing Commands used: ```bash -docker run -d --name juice-shop -p 127.0.0.1:3000:3000 bkimminich/juice-shop:v20.0.0 -docker ps --filter name=juice-shop -curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:3000 -curl -s http://127.0.0.1:3000/api/Products | jq '.data | length' -curl -s http://127.0.0.1:3000/rest/admin/application-version | jq -curl -I http://127.0.0.1:3000 2>&1 | head -20 + ``` -Observed result: Juice Shop started successfully, homepage returned HTTP 200, and API/version endpoints responded. + +Observed output: + +```text + +``` + +## Artifacts & Screenshots + +* `submissions/labN.md` +* + +## Checklist + +* [ ] Title is clear (`feat(labN): ` style) +* [ ] No secrets/large temp files committed +* [ ] Submission file at `submissions/labN.md` exists From 712b7048a2c66b5b0a9bddab790902affcb82b3d Mon Sep 17 00:00:00 2001 From: Lisoon22 Date: Fri, 12 Jun 2026 14:46:27 +0300 Subject: [PATCH 5/6] ci(lab1): add juice shop smoke test --- .github/workflows/lab1-smoke.yml | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/lab1-smoke.yml diff --git a/.github/workflows/lab1-smoke.yml b/.github/workflows/lab1-smoke.yml new file mode 100644 index 000000000..2ed88e2c7 --- /dev/null +++ b/.github/workflows/lab1-smoke.yml @@ -0,0 +1,51 @@ +name: Lab 1 Smoke Test + +on: + pull_request: + branches: + - main + +permissions: + contents: read + +jobs: + smoke-test: + runs-on: ubuntu-latest + + steps: + - name: Pull Juice Shop image + run: docker pull bkimminich/juice-shop:v20.0.0 + + - name: Run Juice Shop + run: | + docker run -d --name juice-shop \ + -p 127.0.0.1:3000:3000 \ + bkimminich/juice-shop:v20.0.0 + + - name: Wait for Juice Shop to become healthy + run: | + for i in $(seq 1 30); do + if curl --silent --fail http://127.0.0.1:3000/rest/admin/application-version; then + echo + echo "Juice Shop is healthy" + exit 0 + fi + + echo "Waiting for Juice Shop... attempt $i/30" + sleep 2 + done + + echo "Juice Shop did not become healthy within 60 seconds" + docker logs juice-shop + exit 1 + + - name: Verify homepage returns HTTP 200 + run: | + STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000) + echo "Homepage HTTP status: $STATUS_CODE" + + if [ "$STATUS_CODE" != "200" ]; then + echo "Expected HTTP 200, got $STATUS_CODE" + docker logs juice-shop + exit 1 + fi From a439dc49b34a5295532f148106b53d89f972c00e Mon Sep 17 00:00:00 2001 From: Lisoon22 Date: Fri, 17 Jul 2026 21:23:46 +0300 Subject: [PATCH 6/6] feat(lab11): hardened nginx + OWASP CRS WAF --- labs/lab11/reverse-proxy/nginx.conf | 209 +++++++++------------ labs/lab11/waf/docker-compose.override.yml | 25 +++ submissions/lab11.md | 206 ++++++++++++++++++++ 3 files changed, 325 insertions(+), 115 deletions(-) create mode 100644 labs/lab11/waf/docker-compose.override.yml create mode 100644 submissions/lab11.md diff --git a/labs/lab11/reverse-proxy/nginx.conf b/labs/lab11/reverse-proxy/nginx.conf index b90f6c476..d3b62c08d 100644 --- a/labs/lab11/reverse-proxy/nginx.conf +++ b/labs/lab11/reverse-proxy/nginx.conf @@ -1,127 +1,106 @@ -user nginx; -worker_processes auto; +user nginx; +worker_processes auto; +pid /var/run/nginx.pid; events { worker_connections 1024; } http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - sendfile on; - keepalive_timeout 10; - 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" ' - '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 - limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m; - limit_req_status 429; - - 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; - 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_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; - proxy_hide_header Permissions-Policy; - proxy_hide_header Cross-Origin-Opener-Policy; - proxy_hide_header Cross-Origin-Resource-Policy; - proxy_hide_header Content-Security-Policy; - 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 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; - 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; + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; keepalive_timeout 10s; - send_timeout 10s; + server_tokens off; + gzip off; + + log_format security '$remote_addr [$time_local] "$request" $status ' + '$body_bytes_sent 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 juice { + server juice:3000; + keepalive 32; + } - # Security headers (include HSTS here only) - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; - add_header X-Frame-Options "DENY" always; + 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; + + client_body_timeout 10s; + client_header_timeout 10s; + send_timeout 30s; + proxy_connect_timeout 5s; + proxy_read_timeout 30s; + proxy_send_timeout 30s; + + proxy_http_version 1.1; + 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_set_header Accept-Encoding ""; + + proxy_hide_header X-Powered-By; + proxy_hide_header Strict-Transport-Security; + proxy_hide_header X-Content-Type-Options; + proxy_hide_header X-Frame-Options; + proxy_hide_header Referrer-Policy; + proxy_hide_header Permissions-Policy; + proxy_hide_header Content-Security-Policy; + proxy_hide_header Content-Security-Policy-Report-Only; + + # BEGIN TASK1_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=(), 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; - - location = /rest/user/login { - limit_req zone=login burst=5 nodelay; - limit_req_log_level warn; - proxy_pass http://juice; + add_header Permissions-Policy + "camera=(), microphone=(), geolocation=()" always; + add_header Content-Security-Policy-Report-Only + "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'" always; + # END TASK1_HEADERS + + server { + listen 8080; + listen [::]:8080; + server_name _; + return 308 https://$host:8443$request_uri; } - location / { - proxy_pass http://juice; + server { + listen 8443 ssl; + listen [::]:8443 ssl; + http2 on; + server_name _; + + limit_conn conn 50; + + ssl_certificate /etc/nginx/certs/localhost.crt; + ssl_certificate_key /etc/nginx/certs/localhost.key; + + # BEGIN TASK1_TLS + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + # END TASK1_TLS + + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_conf_command Ciphersuites + TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256; + ssl_ecdh_curve X25519:secp384r1; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + ssl_session_tickets off; + ssl_stapling off; + + location = /rest/user/login { + limit_req zone=login burst=5 nodelay; + proxy_pass http://juice; + } + + location / { + proxy_pass http://juice; + } } - } } diff --git a/labs/lab11/waf/docker-compose.override.yml b/labs/lab11/waf/docker-compose.override.yml new file mode 100644 index 000000000..981307825 --- /dev/null +++ b/labs/lab11/waf/docker-compose.override.yml @@ -0,0 +1,25 @@ +services: + waf: + image: owasp/modsecurity-crs:4.25-nginx-lts + restart: unless-stopped + depends_on: + - nginx + ports: + - "9443:8443" + environment: + BACKEND: "https://nginx:8443" + PROXY_SSL_VERIFY: "off" + SERVER_NAME: "localhost-waf" + PORT: "8080" + SSL_PORT: "8443" + NGINX_ALWAYS_TLS_REDIRECT: "on" + MODSEC_RULE_ENGINE: "On" + MODSEC_AUDIT_ENGINE: "RelevantOnly" + MODSEC_AUDIT_LOG: "/var/log/modsec/audit.log" + MODSEC_AUDIT_LOG_FORMAT: "JSON" + BLOCKING_PARANOIA: "1" + DETECTION_PARANOIA: "1" + ANOMALY_INBOUND: "5" + ANOMALY_OUTBOUND: "4" + volumes: + - ./waf/logs:/var/log/modsec:rw diff --git a/submissions/lab11.md b/submissions/lab11.md new file mode 100644 index 000000000..2e66685f1 --- /dev/null +++ b/submissions/lab11.md @@ -0,0 +1,206 @@ +# Lab 11 — BONUS — Submission + +## Environment + +```text +Docker: Docker version 29.5.2, build 79eb04c7d8 +Compose: Docker Compose version 5.1.4 +OpenSSL: OpenSSL 3.6.3 9 Jun 2026 (Library: OpenSSL 3.6.3 9 Jun 2026) +Nginx: nginx:stable-alpine +Juice Shop: bkimminich/juice-shop:v20.0.0 +WAF: owasp/modsecurity-crs:4.25-nginx-lts +``` + +## Task 1: TLS + Security Headers + +### TLS configuration + +```nginx +ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; +``` + +### Security headers + +```nginx +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'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'" always; +``` + +### HTTP → HTTPS redirect + +```text +HTTP/1.1 308 Permanent Redirect +Server: nginx +Date: Fri, 17 Jul 2026 18:17:40 GMT +Content-Type: text/html +Content-Length: 164 +Connection: keep-alive +Location: https://localhost:8443/ +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'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' +``` + +### TLS 1.3 negotiation + +```text +Connecting to ::1 +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 +Verification error: self-signed certificate +Peer Temp Key: X25519, 253 bits +DONE +``` + +### Header evidence + +```text +HTTP/2 200 +server: nginx +date: Fri, 17 Jul 2026 18:17:40 GMT +content-type: text/html; charset=UTF-8 +content-length: 9903 +access-control-allow-origin: * +feature-policy: payment 'self' +x-recruiting: /#/jobs +accept-ranges: bytes +cache-control: public, max-age=0 +last-modified: Fri, 17 Jul 2026 18:17:38 GMT +etag: W/"26af-19f714c605d" +vary: Accept-Encoding +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'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' +``` + +### Header rationale + +- **HSTS:** forces future browser requests to HTTPS and reduces downgrade exposure. +- **X-Content-Type-Options:** prevents MIME-sniffing reinterpretation. +- **X-Frame-Options:** blocks framing and reduces clickjacking. +- **Referrer-Policy:** limits URL disclosure through the `Referer` header. +- **Permissions-Policy:** disables unused camera, microphone, and geolocation APIs. +- **CSP Report-Only:** measures policy violations before enforcement can break the app. + +## Task 2: Production Posture + +### Login rate limiting + +| HTTP code | Count | +|---|---:| +| 200 | 0 | +| 400 | 0 | +| 401 | 6 | +| 429 | 54 | +| 5xx | 0 | + +```text +6 401 + 54 429 +``` + +### Slow/incomplete-header timeout + +```text +elapsed_seconds=10.02 +result=server closed or rejected incomplete headers +``` + +### Cipher and key exchange + +```text +Connecting to ::1 +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 +Verification error: self-signed certificate +Peer Temp Key: X25519, 253 bits +DONE +``` + +TLS 1.3 suites are configured through OpenSSL `Ciphersuites`; Nginx +`ssl_ciphers` applies to TLS 1.2 and earlier, which are disabled here. + +### Seven-step certificate-rotation runbook + +1. Monitor `notAfter` continuously; alert 30, 14, and 7 days before expiry. +2. Obtain the replacement certificate through ACME or the approved enterprise CA. +3. Validate SANs, chain, key match, permissions, expiry, and `nginx -t`. +4. Write versioned files and atomically switch symlinks. +5. Reload Nginx without terminating established connections. +6. Verify the served fingerprint, TLS protocol, headers, and application health. +7. Roll back symlinks and reload on failure; record approver, fingerprint, and next expiry. + +### OCSP stapling + +Public CA certificates can staple a signed OCSP response, reducing client-to-CA +requests while preserving revocation checks. The lab certificate is self-signed +and has no CA OCSP responder, so stapling is intentionally disabled rather than +claimed as functioning. + +## Bonus: ModSecurity + OWASP CRS v4 + +- Image: `owasp/modsecurity-crs:4.25-nginx-lts` +- Blocking paranoia level: 1 +- Audit log: `/var/log/modsec/audit.log` +- Probe: `GET /rest/products/search?q=' OR 1=1--` + +### Same request without and with WAF + +```text +no-waf: HTTP 500 +with-waf: HTTP 403 +``` + +### Audit evidence + +```text +{"transaction":{"client_ip":"172.20.0.1","time_stamp":"Fri Jul 17 18:18:21 2026","server_id":"488eabffba1022f0d067be8e49e910dbda7cf1ed","client_port":58650,"host_ip":"172.20.0.4","host_port":8443,"unique_id":"178431230118.628678","is_interrupted":true,"request":{"method":"GET","http_version":"2.0","hostname":"localhost","uri":"/rest/products/search?q=%27%20OR%201%3D1--","headers":{"user-agent":"curl/8.20.0","accept":"*/*","host":"localhost:9443"}},"response":{"body":"\r\n403 Forbidden\r\n\r\n

403 Forbidden

\r\n
nginx
\r\n\r\n\r\n","http_code":403,"headers":{"Server":"nginx\u0000","Date":"Fri, 17 Jul 2026 18:18:21 GMT","Content-Length":"146","Content-Type":"text/html","Access-Control-Allow-Origin":"*","Connection":"close","Access-Control-Max-Age":"3600","Access-Control-Allow-Methods":"GET, POST, PUT, DELETE, OPTIONS","Access-Control-Allow-Headers":"*"}},"producer":{"modsecurity":"ModSecurity v3.0.16 (Linux)","connector":"ModSecurity-nginx v1.0.4","secrules_engine":"Enabled","components":["OWASP_CRS/4.25.1\""]},"messages":[{"message":"SQL Injection Attack Detected via libinjection","details":{"match":"detected SQLi using libinjection.","reference":"v28,10","ruleId":"942100","file":"/etc/modsecurity.d/owasp-crs/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf","lineNumber":"46","data":"Matched Data: s&1c found within ARGS:q: ' OR 1=1--","severity":"2","ver":"OWASP_CRS/4.25.1","rev":"","tags":["application-multi","language-multi","platform-multi","attack-sqli","paranoia-level/1","OWASP_CRS","OWASP_CRS/ATTACK-SQLI","capec/1000/152/248/66"],"maturity":"0","accuracy":"0"}},{"message":"Inbound Anomaly Score Exceeded (Total Score: 5)","details":{"match":"Matched \"Operator `Ge' with parameter `5' against variable `TX:BLOCKING_INBOUND_ANOMALY_SCORE' (Value: `5' )","reference":"","ruleId":"949110","file":"/etc/modsecurity.d/owasp-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf","lineNumber":"222","data":"","severity":"0","ver":"OWASP_CRS/4.25.1","rev":"","tags":["modsecurity","anomaly-evaluation","OWASP_CRS"],"maturity":"0","accuracy":"0"}}]}} +``` + +Detected CRS rule ID: **942100** + +### Trade-offs + +The WAF supplies a runtime request-inspection layer and can temporarily block +known exploit patterns while application fixes are prepared. It also introduces +latency, false positives, tuning work, another patching surface, and operational +ownership. It is most justified for exposed legacy/high-value services and less +justified for low-risk internal services already isolated by stronger controls. + +## Completion checklist + +- [x] HTTP redirects to HTTPS. +- [x] TLS 1.3 is the only enabled protocol. +- [x] All six required headers are present with `always`. +- [x] Login throttling produces HTTP 429. +- [x] Connection and request timeouts are configured. +- [x] TLS 1.3 AEAD suites and X25519 are verified. +- [x] Certificate rotation and OCSP behavior are documented. +- [x] OWASP CRS v4 blocks the SQL-injection probe with HTTP 403. +- [x] Audit evidence contains a CRS SQLi rule.