M-Bus Gateway
← Tilbage til blog
· Nginx· reverse proxy· SSL· Let's Encrypt· rate limiting· SaaS· FastAPI· Docker· sikkerhed

Nginx reverse proxy til SaaS — SSL, rate limiting og routing

Nginx konfiguration til SaaS-platform: SSL/TLS termination med Let's Encrypt, rate limiting, proxy_pass til FastAPI, static file serving og sikkerhedsheaders.

Af M-Bus Gateway

M-Bus Gateway platformen kører Nginx som reverse proxy foran FastAPI. Her er konfigurationen der håndterer SSL, rate limiting og routing.


Arkitektur

Internet
    ↓ HTTPS 443
[Nginx — reverse proxy]
    ↓ HTTP 8000        ↓ HTTP 3000
[FastAPI server]   [React UI (static)]
    ↓
[Mosquitto MQTT 8883]  (Nginx proxier ikke MQTT — direkte)
Nginx håndterer:
  → SSL/TLS termination (Let's Encrypt)
  → HTTP → HTTPS redirect
  → Rate limiting (per IP)
  → Gzip-komprimering
  → Sikkerhedsheaders (HSTS, CSP, X-Frame-Options)
  → Static file serving (React UI fra /var/www/ui)
  → Proxy_pass til FastAPI (/api/v1/)

Basis Nginx-konfiguration

# /etc/nginx/sites-available/mbus-gateway

# Rate limiting: Definer zoner øverst (ikke i server-block):
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=auth:10m rate=10r/m;

# HTTP → HTTPS redirect:
server {
    listen 80;
    server_name mbus-gateway.dk www.mbus-gateway.dk;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name mbus-gateway.dk;

    # Let's Encrypt certifikater:
    ssl_certificate /etc/letsencrypt/live/mbus-gateway.dk/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mbus-gateway.dk/privkey.pem;

    # TLS-konfiguration (moderne profil):
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # HSTS — tving HTTPS i browsere (inkl. subdomæner):
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    # Sikkerhedsheaders:
    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 Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' wss:" always;

    # Gzip:
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
    gzip_min_length 1024;

    # API-endpoints (FastAPI):
    location /api/ {
        limit_req zone=api burst=20 nodelay;

        proxy_pass http://127.0.0.1:8000;
        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;

        # Timeout (generøst for PDF-generering):
        proxy_read_timeout 120s;
        proxy_connect_timeout 10s;
    }

    # Auth-endpoints: Strengere rate limiting:
    location /api/v1/auth/login {
        limit_req zone=auth burst=5 nodelay;
        proxy_pass http://127.0.0.1:8000;
        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;
    }

    # React UI — statiske filer:
    location / {
        root /var/www/ui;
        try_files $uri $uri/ /index.html;  # SPA-routing

        # Cache statiske assets (JS/CSS med content-hash):
        location ~* \.(js|css|woff2|png|svg|ico)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }
    }

    # Health check endpoint (ingen rate limiting, ingen auth):
    location /health {
        proxy_pass http://127.0.0.1:8000/health;
        access_log off;
    }
}

Let's Encrypt automatisk fornyelse

# Installer certbot:
apt install certbot python3-certbot-nginx

# Hent certifikat (Nginx-plugin stopper og genstarter automatisk):
certbot --nginx -d mbus-gateway.dk -d www.mbus-gateway.dk \
  --non-interactive --agree-tos --email admin@mbus-gateway.dk

# Certbot tilføjer automatisk fornyelsescron:
# /etc/cron.d/certbot: 0 */12 * * * certbot renew --quiet

# Test fornyelse:
certbot renew --dry-run

Docker Compose med Nginx

# docker-compose.yml
services:
  nginx:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/sites/:/etc/nginx/sites-available/:ro
      - /var/www/ui:/var/www/ui:ro  # React UI build
      - /etc/letsencrypt:/etc/letsencrypt:ro
      - nginx_logs:/var/log/nginx
    depends_on:
      - server
    restart: unless-stopped

  server:
    build: .
    expose:
      - "8000"  # Kun intern adgang — Nginx proxier
    environment:
      - DATABASE_URL=postgresql+asyncpg://...
    restart: unless-stopped

volumes:
  nginx_logs:

Rate limiting: Beskyt mod brute force

# Avanceret rate limiting med burst og delay:

# Login — max 10 forsøg/min, burst af 5:
location /api/v1/auth/login {
    limit_req zone=auth burst=5 nodelay;
    limit_req_status 429;

    # Return JSON ved rate limit (ikke standard Nginx HTML):
    error_page 429 = @rate_limited;
    proxy_pass http://127.0.0.1:8000;
    ...
}

location @rate_limited {
    default_type application/json;
    return 429 '{"detail": "For mange forsøg. Vent 1 minut."}';
}

# API generelt — 30 req/min, burst af 20:
location /api/ {
    limit_req zone=api burst=20 nodelay;
    ...
}

Konklusion

Nginx som reverse proxy for FastAPI giver SSL-terminering, rate limiting og statisk file serving i ét lag — uden at belaste Python-processer. Let's Encrypt automatiserer certifikathåndtering. Strengere rate limiting på auth-endpoints er kritisk — uden det kan brute-force-angreb teste tusindvis af passwords pr. minut.

Se Hetzner Docker Compose guide eller JWT auth guide.