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

# Security

> Sign, verify, rotate. The three rules of safe webhooks.

Webhooks reach a public endpoint on your infrastructure. Anyone on the internet can hit that URL — including bad actors trying to forge a `render.completed` event so you ship something they didn't pay for.

Three defenses, in order:

1. **Verify the HMAC signature** on every request. Reject anything that doesn't match.
2. **Rotate secrets** periodically and immediately after any suspected leak.
3. **Treat the body as untrusted** until verification passes.

## How signing works

For every delivery, Kynva computes:

```
signature = HMAC_SHA256(secret, raw_request_body)
```

The hex-encoded result is sent in the `X-RenderForge-Signature` header, prefixed with `sha256=`:

```
X-RenderForge-Signature: sha256=4f7c0a...e9d
```

This is byte-for-byte the same construction as
[`signPayload()` in src/webhooks/signer.ts](https://github.com/fredadun/renderforge/blob/main/src/webhooks/signer.ts).

## Verification rules

1. **Use the raw request body**, not a parsed-and-restringified copy. Even a whitespace change breaks the signature.
2. **Compare in constant time** — use `crypto.timingSafeEqual` (Node) or `hmac.compare_digest` (Python). String equality is vulnerable to timing attacks.
3. **Strip the `sha256=` prefix** before comparing.
4. **Verify before processing** anything from the body, including the event type.

Working code lives on the [verification examples](/webhooks/verification-examples) page.

## Rotating secrets

When to rotate:

* On a schedule (we recommend every 90 days).
* Immediately if you suspect a leak.
* When a team member with access to the secret leaves.

How to rotate:

```bash theme={null}
curl -X POST https://api.kynva.ai/api/v1/facade/webhooks/{webhook_id}/rotate-secret \
  -H "Authorization: Bearer $KYNVA_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
```

The response includes the new secret. The previous secret keeps working for **24 hours** so you can roll deploys without a webhook outage. During that window, verify against both secrets — accept if either matches.

## Replay protection

A captured legitimate webhook can be replayed. Two strategies:

### Dedupe on `X-RenderForge-Delivery`

Every delivery has a unique ID. Store the ones you've seen for at least 24h (e.g., in Redis with a TTL) and reject duplicates:

```typescript theme={null}
const id = req.headers["x-renderforge-delivery"];
if (await redis.get(`webhook:seen:${id}`)) {
  return res.status(200).end(); // already handled; ack to stop retries
}
await redis.set(`webhook:seen:${id}`, "1", "EX", 86400);
```

### Reject stale timestamps

Compare `X-RenderForge-Timestamp` to "now" and reject anything older than 5 minutes:

```typescript theme={null}
const ts = Date.parse(req.headers["x-renderforge-timestamp"] as string);
if (Math.abs(Date.now() - ts) > 5 * 60 * 1000) {
  return res.status(400).end();
}
```

Use both for defense in depth.

## Network controls

Optional but useful:

* **Allowlist Kynva egress IPs** at your firewall. The current set is published at [status.kynva.ai/network](https://status.kynva.ai/network).
* **Require TLS** on your webhook URL — `https://...`. We refuse to create webhooks against plain HTTP in production.
* **Don't put your webhook URL on the open internet under a guessable path**. Add a long random segment, e.g. `https://your-app.com/webhooks/kynva/8c4a...`.
