TLS / reverse proxy¶
Three paths.
Path 1 — Let's Encrypt from setup wizard¶
When BASE_URL is a domain and port 80 is reachable from outside, the setup wizard auto-issues a Let's Encrypt cert. Renewal in background.
Prerequisites:
- DNS A/AAAA record points to server
- Ports 80 + 443 reachable from outside
- Nothing else on port 80 (no existing nginx)
Failure mode: if validation fails, Vesana falls back to a self-signed cert and shows a browser warning. Can be retried in Admin → System → TLS.
Path 2 — Custom certificate¶
You have a cert from your own CA or a hardware cert.
sudo cp your-cert.pem /opt/vesana/ssl/cert.pem
sudo cp your-key.pem /opt/vesana/ssl/key.pem
sudo chmod 600 /opt/vesana/ssl/key.pem
docker compose -f /opt/vesana/docker-compose.prod.yml exec nginx nginx -s reload
Cert format: PEM (X.509 with intermediate chain). Key: unencrypted PEM. On renewal, copy again + reload.
Path 3 — Reverse proxy in front¶
When an nginx / Traefik / HAProxy / Caddy already fronts the server and TLS terminates there:
In .env:
TRUST_PROXY=true
HTTP_PORT=8080 # instead of 80
HTTPS_PORT=8443 # instead of 443 — we don't need TLS here, but the container wants the port
In the external reverse proxy:
server {
listen 443 ssl http2;
server_name vesana.example.com;
ssl_certificate /etc/letsencrypt/live/vesana.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vesana.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Receiver endpoint for agent / collector
location /receiver {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_request_buffering off;
client_max_body_size 50m; # for compressed log packets
}
}
Important: set TRUST_PROXY=true, otherwise the backend sees the reverse proxy IP, not the real client IP — audit log and rate limit would be wrong.
Test self-host (example)¶
The test instance test.dailycrust.it runs an nginx in front terminating TLS and proxying onto the stack at ports 8180/8181:
server {
listen 443 ssl;
server_name test.dailycrust.it;
ssl_certificate /etc/letsencrypt/live/test.dailycrust.it/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/test.dailycrust.it/privkey.pem;
location / {
proxy_pass https://127.0.0.1:8181;
proxy_ssl_verify off; # internal stack has self-signed
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
}
proxy_ssl_verify off is acceptable internally — traffic on 127.0.0.1 isn't public.
CSP¶
The frontend sets a strict CSP. For your own iframes or embedded content, extend iframe_allowlist in system_settings.
Header hygiene¶
The built-in nginx sets:
Strict-Transport-Security: max-age=31536000; includeSubDomainsX-Content-Type-Options: nosniffX-Frame-Options: SAMEORIGINReferrer-Policy: strict-origin-when-cross-originPermissions-Policy: geolocation=(), microphone=()
In your reverse proxy: set the same set.
TLS test¶
External tools:
- SSL Labs Test for cert chain and cipher suite rating
testssl.shfor local validation
Goal: A or A+ at SSL Labs.