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
35 changes: 35 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
## Goal

This PR delivers Lab N work: <briefly describe what this lab adds>.

## Changes

* Added/updated `submissions/labN.md`.
* Added/updated lab-related configuration files.
* Documented testing steps, observed output, and required artifacts.

## Testing

Commands used:

```bash
<paste commands used to verify the lab>
```

Observed output:

```text
<paste important output here>
```

## Artifacts & Screenshots

* `submissions/labN.md`
* <add links to screenshots, workflow runs, or other artifacts if needed>

## Checklist

* [ ] Title is clear (`feat(labN): <topic>` style)
* [ ] No secrets/large temp files committed
* [ ] Submission file at `submissions/labN.md` exists

51 changes: 51 additions & 0 deletions .github/workflows/lab1-smoke.yml
Original file line number Diff line number Diff line change
@@ -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
209 changes: 94 additions & 115 deletions labs/lab11/reverse-proxy/nginx.conf
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
25 changes: 25 additions & 0 deletions labs/lab11/waf/docker-compose.override.yml
Original file line number Diff line number Diff line change
@@ -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
Loading