# Fastly Compute (Rust)

> Add Centinel Analytica bot protection to your Fastly Compute service using the Rust SDK.

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

## Overview

The Centinel Rust crate integrates with Fastly Compute to validate requests at the edge before they reach your origin. Suspicious requests are blocked or redirected; legitimate traffic and browser script injection are handled automatically. If the API is unavailable, requests pass through to keep your service online.

## Prerequisites

* Fastly account with Compute enabled
* Centinel secret key (server-side validation)
* Centinel site key (browser script)
* Rust 1.74 or later
* Fastly CLI installed

## Install

Add the Centinel crate to your Fastly Compute project:

```toml
[dependencies]
centinel-analytica-fastly = "0.2"
fastly = "0.12"
```

Both lines matter. The 0.2 crate requires `fastly` 0.12 or newer, and pinning `fastly = "0.11"`
alongside it puts two incompatible versions of the SDK in your dependency graph, which fails to
compile.

## Configure

1. **Update your main function**

   Replace `src/main.rs`:

   ```rust
   use centinel_analytica_fastly::Centinel;
   use fastly::{Error, Request, Response};

   #[fastly::main]
   fn main(req: Request) -> Result<Response, Error> {
       match Centinel::new() {
           Ok(centinel) => centinel.handle_request(req, |r| r.send("origin")),
           Err(_) => Ok(req.send("origin")?), // misconfigured: fail open
       }
   }
   ```

   > **Use handle\_request for normal integrations:** `handle_request` applies validator cookies and response headers. It adds `Server-Timing` after
   > validation. When a site key is configured, it injects the browser script into HTML responses.
   > `check_request` is deprecated since v0.2.4. It returns only a block or challenge response. On
   > `allow`, it discards the cookies and headers that establish the session, so every request starts
   > a new one. Move the code that followed it into the `handle_request` closure.

   Handling the `Centinel::new()` error matters too. Using `?` there turns a missing config store or a
   missing `centinel_secret_key` into a 500 for every request, which fails closed on a
   misconfiguration rather than open.

2. **Configure backends**

   Add to `fastly.toml`:

   ```toml
   [setup.backends]
   [setup.backends.origin]
   address = "your-origin.com"
   port = 443

   [setup.backends.centinel]
   address = "validator.centinelanalytica.com"
   port = 443
   override_host = "validator.centinelanalytica.com"
   use_ssl = true
   max_connections = 500

   [setup.config_stores]
   [setup.config_stores.centinel]
   ```

3. **Create config store**

   ```bash
   # Create the config store
   fastly config-store create --name=centinel

   # Add your secret key
   fastly config-store-entry create \
     --store-id=YOUR_STORE_ID \
     --key=centinel_secret_key \
     --value=YOUR_SECRET_KEY

   # Add your site key (for browser script)
   fastly config-store-entry create \
     --store-id=YOUR_STORE_ID \
     --key=centinel_site_key \
     --value=YOUR_SITE_KEY
   ```

4. **Link config store to service**

   ```bash
   fastly resource-link create \
     --version=latest \
     --autoclone \
     --service-id=YOUR_SERVICE_ID \
     --resource-id=YOUR_STORE_ID
   ```

### Configuration options

All configuration lives in the Fastly Config Store named `centinel`:

| Name                           | Type      | Default | Description                                                                    |
| ------------------------------ | --------- | ------- | ------------------------------------------------------------------------------ |
| `centinel_secret_key`          | `string`  |         | Server-side secret key (required)                                              |
| `centinel_site_key`            | `string`  |         | Browser script key (recommended)                                               |
| `centinel_url_exclusion`       | `string`  |         | Regex pattern for URLs to exclude                                              |
| `centinel_url_inclusion`       | `string`  |         | Regex pattern for URLs to include                                              |
| `centinel_timeout_ms`          | `number`  |         | API timeout in milliseconds (default: 300)                                     |
| `centinel_origin_backend_name` | `string`  |         | Origin backend name (default: origin)                                          |
| `centinel_backend_name`        | `string`  |         | Centinel validator backend name (default: centinel)                            |
| `centinel_api_endpoint`        | `string`  |         | Override validator API URL (default: https\://validator.centinelanalytica.com) |
| `centinel_debug`               | `boolean` |         | Enable debug logging (default: false)                                          |

### Advanced configuration

#### Custom routing

Route different paths through `Centinel::protect`. It applies validator cookies and response
headers, adds `Server-Timing` after validation, and injects the browser script into HTML responses
when a site key is configured:

```rust
use centinel_analytica_fastly::Centinel;
use fastly::{Error, Request, Response};

#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
    Centinel::protect(req, |req| {
        let backend = if req.get_path().starts_with("/api/") {
            "api_backend"
        } else {
            "origin"
        };

        Ok(req.send(backend)?)
    })
}
```

#### Health check bypass

Skip Centinel for health checks:

```rust
use centinel_analytica_fastly::Centinel;
use fastly::{Error, Request, Response};

#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
    if req.get_path() == "/health" {
        return Ok(Response::from_status(200)
            .with_body_text_plain("OK\n"));
    }

    Centinel::protect(req, |req| Ok(req.send("origin")?))
}
```

#### URL exclusions

Exclude static assets from bot protection:

```bash
fastly config-store-entry create \
  --store-id=YOUR_STORE_ID \
  --key=centinel_url_exclusion \
  --value='\.(css|js|png|jpg|svg|woff2?)$'
```

#### Protect specific paths

Only protect API routes:

```bash
fastly config-store-entry create \
  --store-id=YOUR_STORE_ID \
  --key=centinel_url_inclusion \
  --value='^/api/'
```

#### Custom timeout

`centinel_timeout_ms` applies only when the SDK builds a dynamic backend. If you declared a
`centinel` backend in `fastly.toml`, as the setup above does, requests go through that backend and
this key is ignored. Tune the backend's own connect and first-byte timeouts instead.

```bash
fastly config-store-entry create \
  --store-id=YOUR_STORE_ID \
  --key=centinel_timeout_ms \
  --value=2000
```

## Verify

1. **Deploy**

   ```bash
   # Build the WASM package
   fastly compute build

   # Deploy to Fastly
   fastly compute deploy
   ```

### Local testing

**Local server**

Configure local testing in `fastly.toml`:

```toml
[local_server.config_stores]
[local_server.config_stores.centinel]
format = "inline-toml"
[local_server.config_stores.centinel.contents]
centinel_secret_key = "test-secret-key"
centinel_site_key = "test-site-key"
```

Run locally:

```bash
# Start local development server
fastly compute serve

# Service available at http://127.0.0.1:7676
```

**Unit tests**

Install testing tools:

```bash
cargo install viceroy cargo-nextest
rustup target add wasm32-wasip1
```

Create `.cargo/config.toml`:

```toml
[build]
target = "wasm32-wasip1"

[target.wasm32-wasip1]
runner = "viceroy run -C fastly.toml -- "
```

Run tests:

```bash
cargo nextest run --release
```

## Troubleshooting

**Config Store not found**

Link the config store to your service:

```bash
fastly resource-link create \
  --version=latest \
  --autoclone \
  --service-id=YOUR_SERVICE_ID \
  --resource-id=YOUR_STORE_ID
```

**All requests being allowed**

Check your logs for errors:

```bash
fastly log-tail --service-id=YOUR_SERVICE_ID | grep -i centinel
```

Verify your secret key is correct and increase timeout if needed.

**Script not injecting**

Make sure `centinel_site_key` is set in your config store and responses have `Content-Type: text/html`.

### Resources

* **Package**: [crates.io/crates/centinel-analytica-fastly](https://crates.io/crates/centinel-analytica-fastly)

## Changelog

* **v0.2.4** — Session cookie kept; `check_request` deprecated
* **v0.2.3** — Relaxed `fastly` dependency to 0.12+
* **v0.2.1** — Non-UTF-8 header panic fix
* **v0.2.0** — Breaking: Server-Timing, full headers
* **v0.1.11** — Hardened validator client
* **v0.1.6** — Response header passthrough
