Skip to main content

TLS and security headers

Certificates​

  • Caddy obtains and renews Let's Encrypt certificates for every name it serves automatically (PLATFORM_DOMAIN, RMM_DOMAIN, RMM_FILES_DOMAIN and each name in PLATFORM_OLD_DOMAINS; ACME_EMAIL receives expiry notices). Every name needs a DNS record pointing at the host and port 80 reachable (HTTP-01 challenge). The certificates live in the caddy-data volume; back it up to avoid re-issuing after a rebuild.
  • To use your own certificate, replace automatic TLS in the site blocks of deploy/Caddyfile with tls /path/cert.pem /path/key.pem and mount the files into the web container.
  • For labs without public DNS, see Installation → Lab installation.

Sites​

deploy/Caddyfile always serves these sites:

SiteServes
PLATFORM_DOMAINThe Hub's app at / and its API at /api/platform/*; Axis's portal at /axis/* and its API at /axis/api/* (prefix stripped, to the backends); the documentation at /docs/*; /manage/api/* for agents enrolled there, other /manage/* → 308 to /axis/*. /api/platform/internal/* and any other /api/* → 404.
RMM_DOMAINAxis's host from before the Hub: /api/* and /manage/api/* to the backends (enrolled agents, connectors, scripts); /docs/* and /axis/* → 308 to the same path on PLATFORM_DOMAIN; everything else (old portal pages) → 308 to https://PLATFORM_DOMAIN/axis{uri}.
PLATFORM_OLD_DOMAINS (optional)Earlier Hub names: /axis/api/* and /manage/api/* to the backends; everything else → 308 to the same path on PLATFORM_DOMAIN (/manage/* pages to /axis/*).
RMM_FILES_DOMAINObject storage (MinIO) for presigned downloads and uploads.

The remote desktop relay between replicas (/api/remote/v1/internal/*) is refused (404) on every public host; replicas reach each other directly.

Behind an existing nginx​

On a host where nginx already serves other sites on ports 80 and 443 (production since 2026-09-27: the website, the helpdesk and Vaultwarden), Caddy is published on loopback and nginx routes each TLS connection by its server name (SNI) without decrypting it. Caddy keeps its own certificates, WebSockets and port 8322; the other sites keep theirs.

  1. In deploy/.env: WEB_HTTP_PORT=127.0.0.1:8080 and WEB_HTTPS_PORT=127.0.0.1:8443 (HTTP/3 on 443/udp and 8322 stay published directly).

  2. apt install libnginx-mod-stream, and at the top level of /etc/nginx/nginx.conf: stream { include /etc/nginx/stream.d/*.conf; }.

  3. /etc/nginx/stream.d/sni.conf:

    map $ssl_preread_server_name $sni_upstream {
    hub.entrosity.com 127.0.0.1:10444;
    manage.entrosity.com 127.0.0.1:10444;
    portal.entrosity.com 127.0.0.1:10444;
    files.manage.entrosity.com 127.0.0.1:10444;
    media.entrosity.com 127.0.0.1:10444;
    default 127.0.0.1:10443; # nginx's own HTTPS sites
    }
    server {
    listen 443;
    listen [::]:443;
    ssl_preread on;
    proxy_protocol on; # the client's address for the next hop
    proxy_pass $sni_upstream;
    proxy_connect_timeout 5s;
    proxy_timeout 24h; # agents keep WebSockets open for hours
    }
    # Strips the PROXY header before Caddy (the Caddyfile stays unchanged).
    server {
    listen 127.0.0.1:10444 proxy_protocol;
    proxy_pass 127.0.0.1:8443;
    proxy_timeout 24h;
    }
  4. Every existing HTTPS server of nginx listens on listen 127.0.0.1:10443 ssl proxy_protocol; instead of 443/[::]:443, and /etc/nginx/conf.d/00-realip.conf restores the client address: set_real_ip_from 127.0.0.1; real_ip_header proxy_protocol;.

  5. /etc/nginx/conf.d/entrosity-platform.conf hands port 80 of the Entrosity names to Caddy (redirects to HTTPS, HTTP-01 challenges): a server with listen 80;, the five names and location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; }.

  6. nginx -t && systemctl restart nginx (a restart, not a reload: port 443 moves from http to stream).

Caddy sees the connections from nginx, not the client's address; the names behind Cloudflare had Cloudflare's address before anyway.

warning

A new server name for Caddy must be added to the map, otherwise it reaches nginx. Tools that rewrite nginx's configuration put listen 443 back and nginx then fails to start: after bench setup nginx (Frappe helpdesk) or certbot --nginx for a new name, change their listen lines as in step 4 again. certbot renew is not affected.

Headers​

The Hub and Axis (under /axis/) are served with:

  • HSTS (one year, including subdomains);
  • a strict Content Security Policy;
  • X-Frame-Options: DENY, X-Content-Type-Options: nosniff;
  • Referrer and Permissions policies.

The documentation under /docs/ gets its own policy: it also allows inline scripts and blob workers, which the static Docusaurus build and the API reference need. The portals' policy is unchanged.

The files host sends Content-Security-Policy: default-src 'none'; sandbox, and uploads are always stored as application/octet-stream, so nothing uploaded can run in a browser.

Agents and TLS inspection​

Agents and connectors validate the server certificate against the Windows trust store. A TLS-intercepting proxy on the customer network must either be trusted by the machines or excluded for the server names (the address the device enrolled with, and the files host). Symptom otherwise: x509: certificate signed by unknown authority in the agent log.

WebSockets must pass through web proxies. If they are blocked, agents fall back to HTTPS polling (Troubleshooting).