# nginx proxy

> A location block for the nginx server that already serves your site: it answers the one path the dashboard issued and forwards the browser SDK's calls to Fingerly.

Last updated: 2026-09-25

A `location` block for the nginx server that already serves your site. It answers one path the dashboard issued, on your own host, and forwards only the browser SDK's submissions to Fingerly. It is not an open proxy: the upstream is fixed in the configuration, and every other path and method is refused. Version 1.0.0.

## Set up

### Step 1: Create the proxy

In the dashboard, open **Integration > Proxy keys** and choose **New proxy**. Pick the environment of the public key it will forward, and **nginx** under **How it runs**. Copy the [proxy key](https://docs.fingerly.io/docs/proxy-integrations#proxy-keys) it shows once, and note the path it issued, such as `/k7x2qm4a`.

The dashboard then prints the steps below, filled in with your proxy's own path and key. They stay under **Show setup** in the proxy's menu, without the key, which is never shown again.

### Step 2: Add the proxy location

Save this as `/etc/nginx/snippets/fingerly.conf`, with your path in place of the example.

```nginx /etc/nginx/snippets/fingerly.conf
location = /k7x2qm4a {
    # One line, readable only by root: set $fingerly_proxy_key "fly_px_...";
    include /etc/nginx/fingerly-proxy-key.conf;

    if ($request_method != POST) {
        add_header Allow "POST" always;
        add_header Cache-Control "no-store" always;
        return 405;
    }
    if ($fingerly_proxy_key !~ "^fly_px_(production|staging|development)_[A-Za-z0-9_-]+$") {
        add_header Cache-Control "no-store" always;
        return 503;
    }

    set $fingerly_origin $http_origin;
    if ($fingerly_origin = "") {
        set $fingerly_origin "$scheme://$http_host";
    }

    client_max_body_size 1m;

    proxy_pass https://api.fingerly.io/api/v1/ingest;
    proxy_http_version 1.1;
    proxy_ssl_server_name on;
    proxy_ssl_verify on;
    proxy_ssl_verify_depth 3;
    proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
    proxy_redirect off;
    proxy_connect_timeout 5s;
    proxy_send_timeout 5s;
    proxy_read_timeout 5s;

    proxy_pass_request_headers off;
    proxy_set_header Host api.fingerly.io;
    proxy_set_header Content-Type $content_type;
    proxy_set_header X-Api-Key $http_x_api_key;
    proxy_set_header Idempotency-Key $http_idempotency_key;
    proxy_set_header X-Fingerly-Proxy-Key $fingerly_proxy_key;
    proxy_set_header X-Fingerly-Client-Ip $remote_addr;
    proxy_set_header X-Fingerly-Origin $fingerly_origin;
    proxy_set_header X-Fingerly-User-Agent $http_user_agent;
    proxy_set_header X-Fingerly-Proxy-Version "nginx/1.0.0";

    proxy_hide_header Set-Cookie;
    proxy_hide_header Cache-Control;
    add_header Cache-Control "no-store" always;
}
```

`proxy_ssl_trusted_certificate` points at the usual system CA bundle. Where your system keeps it elsewhere, such as `/etc/pki/tls/certs/ca-bundle.crt`, change the path.

### Step 3: Store the proxy key

In its own file, readable only by root. If `/etc/nginx` is in a repository, add the file to its `.gitignore`.

```bash Terminal
sudo install -m 600 -o root -g root /dev/null /etc/nginx/fingerly-proxy-key.conf
echo 'set $fingerly_proxy_key "fly_px_production_…";' | sudo tee /etc/nginx/fingerly-proxy-key.conf >/dev/null
```

### Step 4: Include it in your site

Inside the `server` block that serves your pages.

```nginx nginx.conf
server {
    server_name www.example.com;
    include /etc/nginx/snippets/fingerly.conf;
    # ...
}
```

### Step 5: Reload nginx

Behind a load balancer, set up the realip module first (below), so the visitor's address is right.

```bash Terminal
sudo nginx -t && sudo systemctl reload nginx
```

nginx resolves `api.fingerly.io` when it loads the configuration, so a reload also picks up address changes.

### Step 6: Point the browser SDK at the path

```ts fingerly.ts
import { load } from '@fingerly/web-js'

const fingerly = await load({
  apiKey: 'fly_pk_production_…',
  endpoints: '/k7x2qm4a',
})
```

The SDK posts both of its calls to the path exactly as given. Once pages use it, the proxy's row in **Proxy keys** shows it last seen, running `nginx/1.0.0`, with its requests over the last 30 days.

## How the visitor's address is trusted

The proxy sends `$remote_addr`, the address of the connection nginx accepted, as `x-fingerly-client-ip`. Nothing the visitor sends can change it. When visitors reach nginx directly, that is the visitor.

Behind a load balancer or CDN, `$remote_addr` is the load balancer. Tell the realip module which addresses to trust and which header they write, in the `http` or `server` block:

```nginx nginx.conf
set_real_ip_from 10.0.0.0/8;          # your load balancer's addresses
real_ip_header X-Forwarded-For;
real_ip_recursive on;
```

With `real_ip_recursive on`, nginx walks `X-Forwarded-For` from the right and takes the first address outside the trusted ranges, so entries a visitor adds on the left are ignored.

> **Warning:** Never trust `0.0.0.0/0`. Every visitor could then choose the address Fingerly sees.

## What it sends and relays

Upstream, to `https://api.fingerly.io/api/v1/ingest` only: `content-type`, `x-api-key`, `Idempotency-Key` when present, and the `x-fingerly-*` headers the proxy sets itself (the proxy key, the visitor's address, the page origin, the user agent and `x-fingerly-proxy-version: nginx/1.0.0`). `proxy_pass_request_headers off` makes this an allowlist: cookies, `Authorization` and any `x-fingerly-*` header a visitor forges never leave your server.

Bodies over 1 MiB are refused with `413`, redirects are not followed, and a key that is not shaped like a proxy key answers `503`. Other methods answer `405` with `Allow: POST`. If you prefer `limit_except POST { deny all; }` to the `if` block, they answer `403` instead; the browser SDK only posts, so either works.

Back to the page: the API's status and body, with `Set-Cookie` dropped and `Cache-Control: no-store`. The API sends nothing naming Fingerly to proxied callers, so dropping headers here is defense in depth.
