# Fastly VCL

> Attach Centinel Analytica to your existing Fastly VCL service via the attach script, Fastly Web UI, or Terraform.

Source: https://docs.centinelanalytica.com/platforms/cdn-edge/fastly

## Overview

Centinel attaches to your **existing** Fastly VCL service. The integration adds exactly three things to your service and modifies nothing else. Your domains, origins, custom VCL, and other snippets stay untouched:

1. A backend named `centinel` pointing at the validator (1s connect / 3s first-byte timeouts, so an outage adds at most a bounded delay before failing open)
2. A Private Edge Dictionary named `centinel_config` (API key, debug flag, kill-switch)
3. Seven VCL snippets named `centinel_*`

On every request the snippets check the path filter, validate the request against the Centinel `/validate` API, and block, challenge, or forward to your origin based on the decision. Every validator failure mode fails open. Changes take 2-3 minutes to roll out across Fastly's edge.

Installation aborts up front if any of those names already exist on your service, and uninstalling removes only those names.

## Prerequisites

* An existing Fastly VCL service with at least one domain and origin
* Centinel API key (for validator authentication)
* Fastly account with API token (Account → Personal API Tokens, full-service access)
* `curl` and `jq` (attach script path only)

## Install

Download [centinel-fastly.zip](https://docs.centinelanalytica.com/downloads/centinel-fastly.zip) and extract.

**Attach script (recommended)**

### Attach script

The script clones your service's active version, adds the backend + dictionary + snippets, validates the VCL, and activates.

```bash
cd install

export FASTLY_API_KEY="your-fastly-api-token"
export CENTINEL_API_KEY="your-centinel-api-key"

./attach.sh install <SERVICE_ID>
```

Your service ID is on the service overview page in the Fastly dashboard (or `fastly service list`).

Useful variations:

```bash
./attach.sh install <SERVICE_ID> --no-activate   # review the new version in the UI before activating
./attach.sh install <SERVICE_ID> --debug         # echo x-centinel-* headers (turn off in production)
./attach.sh status  <SERVICE_ID>                 # list installed Centinel components
./attach.sh uninstall <SERVICE_ID>               # remove only the centinel_* components
```

**Fastly Web UI (manual)**

### Manual installation via Fastly Web UI

#### Step 1: Clone the active version

Log into your Fastly dashboard, select your service, and click **Clone version** to create an editable draft (you can't edit active versions).

#### Step 2: Create the `centinel` backend

In **Origins → Hosts**, add a backend alongside your existing origin:

* **Name**: `centinel`
* **Address**: `validator.centinelanalytica.com`
* **Port**: `443`
* **Use SSL**: yes
* **SSL hostname**: `validator.centinelanalytica.com`
* **SSL SNI hostname**: `validator.centinelanalytica.com`
* **Override host**: `validator.centinelanalytica.com`
* **Connect timeout**: `1000` ms
* **First byte timeout**: `3000` ms
* **Between bytes timeout**: `2000` ms

#### Step 3: Create the Edge Dictionary

In **Edge Dictionaries → Create**:

* **Name**: `centinel_config`
* Mark as **Private**
* Add items:
  * `secret_key` → `<your-centinel-api-key>`
  * `debug` → `false`
  * `enabled` → `true`
* Save.

#### Step 4: Upload the seven VCL snippets

In **VCL Snippets → Create snippet**, repeat for each file in the `snippets/` directory from the downloaded zip. The priorities matter. Fastly runs lower numbers first, so these make Centinel run before your own recv/fetch snippets and after your own deliver snippets:

| File          | Snippet type | Name               | Priority |
| ------------- | ------------ | ------------------ | -------- |
| `init.vcl`    | `init`       | `centinel_init`    | `10`     |
| `recv.vcl`    | `recv`       | `centinel_recv`    | `10`     |
| `pass.vcl`    | `pass`       | `centinel_pass`    | `10`     |
| `miss.vcl`    | `miss`       | `centinel_miss`    | `10`     |
| `fetch.vcl`   | `fetch`      | `centinel_fetch`   | `10`     |
| `error.vcl`   | `error`      | `centinel_error`   | `10`     |
| `deliver.vcl` | `deliver`    | `centinel_deliver` | `150`    |

Paste each file's contents verbatim. You don't need to substitute an API key in the VCL; the snippets read it from the dictionary at runtime.

#### Step 5: Activate

1. Review the new backend, dictionary, and snippets.
2. Click **Activate** on the new version.
3. Wait 2-3 minutes for the new VCL to propagate globally.

**Terraform**

### Terraform (service managed by Terraform)

If your service is already managed by an existing `fastly_service_vcl` resource, paste these blocks into it (the Fastly provider is monolithic, so snippets must live inside the service resource):

```hcl
resource "fastly_service_vcl" "your_service" {
  # ... your existing domains, backends, settings ...

  # --- Centinel bot protection ---
  backend {
    name                  = "centinel"
    address               = "validator.centinelanalytica.com"
    port                  = 443
    use_ssl               = true
    ssl_cert_hostname     = "validator.centinelanalytica.com"
    ssl_sni_hostname      = "validator.centinelanalytica.com"
    override_host         = "validator.centinelanalytica.com"
    connect_timeout       = 1000
    first_byte_timeout    = 3000
    between_bytes_timeout = 2000
    max_conn              = 200
  }

  dictionary {
    name       = "centinel_config"
    write_only = true
  }

  dynamic "snippet" {
    for_each = {
      init = 10, recv = 10, pass = 10, miss = 10, fetch = 10, error = 10, deliver = 150
    }
    content {
      type     = snippet.key
      name     = "centinel_${snippet.key}"
      content  = file("${path.module}/centinel-snippets/${snippet.key}.vcl")
      priority = snippet.value
    }
  }
  # --- end Centinel ---
}
```

Copy the seven `.vcl` files from the zip's `snippets/` directory into `centinel-snippets/` next to your Terraform config, and populate the dictionary items (`secret_key`, `debug`, `enabled`) via a `fastly_service_dictionary_items` resource or the Fastly API.

> **write\_only dictionaries and Terraform:** The Fastly provider cannot read back `write_only = true` dictionary items. Either set `write_only = false` while managing items in Terraform, or keep it `true` and populate items via the Fastly API after `terraform apply`.

## Configure

### Edge Dictionary items (`centinel_config`)

| Key                 | Required | Description                                                                                                                |
| ------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
| `secret_key`        | yes      | Centinel validator API key.                                                                                                |
| `enabled`           | no       | Kill-switch. Set to `"false"` to bypass validation instantly, with no version activation or redeploy. Defaults to enabled. |
| `debug`             | no       | `"true"` or `"false"` (string). When `"true"`, debug headers are echoed on client responses. Keep `"false"` in production. |
| `cookie_attributes` | no       | Override the `_centinel` Set-Cookie attributes. Default: `Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=86400`.          |

### Path filtering

Static assets (images, fonts, CSS, JS, media) are excluded by default. The exclusion regex lives in `recv.vcl` as a literal (a Fastly VCL constraint: regex patterns can't come from variables). To customize, edit the regex in `recv.vcl` and re-upload that one snippet.

### Coexisting with your existing VCL

The integration is additive, but two interactions are inherent to pure-VCL bot protection. Audit your existing VCL for these before activating:

**Snippet ordering.** Fastly runs **lower priority numbers first**, and customer snippets default to **100**. Centinel's recv/fetch snippets run at priority 10 (before yours); its deliver snippet runs at priority 150 (after your default-priority deliver snippets, so its cleanup and validator-header application win). Don't `return()` out of `vcl_recv` or `vcl_fetch` from a snippet whose priority is below 10 (it would run before `centinel_recv` and silently bypass protection). If you need a deliver snippet to run after Centinel's cleanup, give it priority above 150.

**Restarts.** Validation consumes exactly one restart per protected request, and origin-bound requests carry the marker header `req.http.X-CN-Done == "1"`. If your VCL branches on `req.restarts`, adapt it:

```text
# run-once logic — was: if (req.restarts == 0)
if (req.restarts == 0 || (req.http.X-CN-Done && req.restarts == 1)) { ... }

# restart-based origin failover — was: if (req.restarts > 0)
if (req.restarts > 1 || (req.restarts == 1 && !req.http.X-CN-Done)) { ... }
```

**Next-Gen WAF (NGWAF / Signal Sciences).** If your service runs Fastly's edge WAF, Centinel's snippets (priority 10) run before NGWAF's (priority 9000) and mark the internal validator leg with `x-sigsci-no-inspection: true`, so the WAF skips the bodyless `POST /validate` hop. Without it, the WAF blocks the validator subrequest with HTTP 406 and logs its response as a spurious security event. The header is a no-op on services without NGWAF, so you don't need a manual allow-rule. Keep your own NGWAF snippets at their default priority (9000); a snippet below 10 runs before Centinel and breaks the bypass.

**Reserved names.** Backend `centinel`, dictionary `centinel_config`, snippets `centinel_*`, request headers `X-CN-*` / `X-Centinel-*` / `x-sigsci-no-inspection` (validator leg only), and synthetic codes `760`/`761` in `vcl_error`.

Requests that are automatically skipped (fail-open): static assets, `FASTLYPURGE`, WebSocket upgrades, ESI subrequests, background fetches, shield-node passes, and everything while the kill-switch is off.

## Verify

```bash
curl -i https://www.example.com/some-page
```

Expect: your origin's normal response, plus `Set-Cookie: _centinel=...; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=86400` and `Server-Timing: centinel;dur=<ms>`. That timing is the validator round-trip only (typically well under 100 ms), appended to any existing Server-Timing.

For temporary debugging, set `debug` → `true` in the dictionary; responses then include `x-centinel-decision`, `x-centinel-request-id`, and crawler info. Turn it back off in production.

## Uninstall

* **Pause instantly:** set the `enabled` dictionary item to `false`. Takes effect within seconds; set back to `true` to resume.
* **Remove completely:** `./attach.sh uninstall <SERVICE_ID>`, or in the UI clone the active version, delete the seven `centinel_*` snippets, the `centinel_config` dictionary, and the `centinel` backend, then activate.

## Changelog

* **v2.3.0** — Next-Gen WAF coexistence
* **v2.2.0** — Attach script and kill-switch
* **v2.1.0** — Dedicated `X-Centinel-Client` gate header
* **v2.0.0** — Header-mode validator contract
* **v1.0.0** — Initial release
