fix(infra): HAProxy HTTPS and stats security
P1.1 - Enable HTTPS in HAProxy for production: - HTTP to HTTPS redirect (301) - HTTPS frontend on port 443 with veza.pem - config/ssl/ structure with README and generate-ssl-cert.sh - docker-compose.prod.yml volume for certs P1.3 - Restrict HAProxy stats to internal network: - ACL from_internal (127.0.0.1, 172.20.0.0/16) - stats admin if from_internal Also: remove errorfile directives (use HAProxy built-in defaults)
This commit is contained in:
parent
66ba082788
commit
b657776892
7 changed files with 123 additions and 27 deletions
24
config/haproxy/README.md
Normal file
24
config/haproxy/README.md
Normal file
|
|
@ -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.
|
||||||
|
|
@ -14,23 +14,17 @@ defaults
|
||||||
timeout client 50000ms
|
timeout client 50000ms
|
||||||
timeout server 50000ms
|
timeout server 50000ms
|
||||||
timeout http-request 10000ms
|
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
|
frontend stats
|
||||||
bind *:8404
|
bind *:8404
|
||||||
stats enable
|
stats enable
|
||||||
stats uri /stats
|
stats uri /stats
|
||||||
stats refresh 30s
|
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)
|
# HTTP FRONTEND (Port 80)
|
||||||
|
|
@ -39,8 +33,8 @@ frontend http_frontend
|
||||||
bind *:80
|
bind *:80
|
||||||
mode http
|
mode http
|
||||||
|
|
||||||
# Redirect HTTP to HTTPS (uncomment for production)
|
# P1.1: Redirect HTTP to HTTPS in production
|
||||||
# redirect scheme https code 301 if !{ ssl_fc }
|
redirect scheme https code 301 if !{ ssl_fc }
|
||||||
|
|
||||||
# ACLs for routing
|
# ACLs for routing
|
||||||
acl is_api path_beg /api/v1
|
acl is_api path_beg /api/v1
|
||||||
|
|
@ -55,23 +49,22 @@ frontend http_frontend
|
||||||
use_backend web_frontend if is_web
|
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
|
frontend https_frontend
|
||||||
# bind *:443 ssl crt /etc/ssl/certs/veza.pem
|
bind *:443 ssl crt /etc/ssl/veza/veza.pem
|
||||||
# mode http
|
mode http
|
||||||
#
|
# ACLs for routing
|
||||||
# # ACLs for routing
|
acl is_api path_beg /api/v1
|
||||||
# acl is_api path_beg /api/v1
|
acl is_ws path_beg /ws
|
||||||
# acl is_ws path_beg /ws
|
acl is_stream path_beg /stream
|
||||||
# acl is_stream path_beg /stream
|
acl is_web path_beg /
|
||||||
# acl is_web path_beg /
|
# Route to appropriate backend
|
||||||
#
|
use_backend backend_api if is_api
|
||||||
# # Route to appropriate backend
|
use_backend chat_ws if is_ws
|
||||||
# use_backend backend_api if is_api
|
use_backend stream_ws if is_stream
|
||||||
# use_backend chat_ws if is_ws
|
use_backend web_frontend if is_web
|
||||||
# use_backend stream_ws if is_stream
|
|
||||||
# use_backend web_frontend if is_web
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# BACKENDS
|
# BACKENDS
|
||||||
|
|
|
||||||
6
config/ssl/.gitignore
vendored
Normal file
6
config/ssl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Never commit certificates or private keys
|
||||||
|
*.pem
|
||||||
|
*.crt
|
||||||
|
*.key
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
0
config/ssl/.gitkeep
Normal file
0
config/ssl/.gitkeep
Normal file
44
config/ssl/README.md
Normal file
44
config/ssl/README.md
Normal file
|
|
@ -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.
|
||||||
|
|
@ -239,11 +239,17 @@ services:
|
||||||
image: haproxy:2.8-alpine
|
image: haproxy:2.8-alpine
|
||||||
container_name: veza_haproxy
|
container_name: veza_haproxy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 128M
|
||||||
ports:
|
ports:
|
||||||
- "${PORT_HAPROXY:-80}:80"
|
- "${PORT_HAPROXY:-80}:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
volumes:
|
volumes:
|
||||||
- ./config/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
|
- ./config/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
|
||||||
|
- ./config/ssl:/etc/ssl/veza:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend-api
|
- backend-api
|
||||||
- chat-server
|
- chat-server
|
||||||
|
|
|
||||||
23
scripts/generate-ssl-cert.sh
Executable file
23
scripts/generate-ssl-cert.sh
Executable file
|
|
@ -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."
|
||||||
Loading…
Reference in a new issue