Skip to content

Put Mosaica behind TLS

Browsers should reach Mosaica's viewer address over TLS, and preferably from the same origin as the page that shows the map. Mosaica doesn't terminate TLS itself, so this guide puts nginx in front of it. It assumes an install from the systemd guide, with the viewer address on 127.0.0.1:9151. Under Docker, point nginx at 127.0.0.1:9161 instead.

Only the viewer address goes behind the proxy. The session and control addresses stay private, as Addresses and credentials explains.

The viewer address also takes passwords, at POST /v1/login, and Mosaica accepts them over plain HTTP. A deployment whose users log in with a password needs this proxy for that reason alone.

Configure nginx

This configuration serves your application at maps.example.org and passes /v1/ to Mosaica:

server {
    listen 443 ssl http2;
    server_name maps.example.org;

    ssl_certificate     /etc/ssl/maps.example.org/fullchain.pem;
    ssl_certificate_key /etc/ssl/maps.example.org/privkey.pem;

    location /v1/ {
        proxy_pass http://127.0.0.1:9151;
        proxy_http_version 1.1;
        proxy_buffering off;
        client_max_body_size 2m;
    }

    location = /readyz {
        proxy_pass http://127.0.0.1:9151;
    }

    location / {
        proxy_pass http://127.0.0.1:8080;
    }
}

127.0.0.1:8080 stands for your application. /readyz is passed through so that a load balancer or monitor outside the machine can check the server, as Health and readiness describes. It needs no credential and answers with a status code alone.

A map response is streamed. The counts for each tile come first and the points follow as they are found, with a final frame to mark the end. With proxy_buffering off, nginx passes each piece on as it arrives, so the browser can start drawing before the response is complete.

nginx refuses a request body over 1 MiB unless told otherwise. A viewport request that names many tiles can be larger than that, and the viewer address itself accepts up to 2 MiB, so client_max_body_size 2m raises nginx's limit to match.

nginx's proxy_read_timeout, 60 seconds by default, limits the time between two reads from the server, not the whole response. Mosaica ends a viewport response at stream_deadline_ms, 60 seconds by default, and a bulk-read response at bulk_response_ms, 30 seconds by default, with a cursor to resume from. No response runs longer than nginx waits between reads, so leave nginx's timeout alone.

Forward every header

nginx passes on every response header unless you tell it not to. Add no proxy_hide_header for these:

  • etag
  • x-mosaica-identity-key
  • x-mosaica-pin
  • x-mosaica-stale
  • x-mosaica-server-us
  • x-mosaica-admission-us
  • x-mosaica-region
  • retry-after, sent with a 429 when the server is busy

The browser client keeps the answers it has already been sent, filed under x-mosaica-identity-key. Despite its name, that header is not the identity key. It is a value derived from the viewer's grant and the view, and it changes when either does. Without it, one person's cached map could be shown to the next person who signs in on the same page. The client reads etag, x-mosaica-pin and x-mosaica-stale to tell whether what it holds is out of date.

To check the headers come through, get a token from the session address and ask for a viewport through the proxy:

mosaica$ TOKEN=$(curl -sS http://127.0.0.1:9152/session/authorise \
  -H "authorization: Bearer $(cat secrets/operator.secret)" \
  -H 'content-type: application/json' \
  -d '{"terms": ["public"]}' | jq -r .token)
mosaica$ curl -sS -o /dev/null -D - https://maps.example.org/v1/viewport \
  -H "authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  -d '{"view": "ireland", "zoom": 0, "tiles": [0], "k": 0}'
HTTP/2 200 
server: nginx/1.18.0 (Ubuntu)
date: Thu, 24 Sep 2026 09:26:04 GMT
content-type: application/octet-stream
x-mosaica-identity-key: 3a6acc325d736e5fc758f1ae29a489db
x-mosaica-server-us: 363
x-mosaica-admission-us: 1
etag: "e0bf26387bb9b086ad036a0f8206a42f"
x-mosaica-pin: {"prefix":"v00000","segments_version":0}
x-mosaica-stale: 0

Let a page on another origin in

If the page with the map comes from a different origin, such as https://www.example.org calling the viewer at https://maps.example.org, the browser hands it the response only if Mosaica names the page's origin. List it under [serve] in mosaica.toml:

cors_origins = ["https://www.example.org"]

The list applies to the viewer address only. A page on those origins still cannot call the session address, which has no cross-origin support. A wildcard is refused:

mosaica serve: refused to start: serve.cors_origins contains "*", which a CORS origin list cannot hold; list each origin, such as "https://app.example", or remove the key

Leave dev_cors_origins unset outside development. It opens the session address to the pages it lists as well as the viewer address, and the server logs a warning when it is set. cors_loopback = true lets any page served from localhost, 127.0.0.1 or [::1] use the viewer address, which is what a notebook needs.