HTTPS and reverse proxy
View as MarkdownThe web container listens on plain HTTP (port 8080 by default). Put a reverse proxy in front of it to terminate TLS. Everything goes through web: the proxy needs a single upstream and never talks to api or gateway directly.
What the proxy must do
Section titled “What the proxy must do”| Path | Traffic | Proxy requirement |
|---|---|---|
/ |
Static interface | Nothing special. |
/api/ |
REST API and server-sent events (live progress of enrolments and jobs) | Do not buffer responses. Allow idle connections of at least 1 hour. |
/gateway/ |
WebSocket of the web terminal | Pass the Upgrade and Connection headers. Allow connections of up to 9 hours (the gateway closes idle sessions after 15 minutes and any session after 8 hours). |
Also:
- Set
INFRAWARD_PUBLIC_URLin.envto the exact public URL, for examplehttps://infraward.example.com, and rundocker compose up -dafter changing it. The API checks theOriginheader of write requests against it and the enrolment command is built from it. - Bind
webto the loopback interface (INFRAWARD_HTTP_PORT=127.0.0.1:8080) when the proxy runs on the same host, so port 8080 is not reachable from the network. - Send
X-Forwarded-ForandX-Forwarded-Proto, and setINFRAWARD_TRUSTED_PROXYin.envto the proxy’s IP address (or CIDR) as thewebcontainer sees it. Thewebcontainer only believes those headers when they come from that address, and passes a single client IP to the API and the gateway. The audit log, the login rate limits and the terminal sessions use it. Until you set it, they record the proxy’s IP. See Find the proxy’s address. - Do not write
/api/enroll/requests to the proxy’s access log: the single-use enrolment token travels in the path. Thewebcontainer already excludes them from its own log. - The
webcontainer already sendsX-Frame-Options,Content-Security-Policy: frame-ancestors 'none',Strict-Transport-Security,X-Content-Type-OptionsandReferrer-Policy. Do not add them again in the proxy.
Examples
Section titled “Examples”Caddy obtains and renews the certificate automatically and handles WebSocket upgrades without extra settings.
infraward.example.com { # Caddy does not write access logs unless you enable the log directive. # If you do, exclude /api/enroll/*: the enrolment token is in the path. reverse_proxy 127.0.0.1:8080 { # Server-sent events: send each event as soon as it arrives. flush_interval -1 }}server { listen 80; server_name infraward.example.com; return 301 https://$host$request_uri;}
server { listen 443 ssl; http2 on; server_name infraward.example.com;
ssl_certificate /etc/letsencrypt/live/infraward.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/infraward.example.com/privkey.pem;
proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;
location / { proxy_pass http://127.0.0.1:8080; }
# Enrolment script: the single-use token is in the path, keep it out of the log. location /api/enroll/ { proxy_pass http://127.0.0.1:8080; access_log off; }
# REST API and server-sent events. location /api/ { proxy_pass http://127.0.0.1:8080; proxy_buffering off; proxy_read_timeout 1h; }
# Web terminal (WebSocket). location /gateway/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_buffering off; proxy_read_timeout 9h; proxy_send_timeout 9h; }}Attach web to Traefik’s network and declare the router with labels in compose.override.yml (add the file to COMPOSE_FILE in .env). Remove the published port so that only Traefik reaches the container.
services: web: ports: !reset [] networks: [default, traefik] labels: traefik.enable: "true" traefik.docker.network: traefik traefik.http.routers.infraward.rule: Host(`infraward.example.com`) traefik.http.routers.infraward.entrypoints: websecure traefik.http.routers.infraward.tls.certresolver: letsencrypt traefik.http.services.infraward.loadbalancer.server.port: "8080"
networks: traefik: external: trueTraefik forwards WebSocket upgrades and streams responses without buffering by default. If you changed the respondingTimeouts of the entry point, make sure they do not cut long-lived connections.
Find the proxy’s address
Section titled “Find the proxy’s address”The value of INFRAWARD_TRUSTED_PROXY is the address the connections come from inside the container, which is not always the one you expect:
- Proxy on the same host (Caddy or nginx connecting to
127.0.0.1:8080): with Docker it is usually the gateway of the Compose network, not127.0.0.1. - Proxy in a container (Traefik): the IP of the proxy container on the network it shares with
web, or the CIDR of that network if only proxy containers are attached to it.
To find it, open InfraWard through the proxy and read the source address of the requests in the log of web:
docker compose logs --tail 5 webSet it in .env and recreate web:
INFRAWARD_TRUSTED_PROXY=172.18.0.1docker compose up -d webAfter that, the same log shows the real client IPs. The default (127.0.0.1) trusts no proxy. Never use a range that clients can connect from directly: anyone in it could choose the IP that appears in the audit log.
Check the proxy
Section titled “Check the proxy”- Open
https://infraward.example.comand sign in. If the login page loads but signing in returns you to it, the session cookie is being dropped: check that the site is served over HTTPS. - Enrol a server or start a scan and watch the progress page. If the steps only update when you reload, the proxy is buffering
/api/. - Open a terminal on a server with administration access. If it shows Se ha perdido la conexión con el gateway (Lost connection to the gateway) right away, the proxy is not forwarding the WebSocket upgrade on
/gateway/. - Look at your sign-in in Ajustes → Auditoría (Settings → Audit). If it shows the proxy’s IP instead of yours,
INFRAWARD_TRUSTED_PROXYis unset or does not match the proxy.