# HTTPS and reverse proxy

> Serve InfraWard over HTTPS behind Caddy, nginx or Traefik, with the settings that server-sent events and the web terminal need.

The `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

| 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_URL` in `.env` to the exact public URL, for example `https://infraward.example.com`, and run `docker compose up -d` after changing it. The API checks the `Origin` header of write requests against it and the enrolment command is built from it.
- Bind `web` to 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-For` and `X-Forwarded-Proto`, and set `INFRAWARD_TRUSTED_PROXY` in `.env` to the proxy's IP address (or CIDR) as the `web` container sees it. The `web` container 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](#find-the-proxys-address).
- Do not write `/api/enroll/` requests to the proxy's access log: the single-use enrolment token travels in the path. The `web` container already excludes them from its own log.
- The `web` container already sends `X-Frame-Options`, `Content-Security-Policy: frame-ancestors 'none'`, `Strict-Transport-Security`, `X-Content-Type-Options` and `Referrer-Policy`. Do not add them again in the proxy.

## Examples

**Caddy**

Caddy obtains and renews the certificate automatically and handles WebSocket upgrades without extra settings.

```text title="/etc/caddy/Caddyfile"
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
	}
}
```

**nginx**

```nginx title="/etc/nginx/sites-available/infraward.conf"
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;
    }
}
```

**Traefik**

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.

```yaml title="compose.override.yml"
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: true
```

Traefik 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

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, not `127.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`:

```bash
docker compose logs --tail 5 web
```

Set it in `.env` and recreate `web`:

```bash title=".env"
INFRAWARD_TRUSTED_PROXY=172.18.0.1
```

```bash
docker compose up -d web
```

After 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

1. Open `https://infraward.example.com` and 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.
2. 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/`.
3. 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/`.
4. Look at your sign-in in **Ajustes → Auditoría** (Settings → Audit). If it shows the proxy's IP instead of yours, `INFRAWARD_TRUSTED_PROXY` is unset or does not match the proxy.

> **Internal certificate authority**
>
> If OpenBao or the OIDC provider use certificates signed by your own CA, mount the CA file into `api` and `worker` and set `NODE_EXTRA_CA_CERTS=/path/to/ca.pem` in their environment.

---

HTML page: https://infraward.net/docs/reverse-proxy/
