Fastly Compute (Rust)
Add Centinel Analytica bot protection to your Fastly Compute service using the Rust SDK.
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:
[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
Replace src/main.rs:
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 returns only a block or challenge response. On allow, it discards the cookies
and headers that establish the session. Do not use it as a drop-in middleware replacement.
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.
Add to fastly.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]# 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_KEYfastly resource-link create \
--version=latest \
--autoclone \
--service-id=YOUR_SERVICE_ID \
--resource-id=YOUR_STORE_IDConfiguration options
All configuration lives in the Fastly Config Store named centinel:
Prop
Type
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:
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:
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:
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:
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.
fastly config-store-entry create \
--store-id=YOUR_STORE_ID \
--key=centinel_timeout_ms \
--value=2000Verify
# Build the WASM package
fastly compute build
# Deploy to Fastly
fastly compute deployLocal testing
Configure local testing in fastly.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:
# Start local development server
fastly compute serve
# Service available at http://127.0.0.1:7676Install testing tools:
cargo install viceroy cargo-nextest
rustup target add wasm32-wasip1Create .cargo/config.toml:
[build]
target = "wasm32-wasip1"
[target.wasm32-wasip1]
runner = "viceroy run -C fastly.toml -- "Run tests:
cargo nextest run --releaseTroubleshooting
Config Store not found
Link the config store to your service:
fastly resource-link create \
--version=latest \
--autoclone \
--service-id=YOUR_SERVICE_ID \
--resource-id=YOUR_STORE_IDAll requests being allowed
Check your logs for errors:
fastly log-tail --service-id=YOUR_SERVICE_ID | grep -i centinelVerify 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
Changelog
- v0.2.3 — Relaxed
fastlydependency 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