TLS / certificate¶
Five paths — the second one is right for most installations.
| Path | When |
|---|---|
| 1 — Self-signed | default after installation, for testing |
| 2 — Certificate from Vesana | Let's Encrypt or a company CA, Vesana renews on its own |
| 3 — Upload your own certificate | purchased certificate, company CA without ACME |
| 4 — By hand (certbot etc.) | existing processes on the host |
| 5 — Reverse proxy in front | TLS already ends in front of Vesana |
Path 1 — Self-signed (default)¶
Without a certificate of your own, the nginx container creates a self-signed pair on first start (10 years, CN = host from BASE_URL) in the ssl volume. HTTPS works immediately, but browsers warn, and agents only connect with a certificate pin (the install command brings it along).
Path 2 — Certificate straight from Vesana¶
Admin → System → Certificate. Vesana obtains the certificate itself via ACME (the Let's Encrypt protocol), installs it and renews it in time — no certbot, no cron job, no manual work on the host. The compose file stays as it is.
1. Names¶
Every name Vesana is reached under: the browser address, the agents' server URL, and in split-container mode the admin host. Wildcards (*.company.com) only work with DNS validation; a wildcard covers exactly one level (a.company.com), not company.com itself and not a.b.company.com.
2. Validation and CA¶
| Validation | Requirement |
|---|---|
| DNS provider (recommended) | Vesana briefly creates a TXT record _acme-challenge.<name> through your DNS provider's API. No inbound port needed, wildcards possible. |
| HTTP via port 80 | The name must reach this server on port 80 from the internet. Vesana then forwards only /.well-known/acme-challenge/ to itself; everything else keeps redirecting to HTTPS. If the name has an AAAA record, Let's Encrypt validates over IPv6 first. |
CA: Let's Encrypt, Let's Encrypt (test) (staging — certificates nobody trusts; for trying things out) or your own ACME CA (e.g. step-ca, Smallstep, Sectigo/HARICA with EAB). For your own CA enter its directory URL, if required the EAB key ID and key, and — if the CA itself uses a company certificate — its root.
3. DNS provider¶
Vesana knows every DNS provider lego supports (more than 200, including OVH, Cloudflare, IONOS, Hetzner, Route 53, Azure DNS, Google Cloud DNS, Gandi, INWX, Netcup, Infomaniak, deSEC, PowerDNS, RFC2136 and acme-dns). The fields for each provider appear automatically; “Guide for …” opens the provider's page with the permissions the account needs.
Example OVH: OVH supports three sign-in methods — application key + secret + consumer key, OAuth2 (client ID + secret) or an access token, each with OVH_ENDPOINT (e.g. ovh-eu). ONE of them is enough. For the consumer key, POST and DELETE on /domain/zone/* suffice.
Test access creates a test record, waits until it is visible and removes it again — without asking the CA. You see immediately whether credentials and permissions work.
Credentials are stored encrypted in the database (AES-256-GCM) and never appear in plain text in any response, log or support bundle. Give the account permissions for the one zone only.
Windows DNS / DNS without an API¶
Two ways:
- CNAME delegation: create one CNAME per name in your zone pointing to a zone that has an API, and choose that provider: Vesana follows the CNAME and writes the TXT record into the delegated zone.
- RFC2136 (provider “RFC2136”/dnsupdate): dynamic update straight to the DNS server — with a TSIG key or, for Active Directory, with Kerberos (GSS-TSIG:
DNSUPDATE_TSIG_GSS_REALM,…_USERNAME,…_PASSWORD).
Company network without outbound access¶
Vesana checks whether the check record has arrived at the zone’s authoritative name servers — that is where the CA looks as well. Answers from resolvers in your own network are shown for information only: a resolver asked too early remembers “does not exist” for the zone’s negative caching time (often 10–60 minutes).
If the network blocks outbound DNS (port 53), enter your own resolvers under “Resolvers and propagation” and choose “Check via resolvers only” — or “Just wait” with a fixed delay. If the server cannot reach the CA at all, the error message says so; then an own CA inside the network or path 3 remain.
4. Issue and install¶
- Show and accept the CA's terms of service (once per CA).
- Dry run: issues a test certificate from Let's Encrypt staging (does not count against the limits) and installs nothing.
- Issue: the certificate is then “ready”. If a dry run (or another issuance) for the same names ran shortly before, Vesana first waits for the lifetime (TTL) of the check record — usually 2 to 5 minutes, shown with a time: the CA’s resolvers remember the old record that long, and a check before would be rejected.
- Pre-check: Vesana names every device (agent, collector) whose server URL the new certificate does not cover — e.g. because it reaches Vesana via an IP address. Move such devices to a covered name first via Admin → Agents & Collectors → Agent Connection. With findings, installing requires explicit confirmation.
- Install: the updater checks the pair again, writes it atomically, tests the nginx configuration, reloads every nginx role (web UI, agent gateway, admin port) and verifies via a TLS connection that the new certificate is served. If any step fails, the previous version is back immediately.
- Control phase (30 minutes): Vesana watches whether agents/collectors that were in contact before keep reporting, and names every one that goes silent.
Renewal¶
Vesana renews on its own: inside the renewal window the CA announces (ARI), otherwise when a third of the lifetime is left (90 days ⇒ after 60 days). If a renewal fails, Vesana retries after 1 h, 6 h and then daily — never in a tight loop. The cause is shown on the page, in the instance health and in the diagnosis log (category “Certificate”); if it lasts longer than 48 hours, the “TLS Certificate” check on the “Vesana Server” host warns through the normal alert rules — weeks before expiry.
Roll back, turn off¶
- Roll back reinstalls the previous version — if there is none, the certificate served before Vesana installed its first one (usually the self-signed one).
- ⚠️ HSTS: Vesana sends
Strict-Transport-Security. Once browsers have seen Vesana with a valid certificate, they block the name for up to a year if a self-signed one comes back — without “proceed anyway”. Rolling back to a self-signed certificate therefore requires explicit confirmation. - Turn off ACME keeps the active certificate until it expires (with expiry warning) but no longer renews it.
Agents and collectors¶
Agents and collectors accept a publicly trusted certificate (Let's Encrypt) that covers the server URL's name without a pin — including those that were connected to the self-signed one with a pin. Vesana never pins an ACME certificate (it changes with every renewal). If the certificate comes from a company CA, its root must be in the devices' trust store. The local Active Collector connects via localhost and is not affected.
Backup and moving¶
Certificate, key, ACME account and credentials live in the database. After a restore onto a fresh machine, Vesana reinstalls the certificate on its own within 15 minutes.
Path 3 — Upload your own certificate¶
Admin → System → Certificate → Upload own certificate. Paste the chain (server certificate first, then intermediates) and the unencrypted key as PEM, or choose files. Vesana checks that the key matches, the chain is connected and the certificate is valid; it is installed through the same pre-check as path 2. The expiry warning applies the same way.
Path 4 — By hand (certbot etc.)¶
The stack nginx reads exactly ONE pair from the ssl volume: /etc/nginx/ssl/fullchain.pem + privkey.pem. It applies to every host name this nginx serves — with split-container mode active, also to the admin name.
cd /opt/vesana # your Vesana directory
docker compose -f docker-compose.prod.yml cp /etc/letsencrypt/live/vesana.example.com/fullchain.pem nginx:/etc/nginx/ssl/fullchain.pem
docker compose -f docker-compose.prod.yml cp /etc/letsencrypt/live/vesana.example.com/privkey.pem nginx:/etc/nginx/ssl/privkey.pem
docker compose -f docker-compose.prod.yml exec nginx nginx -s reload
If the agent gateway or a dedicated admin container is running, they read the same pair — reload them as well (… exec agent-gateway nginx -s reload or … exec nginx-admin nginx -s reload).
Vesana recognises a certificate installed this way (“installed manually”), never silently overwrites it, and monitors its expiry (check “TLS Certificate”).
Path 5 — Reverse proxy in front¶
If an nginx / Traefik / HAProxy / Caddy already sits in front of the server and terminates TLS.
The Vesana stack itself always speaks HTTPS (at least with the self-signed certificate); its HTTP port only redirects to HTTPS. The reverse proxy therefore forwards via HTTPS to the stack's HTTPS port. If the proxy holds 80/443, give Vesana other ports in the .env:
(Not 8443 — that is the agent gateway's default port.) Then recreate the stack.
In the external reverse proxy (nginx example):
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 https://127.0.0.1:9443;
proxy_ssl_verify off; # stack certificate is self-signed; the connection stays on 127.0.0.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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Login response headers are larger than nginx's default buffer (4 KB).
# Without these lines: "upstream sent too big header" -> 502 on login.
proxy_buffer_size 32k;
proxy_buffers 8 32k;
proxy_busy_buffers_size 64k;
proxy_request_buffering off;
proxy_read_timeout 1800s;
client_max_body_size 20g; # backup upload and offline update
}
}
Then three points:
- Real client addresses: add the proxy's address or network under Admin → Login & Security → Admin Access → Advanced as a trusted proxy. Otherwise the session list, audit log and admin allowlist see every user with the proxy's address. There is no environment variable for this.
- Don't decrypt the agent gateway: if the agent gateway is enabled, pass the gateway port through as a plain TCP stream or leave it directly reachable — agents and collectors check the stack's certificate there.
- Agents behind the proxy: the install command checks what really answers under the agent server URL: a publicly trusted proxy certificate needs no pin, any other gets its own.
A certificate from Vesana (path 2) with HTTP validation only works behind a proxy if it forwards /.well-known/acme-challenge/ to the stack's HTTP port; DNS validation is simpler — or leave the certificate to the proxy.
CSP¶
The built-in nginx sends a strict Content Security Policy and enforces it: scripts, styles, images, connections and fonts are only allowed from the instance itself (plus the map tiles from OpenStreetMap and OpenFreeMap and Google Fonts). The policy's frame-ancestors 'none' and X-Frame-Options: SAMEORIGIN prevent embedding Vesana in other sites; only a dashboard share link may be embedded, and only for the origins entered there. Browsers report violations to the instance; they appear in the diagnose log (category “Security”). A reverse proxy in front must not strip the headers and should not inject its own scripts into the page, they would be blocked.
Header hygiene¶
The built-in nginx sets:
Strict-Transport-Security: max-age=31536000; includeSubDomainsX-Content-Type-Options: nosniffX-Frame-Options: SAMEORIGINX-XSS-Protection: 1; mode=blockReferrer-Policy: strict-origin-when-cross-originPermissions-Policy: geolocation=(), microphone=(), camera=()Content-Security-Policy: …
Behind your own reverse proxy these headers already come from the stack; the proxy only has to pass them through.
TLS test¶
External tools:
- SSL Labs test for cert chain and cipher suite rating
testssl.shfor local validation