> ## Documentation Index
> Fetch the complete documentation index at: https://docs.molesignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# nginx

> Build two entry points with nginx: the internal one forwards every request, the public one forwards only external callbacks and public pages and answers 404 for every other path.

The configuration below follows the recommended way in [Public access](https://docs.molesignal.com/en-US/public-access): one name, `molesignal.example.com`, that the internal DNS resolves to nginx's internal address `10.0.0.10` and the public DNS to its public address `203.0.113.10`. The two `server` blocks share one certificate and are told apart by their listen addresses. Keep only the `location` blocks in use.

## Shared parts

Put the upstream, the public log format and the rate limit zone in the `http` block, for example at the top of `/etc/nginx/conf.d/molesignal.conf`:

```nginx theme={null}
upstream molesignal {
    # A standalone node, or the router of a role-split deployment
    server 10.0.1.20:5080;
    keepalive 16;
}

# The public entry point logs paths, never query strings; heartbeat and calendar paths carry a token,
# so only their prefix is logged.
map $uri $molesignal_log_uri {
    ~^(/api/v1/(?:intake/heartbeats|public/calendar)/)  $1<token>;
    default                                             $uri;
}

log_format molesignal_public '$remote_addr [$time_local] "$request_method $molesignal_log_uri $server_protocol" '
                             '$status $body_bytes_sent "$http_user_agent" $request_time';

limit_req_zone $binary_remote_addr zone=molesignal_public:10m rate=20r/s;
```

Every forwarding location uses the same proxy settings, kept in `/etc/nginx/snippets/molesignal-proxy.conf`:

```nginx theme={null}
proxy_pass http://molesignal;
proxy_http_version 1.1;
proxy_set_header Connection "";
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;
```

## Internal entry point

The internal entry point forwards every path, as it would without a public entry point. Live queries are streamed responses and must not be buffered:

```nginx theme={null}
server {
    listen 10.0.0.10:443 ssl;
    server_name molesignal.example.com;
    ssl_certificate     /etc/nginx/tls/molesignal.crt;
    ssl_certificate_key /etc/nginx/tls/molesignal.key;

    client_max_body_size 10m;

    location /api/v1/query/stream {
        include /etc/nginx/snippets/molesignal-proxy.conf;
        proxy_buffering off;
        proxy_read_timeout 600s;
        proxy_send_timeout 600s;
    }

    location / {
        include /etc/nginx/snippets/molesignal-proxy.conf;
        proxy_read_timeout 60s;
    }
}
```

## Public entry point

The public entry point lists only the paths to open, each taking only the methods it uses. The final `location /` answers 404 for everything else:

```nginx theme={null}
server {
    listen 203.0.113.10:443 ssl;
    server_name molesignal.example.com;
    ssl_certificate     /etc/nginx/tls/molesignal.crt;
    ssl_certificate_key /etc/nginx/tls/molesignal.key;
    ssl_protocols TLSv1.2 TLSv1.3;

    access_log /var/log/nginx/molesignal-public.log molesignal_public;
    client_max_body_size 2m;
    limit_req zone=molesignal_public burst=40 nodelay;
    limit_req_status 429;

    # External alerts
    location = /api/v1/intake/alerts {
        limit_except POST { deny all; }
        client_max_body_size 4m;
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }

    # Heartbeat check-ins
    location ^~ /api/v1/intake/heartbeats/ {
        limit_except GET POST { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }

    # The Jira ticket webhook
    location ~ ^/api/v1/public/tickets/jira/[^/]+$ {
        limit_except POST { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }

    # On-call calendar subscriptions
    location ^~ /api/v1/public/calendar/ {
        limit_except GET { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }

    # Mole Agent in Teams
    location ^~ /api/v1/public/im/teams/ {
        limit_except POST { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }

    # Replies to texts and calls
    location ~ ^/api/v1/public/notify/[^/]+/(replies|keypress)$ {
        limit_except POST { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }

    # Automation API triggers
    location ~ ^/api/v1/public/automations/[^/]+/trigger$ {
        limit_except POST { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }

    # Push sources
    location ~ ^/api/v1/(_kinesis_firehose|_cloudflare|_heroku)$ {
        limit_except POST { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }

    # RUM: browsers send a CORS preflight first
    location ~ ^/api/v1/rum/(events|replay)$ {
        limit_except POST OPTIONS { deny all; }
        client_max_body_size 8m;
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }

    # Synthetics Probe Agent artifact uploads
    location ^~ /api/v1/synthetics/artifacts/ {
        limit_except PUT { deny all; }
        client_max_body_size 10m;
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }

    # Status pages
    location ^~ /status/ {
        limit_except GET { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }
    location ^~ /api/v1/public/status-pages/ {
        limit_except GET POST { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }

    # Resource sharing
    location ^~ /s/ {
        limit_except GET { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }
    location = /shared {
        limit_except GET { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }
    location ^~ /api/v1/public/share {
        limit_except GET POST { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }

    # Web files of status pages and shared resources
    location ^~ /assets/ {
        limit_except GET { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }
    location ~ ^/favicon\.(ico|svg)$ {
        limit_except GET { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }

    # No other path exists on the internet
    location / {
        return 404;
    }
}
```

`limit_except GET` also allows `HEAD`. To tighten one path by source address, add `allow` and `deny all;` to its `location`, for example to let only the ranges of the Azure service tag `AzureBotService` reach the Teams callback.

## Custom domains of status pages

When a status page uses `status.example.org`, add a public `server` for it that opens only the status page's root paths, endpoints and web files:

```nginx theme={null}
server {
    listen 203.0.113.10:443 ssl;
    server_name status.example.org;
    ssl_certificate     /etc/nginx/tls/status.example.org.crt;
    ssl_certificate_key /etc/nginx/tls/status.example.org.key;

    access_log /var/log/nginx/molesignal-public.log molesignal_public;
    limit_req zone=molesignal_public burst=40 nodelay;
    limit_req_status 429;

    location ~ ^/(history|uptime)?$ {
        limit_except GET { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }
    location ^~ /incidents/ {
        limit_except GET { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }
    location ^~ /api/v1/public/status-pages/ {
        limit_except GET POST { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }
    location ^~ /assets/ {
        limit_except GET { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }
    location ~ ^/favicon\.(ico|svg)$ {
        limit_except GET { deny all; }
        include /etc/nginx/snippets/molesignal-proxy.conf;
    }
    location / {
        return 404;
    }
}
```

When MoleSignal issues the custom domain's certificate itself, TLS ends on MoleSignal. nginx can then only pass the connection through by SNI with the `stream` module, sees no paths, and cannot open just the status page. To restrict paths on the entry point, manage the certificate on nginx.

## With a single entry point

When nginx has one listen address, tell sources apart by address: keep the open paths of the public `server` as they are, replace the final `location /` with one that allows only the ranges of the office network and the VPN, and add the streaming settings for live queries:

```nginx theme={null}
location /api/v1/query/stream {
    allow 10.0.0.0/8;
    allow 192.168.0.0/16;
    deny all;
    include /etc/nginx/snippets/molesignal-proxy.conf;
    proxy_buffering off;
    proxy_read_timeout 600s;
    proxy_send_timeout 600s;
}

location / {
    allow 10.0.0.0/8;
    allow 192.168.0.0/16;
    deny all;
    include /etc/nginx/snippets/molesignal-proxy.conf;
}
```

Internal requests then pass the same `server`: raise `client_max_body_size` to `10m`, and expect the access log to record the paths of internal requests too. When a load balancer stands in front of nginx, take the original address with `set_real_ip_from` and `real_ip_header`; otherwise `allow` sees only the load balancer's address.

## Check

Check the configuration and reload it:

```bash theme={null}
nginx -t
```

```bash theme={null}
nginx -s reload
```

From the internet, the sign-in endpoint answers `404`:

```bash theme={null}
curl -s -o /dev/null -w '%{http_code}\n' https://molesignal.example.com/api/v1/auth/signin
```

From the internet, alert intake without a token answers `401`, which shows that the request reached MoleSignal:

```bash theme={null}
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://molesignal.example.com/api/v1/intake/alerts
```

Finally, in the `_sys` organization, list nginx's addresses as trusted proxies under **Settings → Client IP recognition**, see [Client IP recognition](https://docs.molesignal.com/en-US/settings/platform.md#client-ip-recognition).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.