diff --git a/contrib/reverse-proxy/README.md b/contrib/reverse-proxy/README.md new file mode 100644 index 0000000000..a6387e689a --- /dev/null +++ b/contrib/reverse-proxy/README.md @@ -0,0 +1,64 @@ +# Prowler Reverse Proxy Configuration + +Ready-to-use nginx configuration for running Prowler behind a reverse proxy. + +## Problem + +Prowler's default Docker setup exposes two separate services: +- **UI** on port 3000 +- **API** on port 8080 + +This causes CORS issues and authentication failures (especially SAML SSO) when accessed through an external reverse proxy, since the proxy typically exposes a single domain. + +## Solution + +This adds an nginx container that unifies both services behind a single port, correctly forwarding headers so that Django generates proper URLs for SAML ACS callbacks and API responses. + +## Quick Start + +From the prowler root directory: + + docker compose -f docker-compose.yml \ + -f contrib/reverse-proxy/docker-compose.reverse-proxy.yml \ + up -d + +Access Prowler at http://localhost (port 80). + +## With an External Reverse Proxy + +Point your external reverse proxy to the prowler-nginx container on port 80. + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| PROWLER_PROXY_PORT | 80 | Port exposed by the nginx proxy | + +### Example: Traefik + + services: + nginx: + labels: + - "traefik.enable=true" + - "traefik.http.routers.prowler.rule=Host(`prowler.example.com`)" + - "traefik.http.routers.prowler.tls.certresolver=letsencrypt" + - "traefik.http.services.prowler.loadbalancer.server.port=80" + +### Example: Caddy + + prowler.example.com { + reverse_proxy prowler-nginx:80 + } + +## SAML SSO + +If using SAML SSO behind a reverse proxy, also set the SAML_ACS_BASE_URL environment variable: + + SAML_ACS_BASE_URL=https://prowler.example.com + +## Architecture + + Internet -> External Reverse Proxy -> prowler-nginx:80 + |-- /api/* -> prowler-api:8080 + |-- /accounts/saml/ -> prowler-api:8080 + +-- /* -> prowler-ui:3000 diff --git a/contrib/reverse-proxy/docker-compose.reverse-proxy.yml b/contrib/reverse-proxy/docker-compose.reverse-proxy.yml new file mode 100644 index 0000000000..08c52f3558 --- /dev/null +++ b/contrib/reverse-proxy/docker-compose.reverse-proxy.yml @@ -0,0 +1,42 @@ +# Prowler Reverse Proxy - Docker Compose Override +# +# Use this alongside the main docker-compose.yml to add an nginx +# reverse proxy that unifies UI and API behind a single port. +# +# Usage: +# docker compose -f docker-compose.yml -f contrib/reverse-proxy/docker-compose.reverse-proxy.yml up -d +# +# Then access Prowler at http://localhost (port 80) or configure +# your external reverse proxy (Traefik, Caddy, Cloudflare Tunnel, +# Pangolin, etc.) to point to this container on port 80. +# +# For HTTPS with your own certs, see the README in this directory. +# +# Fixes: https://github.com/prowler-cloud/prowler/issues/8516 + +services: + nginx: + image: nginx:alpine + container_name: prowler-nginx + restart: unless-stopped + ports: + - "${PROWLER_PROXY_PORT:-80}:80" + volumes: + - ./contrib/reverse-proxy/nginx.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - prowler-ui + - prowler-api + networks: + - prowler-network + + # Override UI to not expose port externally (nginx handles it) + prowler-ui: + ports: !reset [] + + # Override API to not expose port externally (nginx handles it) + prowler-api: + ports: !reset [] + +networks: + prowler-network: + driver: bridge diff --git a/contrib/reverse-proxy/nginx.conf b/contrib/reverse-proxy/nginx.conf new file mode 100644 index 0000000000..58520295bc --- /dev/null +++ b/contrib/reverse-proxy/nginx.conf @@ -0,0 +1,70 @@ +# Prowler Reverse Proxy Configuration +# Routes both UI and API through a single endpoint +# +# Usage: See docker-compose.reverse-proxy.yml +# Fixes: https://github.com/prowler-cloud/prowler/issues/8516 + +upstream prowler-ui { + server prowler-ui:3000; +} + +upstream prowler-api { + server prowler-api:8080; +} + +server { + listen 80; + server_name _; + + # Security headers + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # API requests — proxy to prowler-api + location /api/ { + proxy_pass http://prowler-api/api/; + 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 X-Forwarded-Host $host; + proxy_read_timeout 300s; + proxy_connect_timeout 10s; + + # Handle large scan payloads + client_max_body_size 50m; + } + + # SAML endpoints — proxy to prowler-api + location /accounts/saml/ { + proxy_pass http://prowler-api/accounts/saml/; + 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 X-Forwarded-Host $host; + } + + # Everything else — proxy to prowler-ui + location / { + proxy_pass http://prowler-ui/; + 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 X-Forwarded-Host $host; + + # WebSocket support for Next.js HMR (dev) and live updates + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "ok\n"; + add_header Content-Type text/plain; + } +}