diff --git a/config/haproxy/README.md b/config/haproxy/README.md new file mode 100644 index 000000000..4a1d548a9 --- /dev/null +++ b/config/haproxy/README.md @@ -0,0 +1,24 @@ +# HAProxy Configuration + +## Production (haproxy.cfg) + +- **HTTP (port 80)**: Redirects all traffic to HTTPS (301) +- **HTTPS (port 443)**: Serves traffic with TLS. Certificates from `config/ssl/` mounted at `/etc/ssl/veza/` +- **Stats (port 8404)**: Restricted to localhost and Docker network (172.20.0.0/16) + +## SSL Certificates + +Before starting production, add at least one certificate to `config/ssl/`. See `config/ssl/README.md` for instructions. + +For quick local testing with self-signed cert: + +```bash +cd config/ssl +openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout key.pem -out cert.pem -subj "/CN=veza.local" +cat cert.pem key.pem > veza.pem +``` + +## Development Without HTTPS + +For local development without SSL, use `docker-compose.yml` (not prod) or create a `haproxy.dev.cfg` that omits the HTTPS frontend and HTTP redirect. diff --git a/config/haproxy/haproxy.cfg b/config/haproxy/haproxy.cfg index 910f1b98b..f02cc5543 100644 --- a/config/haproxy/haproxy.cfg +++ b/config/haproxy/haproxy.cfg @@ -14,23 +14,17 @@ defaults timeout client 50000ms timeout server 50000ms timeout http-request 10000ms - errorfile 400 /etc/haproxy/errors/400.http - errorfile 403 /etc/haproxy/errors/403.http - errorfile 408 /etc/haproxy/errors/408.http - errorfile 500 /etc/haproxy/errors/500.http - errorfile 502 /etc/haproxy/errors/502.http - errorfile 503 /etc/haproxy/errors/503.http - errorfile 504 /etc/haproxy/errors/504.http # ============================================================================ -# STATS & MONITORING +# STATS & MONITORING (P1.3: restricted to internal network) # ============================================================================ frontend stats bind *:8404 stats enable stats uri /stats stats refresh 30s - stats admin if TRUE + acl from_internal src 127.0.0.1 172.20.0.0/16 + stats admin if from_internal # ============================================================================ # HTTP FRONTEND (Port 80) @@ -39,8 +33,8 @@ frontend http_frontend bind *:80 mode http - # Redirect HTTP to HTTPS (uncomment for production) - # redirect scheme https code 301 if !{ ssl_fc } + # P1.1: Redirect HTTP to HTTPS in production + redirect scheme https code 301 if !{ ssl_fc } # ACLs for routing acl is_api path_beg /api/v1 @@ -55,23 +49,22 @@ frontend http_frontend use_backend web_frontend if is_web # ============================================================================ -# HTTPS FRONTEND (Port 443) - Uncomment and configure for production +# HTTPS FRONTEND (Port 443) - P1.1: Production HTTPS +# Certificates from config/ssl/ mounted at /etc/ssl/veza/ # ============================================================================ -# frontend https_frontend -# bind *:443 ssl crt /etc/ssl/certs/veza.pem -# mode http -# -# # ACLs for routing -# acl is_api path_beg /api/v1 -# acl is_ws path_beg /ws -# acl is_stream path_beg /stream -# acl is_web path_beg / -# -# # Route to appropriate backend -# use_backend backend_api if is_api -# use_backend chat_ws if is_ws -# use_backend stream_ws if is_stream -# use_backend web_frontend if is_web +frontend https_frontend + bind *:443 ssl crt /etc/ssl/veza/veza.pem + mode http + # ACLs for routing + acl is_api path_beg /api/v1 + acl is_ws path_beg /ws + acl is_stream path_beg /stream + acl is_web path_beg / + # Route to appropriate backend + use_backend backend_api if is_api + use_backend chat_ws if is_ws + use_backend stream_ws if is_stream + use_backend web_frontend if is_web # ============================================================================ # BACKENDS diff --git a/config/ssl/.gitignore b/config/ssl/.gitignore new file mode 100644 index 000000000..c4fac95b7 --- /dev/null +++ b/config/ssl/.gitignore @@ -0,0 +1,6 @@ +# Never commit certificates or private keys +*.pem +*.crt +*.key +*.p12 +*.pfx diff --git a/config/ssl/.gitkeep b/config/ssl/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/config/ssl/README.md b/config/ssl/README.md new file mode 100644 index 000000000..1b1a9130c --- /dev/null +++ b/config/ssl/README.md @@ -0,0 +1,44 @@ +# SSL Certificates for HAProxy + +This directory holds SSL certificates for HTTPS in production. **Never commit certificates or private keys** (see `.gitignore`). + +## Required for Production HTTPS + +HAProxy expects a single combined PEM file: **`veza.pem`** containing certificate + private key (concatenated). The config uses `crt /etc/ssl/veza/veza.pem` to avoid loading non-cert files (e.g. README.md). + +## Obtaining Certificates + +### Option 1: Let's Encrypt (Production) + +```bash +# Standalone mode (stop HAProxy first) +certbot certonly --standalone -d yourdomain.com + +# Copy to config +cat /etc/letsencrypt/live/yourdomain.com/fullchain.pem \ + /etc/letsencrypt/live/yourdomain.com/privkey.pem > config/ssl/veza.pem +``` + +### Option 2: Self-Signed (Development/Staging) + +```bash +openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout config/ssl/key.pem -out config/ssl/cert.pem \ + -subj "/CN=veza.local" + +cat config/ssl/cert.pem config/ssl/key.pem > config/ssl/veza.pem +``` + +## Docker Volume + +`docker-compose.prod.yml` mounts this directory to `/etc/ssl/veza` in the HAProxy container. **You must create `veza.pem` before starting production** — the HAProxy healthcheck will fail otherwise. + +## Quick Start (First-Time Setup) + +Run from repo root: + +```bash +./scripts/generate-ssl-cert.sh +``` + +This creates a self-signed certificate for `veza.local`. For production, replace with Let's Encrypt or your CA. diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 55275bd4e..243056523 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -239,11 +239,17 @@ services: image: haproxy:2.8-alpine container_name: veza_haproxy restart: unless-stopped + deploy: + resources: + limits: + cpus: '0.5' + memory: 128M ports: - "${PORT_HAPROXY:-80}:80" - "443:443" volumes: - ./config/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro + - ./config/ssl:/etc/ssl/veza:ro depends_on: - backend-api - chat-server diff --git a/scripts/generate-ssl-cert.sh b/scripts/generate-ssl-cert.sh new file mode 100755 index 000000000..55e13d92c --- /dev/null +++ b/scripts/generate-ssl-cert.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Generate a self-signed SSL certificate for local/staging HAProxy. +# For production, use Let's Encrypt or your CA. +# Usage: ./scripts/generate-ssl-cert.sh [domain] + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +SSL_DIR="$REPO_ROOT/config/ssl" +DOMAIN="${1:-veza.local}" + +mkdir -p "$SSL_DIR" +cd "$SSL_DIR" + +echo "Generating self-signed certificate for $DOMAIN..." +openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout key.pem -out cert.pem \ + -subj "/CN=$DOMAIN" + +cat cert.pem key.pem > veza.pem +echo "Created config/ssl/veza.pem" +echo "Add key.pem and cert.pem to .gitignore if not already excluded."