201 lines
5.6 KiB
Markdown
201 lines
5.6 KiB
Markdown
---
|
|
title: Configure Reverse Proxy Correctly
|
|
impact: LOW-MEDIUM
|
|
impactDescription: HTTPS, caching, rate limiting, and security headers
|
|
tags: production, nginx, caddy, https, proxy
|
|
---
|
|
|
|
## Configure Reverse Proxy Correctly
|
|
|
|
Use a reverse proxy (Nginx, Caddy) for HTTPS termination, caching, rate limiting, and security headers.
|
|
|
|
**Incorrect (exposing PocketBase directly):**
|
|
|
|
```bash
|
|
# Direct exposure - no HTTPS, no rate limiting
|
|
./pocketbase serve --http="0.0.0.0:8090"
|
|
|
|
# Port forwarding without proxy
|
|
iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8090
|
|
# Still no HTTPS!
|
|
```
|
|
|
|
**Correct (Caddy - simplest option):**
|
|
|
|
```caddyfile
|
|
# /etc/caddy/Caddyfile
|
|
myapp.com {
|
|
# Automatic HTTPS via Let's Encrypt
|
|
reverse_proxy 127.0.0.1:8090 {
|
|
# Required for SSE/Realtime
|
|
flush_interval -1
|
|
}
|
|
|
|
# Security headers
|
|
header {
|
|
X-Content-Type-Options "nosniff"
|
|
X-Frame-Options "DENY"
|
|
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
|
|
Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
|
|
Referrer-Policy "strict-origin-when-cross-origin"
|
|
-Server
|
|
}
|
|
|
|
# Restrict admin UI to internal/VPN networks
|
|
# @admin path /_/*
|
|
# handle @admin {
|
|
# @blocked not remote_ip 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
|
|
# respond @blocked 403
|
|
# reverse_proxy 127.0.0.1:8090
|
|
# }
|
|
|
|
# Rate limiting (requires caddy-ratelimit plugin)
|
|
# Install: xcaddy build --with github.com/mholt/caddy-ratelimit
|
|
# Without this plugin, use PocketBase's built-in rate limiter (--rateLimiter=true)
|
|
# rate_limit {
|
|
# zone api {
|
|
# key {remote_host}
|
|
# events 100
|
|
# window 1m
|
|
# }
|
|
# }
|
|
}
|
|
```
|
|
|
|
**Correct (Nginx configuration):**
|
|
|
|
```nginx
|
|
# /etc/nginx/sites-available/pocketbase
|
|
|
|
# Rate limit zones must be defined in http context (e.g., /etc/nginx/nginx.conf)
|
|
# limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
|
|
|
upstream pocketbase {
|
|
server 127.0.0.1:8090;
|
|
keepalive 64;
|
|
}
|
|
|
|
server {
|
|
listen 80;
|
|
server_name myapp.com;
|
|
return 301 https://$server_name$request_uri;
|
|
}
|
|
|
|
server {
|
|
listen 443 ssl http2;
|
|
server_name myapp.com;
|
|
|
|
# SSL configuration
|
|
ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
|
ssl_prefer_server_ciphers off;
|
|
|
|
# Security headers
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-Frame-Options "DENY" always;
|
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
# Note: X-XSS-Protection is deprecated and can introduce vulnerabilities.
|
|
# Use Content-Security-Policy instead.
|
|
|
|
location / {
|
|
proxy_pass http://pocketbase;
|
|
proxy_http_version 1.1;
|
|
|
|
# Headers
|
|
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;
|
|
|
|
# SSE/Realtime support
|
|
proxy_set_header Connection '';
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
chunked_transfer_encoding off;
|
|
|
|
# Timeouts
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
}
|
|
|
|
# Rate limit API endpoints
|
|
location /api/ {
|
|
limit_req zone=api burst=20 nodelay;
|
|
|
|
proxy_pass http://pocketbase;
|
|
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 Connection '';
|
|
proxy_buffering off;
|
|
}
|
|
|
|
# Static file caching
|
|
location /api/files/ {
|
|
proxy_pass http://pocketbase;
|
|
proxy_cache_valid 200 1d;
|
|
expires 1d;
|
|
add_header Cache-Control "public, immutable";
|
|
}
|
|
|
|
# Gzip compression
|
|
gzip on;
|
|
gzip_types text/plain application/json application/javascript text/css;
|
|
gzip_min_length 1000;
|
|
}
|
|
```
|
|
|
|
**Docker Compose with Caddy:**
|
|
|
|
```yaml
|
|
# docker-compose.yml
|
|
version: '3.8'
|
|
|
|
services:
|
|
pocketbase:
|
|
# NOTE: This is a third-party community image, not officially maintained by PocketBase.
|
|
# For production, consider building your own image from the official PocketBase binary.
|
|
# See: https://pocketbase.io/docs/going-to-production/
|
|
image: ghcr.io/muchobien/pocketbase:latest
|
|
restart: unless-stopped
|
|
volumes:
|
|
- ./pb_data:/pb_data
|
|
environment:
|
|
- PB_ENCRYPTION_KEY=${PB_ENCRYPTION_KEY}
|
|
|
|
caddy:
|
|
image: caddy:2-alpine
|
|
restart: unless-stopped
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
volumes:
|
|
- ./Caddyfile:/etc/caddy/Caddyfile
|
|
- caddy_data:/data
|
|
- caddy_config:/config
|
|
depends_on:
|
|
- pocketbase
|
|
|
|
volumes:
|
|
caddy_data:
|
|
caddy_config:
|
|
```
|
|
|
|
**Key configuration points:**
|
|
|
|
| Feature | Why It Matters |
|
|
|---------|---------------|
|
|
| HTTPS | Encrypts traffic, required for auth |
|
|
| SSE support | `proxy_buffering off` for realtime |
|
|
| Rate limiting | Prevents abuse |
|
|
| Security headers | XSS/clickjacking protection |
|
|
| Keepalive | Connection reuse, better performance |
|
|
|
|
Reference: [PocketBase Going to Production](https://pocketbase.io/docs/going-to-production/)
|