# Caddy proxy

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

Last updated: 2026-09-25

A `handle` block for the Caddy site that already serves your pages. 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 **Caddy** 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 handle block

First among the handlers in the site block that serves your pages, with your path in place of the example.

```caddy Caddyfile
www.example.com {
    handle /k7x2qm4a {
        header >Cache-Control no-store

        @notpost not method POST
        header @notpost Allow POST
        respond @notpost 405

        @nokey not vars_regexp {env.FINGERLY_PROXY_KEY} ^fly_px_(production|staging|development)_[A-Za-z0-9_-]+$
        respond @nokey 503

        request_body {
            max_size 1MiB
        }

        request_header -X-Fingerly-*

        @noorigin not header Origin *
        request_header @noorigin Origin {scheme}://{hostport}

        rewrite * /api/v1/ingest
        reverse_proxy https://api.fingerly.io {
            header_up Host {upstream_hostport}
            header_up -Cookie
            header_up -Authorization
            header_up X-Fingerly-Proxy-Key {env.FINGERLY_PROXY_KEY}
            header_up X-Fingerly-Client-Ip {client_ip}
            header_up X-Fingerly-Origin {http.request.header.Origin}
            header_up X-Fingerly-User-Agent {http.request.header.User-Agent}
            header_up X-Fingerly-Proxy-Version caddy/1.0.0

            header_down -Set-Cookie

            transport http {
                dial_timeout 5s
                response_header_timeout 5s
            }
        }
    }

    # ... the rest of your site ...
}
```

`header_up Host {upstream_hostport}` is automatic for HTTPS upstreams since Caddy 2.11. It stays in the block for older versions.

### Step 3: Give Caddy the proxy key

As the `FINGERLY_PROXY_KEY` environment variable of the Caddy process, never in the Caddyfile. With systemd, run `sudo systemctl edit caddy` and add these lines, or point `EnvironmentFile=` at a file readable only by root. In a container, pass it from your secret store.

```ini Service override
[Service]
Environment=FINGERLY_PROXY_KEY=fly_px_production_…
```

### Step 4: Restart Caddy

A restart rather than a reload, so Caddy sees the new environment. Behind a load balancer, set `trusted_proxies` and `trusted_proxies_strict` first (below).

```bash Terminal
sudo systemctl restart caddy
```

### Step 5: 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 `caddy/1.0.0`, with its requests over the last 30 days.

## How the visitor's address is trusted

The proxy sends `{client_ip}` as `x-fingerly-client-ip`. With no `trusted_proxies` configured, that is the address of the connection Caddy accepted, and nothing the visitor sends can change it.

Behind a load balancer or CDN, trust only its addresses, and have Caddy read `X-Forwarded-For` from the right so entries a visitor adds on the left are ignored:

```caddy Caddyfile
{
    servers {
        trusted_proxies static 10.0.0.0/8
        trusted_proxies_strict
    }
}
```

> **Warning:** Without `trusted_proxies_strict`, Caddy reads the header from the left, where a visitor can write anything.

## What it sends and relays

Upstream, to `https://api.fingerly.io/api/v1/ingest` only: the request with `Cookie`, `Authorization` and every visitor-sent `X-Fingerly-*` header removed, plus 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: caddy/1.0.0`).

Bodies over 1 MiB are refused with `413`, redirects are not followed, and a missing or malformed key answers `503`. Other methods answer `405` with `Allow: POST`.

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.
