# Protecting Your Origins

- [Introduction](#introduction)
- [The Origin Secret](#the-origin-secret)
    - [Headers Sent to Your Origin](#headers-sent-to-your-origin)
- [Locking Your Origin](#locking-your-origin)
    - [Locking Your Origin Using AI](#locking-your-origin-using-ai)
    - [Laravel](#laravel)
    - [Nginx](#nginx)
    - [Caddy](#caddy)
- [Testing the Lock](#testing-the-lock)
- [Rotating the Secret](#rotating-the-secret)
- [Additional Hardening](#additional-hardening)

## Introduction

Once your domain's DNS points at the edge, every visitor who types your domain passes through NiceTryBot. However, your origin is still on the internet. A scanner that already knows your origin's address, or simply finds it, can send requests straight to it and skip the edge entirely.

To close that back door, the edge signs every request it forwards with a secret header. Your origin's only job is to refuse any request that doesn't carry it.

## The Origin Secret

Each domain has its own randomly generated, 48 character origin secret. You will find it in the **Lock the origin to the edge** step of the domain's **Go live** panel, alongside the header name:

```text
X-NiceTry-Origin-Secret
```

Every request the edge forwards to your origin includes this header. Store the secret in your application's environment, such as an `NICETRY_ORIGIN_SECRET` environment variable, and never commit it to source control.

> [!NOTE]
> If you use the Origins add-on to serve many domains from one backend, the secret belongs to the origin rather than each domain, and every site behind that origin shares it.

### Headers Sent to Your Origin

Along with the secret, the edge sends a few headers your application may find useful:

| Header | Description |
| --- | --- |
| `Host` | The visitor's host, or your rewrite host. See [Host Headers](https://docs.nicetrybot.com/docs/0.x/domains.md#host-headers). |
| `X-Forwarded-Host` | The site the visitor asked for. |
| `X-Forwarded-For` | The visitor's IP address. |
| `X-Forwarded-Proto` | The scheme the visitor used, which is always `https`. |
| `X-NiceTry-Origin-Secret` | The domain's origin secret. |
| `X-NiceTry-JA3` | The visitor's JA3 TLS fingerprint. |
| `X-NiceTry-JA4` | The visitor's JA4 TLS fingerprint. |

Since your origin now sits behind a proxy, configure your application to trust the edge so that it reads the visitor's IP address from `X-Forwarded-For`. In Laravel, this is done using the [TrustProxies middleware](https://laravel.com/docs/requests#configuring-trusted-proxies).

## Locking Your Origin

Your origin should refuse any request without a matching `X-NiceTry-Origin-Secret` header, before any other code runs. A few guidelines apply no matter what your origin is built with:

<div class="content-list" markdown="1">

- Compare the secret in constant time, and respond with `403 Forbidden` when it is missing or wrong.
- In production, refuse every request if the secret has not been configured, rather than silently letting everything through.
- Until your DNS points at the edge, lock only the origin's own hostname, such as `www-origin.example.com`, so your live site keeps working. Once DNS moves, lock every hostname.
- Match against the `Host` header the request arrived with, not `X-Forwarded-Host`, which anyone can send.
- If your hosting platform needs a health check path, allow only that path.

</div>

### Locking Your Origin Using AI

Locking an origin is a small, well-defined change, which makes it a great job for a coding agent. Open your agent in your application's repository and paste the following prompt:

```text
Read https://docs.nicetrybot.com/docs/0.x/origins.md. Then add an origin lock to this application that follows it exactly: refuse requests without a valid X-NiceTry-Origin-Secret header with a 403, read the secret from NICETRY_ORIGIN_SECRET, and add tests. Show me the plan before you change any code.
```

### Laravel

If your origin is a Laravel application, you may use the middleware below. It is the same middleware the NiceTryBot website runs in production. First, add the configuration to your application's `config/services.php` file:

```php
'nicetry' => [
    'origin_secret' => env('NICETRY_ORIGIN_SECRET'),

    // Hosts whose requests must come through the edge, separated by commas, or * for every request.
    'locked_hosts' => env('NICETRY_LOCKED_HOSTS') ?: '*',
],
```

Next, create the middleware:

```php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\URL;
use Symfony\Component\HttpFoundation\Response;

class RequireNiceTryOrigin
{
    /**
     * Refuse requests that didn't come through the NiceTryBot edge.
     */
    public function handle(Request $request, Closure $next): Response
    {
        $secret = config('services.nicetry.origin_secret');

        if (blank($secret)) {
            return app()->isProduction() ? $this->refuse() : $next($request);
        }

        if (! $this->locks($request)) {
            return $next($request);
        }

        $sent = $request->headers->get('X-NiceTry-Origin-Secret');

        if (! is_string($sent) || ! hash_equals($secret, $sent)) {
            return $this->refuse();
        }

        URL::forceRootUrl(config('app.url'));

        return $next($request);
    }

    /**
     * Determine if the request's host is locked to the edge.
     */
    protected function locks(Request $request): bool
    {
        $locked = array_map(trim(...), explode(',', strtolower((string) config('services.nicetry.locked_hosts'))));

        if (in_array('*', $locked, true)) {
            return true;
        }

        $hosts = [
            strtolower($request->getHost()),
            strtolower(preg_replace('/:\d+$/', '', (string) $request->server('HTTP_HOST'))),
        ];

        return array_intersect($hosts, $locked) !== [];
    }

    protected function refuse(): Response
    {
        return response('Forbidden', 403, ['Content-Type' => 'text/plain', 'Cache-Control' => 'no-store']);
    }
}
```

Finally, prepend the middleware in your application's `bootstrap/app.php` file so it runs before anything else:

```php
use App\Http\Middleware\RequireNiceTryOrigin;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->prepend(RequireNiceTryOrigin::class);
})
```

While you are still testing, set `NICETRY_LOCKED_HOSTS` to your origin hostname. Once your DNS points at the edge, remove it, and every request will be locked.

### Nginx

If your origin is served by Nginx, you may refuse requests without the secret in the `server` block for your site. Replace the example value with your domain's secret:

```nginx
server {
    server_name www-origin.example.com;

    if ($http_x_nicetry_origin_secret != "your-origin-secret") {
        return 403;
    }

    # ...
}
```

### Caddy

If your origin is served by Caddy, you may use a named matcher that matches requests without the secret:

```caddyfile
www-origin.example.com {
    @outsider not header X-NiceTry-Origin-Secret {env.NICETRY_ORIGIN_SECRET}

    respond @outsider 403

    # ...
}
```

> [!NOTE]
> The Nginx and Caddy examples compare the secret with a plain string match rather than in constant time. They are a reasonable starting point, but checking the secret in your application, as the Laravel middleware does, is the stronger option.

## Testing the Lock

Once your origin is locked, request it directly. Without the header, it should respond with a `403`:

```shell
curl -sI https://www-origin.example.com/
```

With the header, the request should be served as usual:

```shell
curl -sI https://www-origin.example.com/ \
  -H "X-NiceTry-Origin-Secret: $NICETRY_ORIGIN_SECRET"
```

If you have connected an agent to NiceTryBot's [MCP server](https://docs.nicetrybot.com/docs/0.x/mcp.md), the `check-domain` tool runs the same test for you. It requests your origin's `/` path twice, once without the secret and once with it, and reports the origin as locked when the first request is refused with a `4xx` response and the second succeeds.

## Rotating the Secret

If you believe a domain's secret has leaked, you may issue a new one using the **Rotate secret** button on the domain's page.

> [!WARNING]
> Rotation takes effect immediately. The edge starts sending the new secret right away and the old secret stops working at the same moment, so requests will fail at your origin until it accepts the new value.

To rotate without any failed requests, briefly let your origin accept both the old and the new secret:

<div class="content-list" markdown="1">

1. Copy the current secret, then click **Rotate secret** and copy the new one.
2. Deploy your origin so that it accepts either value.
3. Once the deployment is live, deploy again with only the new secret.

</div>

Since step 2 can't happen before the rotation, expect a short window of refused requests between rotating and deploying. If that is not acceptable, deploy during a quiet period.

## Additional Hardening

The origin secret is the primary way to lock an origin, and on its own it is enough. If your hosting provider offers a firewall, you may additionally allow inbound traffic to your origin only from your NiceTryBot server. Note that the address your server uses for **outbound** connections may differ from the addresses in `NICETRY_EDGE_ADDRESSES`, so confirm it before you write the rule.

It is also a good idea to give your origin a hostname nobody would guess from your public one, and to keep it out of public links, sitemaps, and certificates you share with other hostnames.
