# Determine a request came from the proxy

## Goal

Reject traffic that hits your origin without going through the pool. When
**Origin verification** is enabled, nthbouncer signs every forwarded request;
your origin checks that signature before doing work.

Reference for headers and limits:
[Access → Origin verification](/docs/pools/access#origin-verification-outbound-hmac).

## Enable signing

1. Open **Pools → Access → Origin verification**.
2. Enable it and copy the `osk_…` secret once.
3. Store it as `NTHPOOL_ORIGIN_SIGNING_SECRET` (or equivalent) on the origin.
4. Deploy verification that reads the **raw** body bytes before JSON parsers or
   decompressors rewrite them.

The dashboard pool page also has language-specific verify snippets under the
examples panel.

## What to verify

On each inbound request at the origin:

1. Require `X-Nthpool-Timestamp`, `X-Nthpool-Nonce`, and `X-Nthpool-Signature`.
2. Reject if the timestamp is outside ±**300** seconds.
3. Let `n` be the raw body length. If `Content-Length` is present, reject when
   it does not equal `n`.
4. Build the outbound canonical string (fields joined with `\n`):

```text
METHOD
PATH
QUERY
SHA256(BODY_PREFIX)
CONTENT_LENGTH
TIMESTAMP
NONCE
```

`BODY_PREFIX` is `body[0 .. min(n, 1 MiB))`. `CONTENT_LENGTH` is the decimal
string of `n` (`0` for empty / `GET` / `HEAD`).

5. HMAC-SHA256 with the `osk_…` secret; hex-encode; compare in constant time.

`PATH` and `QUERY` must match what the origin received (query without `?`).
This is **not** the inbound caller HMAC scheme (that hashes the whole body and
omits `CONTENT_LENGTH`).

## Node example

```javascript
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';

const MAX_SKEW_SECONDS = 300;
const PREFIX_BYTES = 1024 * 1024; // 1 MiB

export function cameThroughNthpool(req, rawBody) {
    const timestamp = req.headers['x-nthpool-timestamp'];
    const nonce = req.headers['x-nthpool-nonce'];
    const signature = req.headers['x-nthpool-signature'];
    if (!timestamp || !nonce || !signature) return false;
    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > MAX_SKEW_SECONDS) {
        return false;
    }

    const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody ?? '');
    const declared = req.headers['content-length'];
    if (declared !== undefined && Number(declared) !== body.length) return false;

    const originalUrl = req.originalUrl ?? req.url;
    const [path, query = ''] = originalUrl.split('?', 2);
    const prefix = body.subarray(0, Math.min(body.length, PREFIX_BYTES));
    const bodyHash = createHash('sha256').update(prefix).digest('hex');
    const canonical = [
        req.method.toUpperCase(),
        path,
        query,
        bodyHash,
        String(body.length),
        timestamp,
        nonce,
    ].join('\n');
    const expected = createHmac(
        'sha256',
        process.env.NTHPOOL_ORIGIN_SIGNING_SECRET,
    )
        .update(canonical)
        .digest('hex');

    return (
        signature.length === expected.length &&
        timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
    );
}

// Express: capture rawBody before JSON parsing, then reject false with 403.
```

## Limits and gotchas

- Bodies larger than **1 MB** still get a valid signature: only the prefix is
  hashed; send a correct `Content-Length` so the pool can stream without
  buffering the whole payload.
- Without `Content-Length`, the pool buffers up to **10 MB**. Larger
  chunked bodies return `413 payload_too_large` to the caller.
- Inbound `X-Nthpool-*` headers are stripped before forwarding, so only the pool
  can set the outbound signature headers.
- Retries are re-signed (new timestamp and nonce each attempt).
- Optional: track nonces at the origin for replay defense; the pool already
  uses a fresh UUID per attempt.
- When rotating, accept both secrets until you **Activate** the new one in the
  dashboard.

## See also

- [Access](/docs/pools/access) (inbound HMAC vs origin verification)
- [Pools](/docs/pools)
- [Admission errors](/docs/knowledge-base/admission-errors)
