Fastly VCL
Deploy Centinel Analytica on your Fastly VCL service via Terraform or the Fastly Web UI.
Overview
Seven VCL snippets and a Terraform module add an edge-side validation step against the Centinel /validate API. Requests are checked, blocked, redirected, or forwarded to your origin based on the validator's decision. Fail-open by default. Takes 2-3 minutes to roll out across Fastly's edge.
API key storage
The integration stores the API key in a Fastly Edge Dictionary named centinel_config. By default this is a standard (non-private) dictionary, so the value is visible in the Fastly dashboard and via the API to anyone who can read the service.
To hide the value from list/read endpoints, mark the dictionary as private (write_only=true). Private dictionaries can only be created via the Fastly API, not the Web UI. Even when private, Fastly stores the value in plaintext on its infrastructure.
Pure VCL services cannot use Fastly's Secret Store (Compute@Edge-only). If you need encryption-at-rest for the API key, use the Fastly Compute (Rust) integration instead.
Prerequisites
- Centinel API key (for validator authentication)
- Fastly account with API token (Account → Personal API Tokens, full-service access)
- A configured origin backend
- Terraform 1.0+ (recommended) or access to the Fastly Web UI
Download
Download centinel-fastly.zip — contains the seven VCL snippet files (init, recv, pass, miss, fetch, deliver, error), the Terraform module (main.tf), and a README explaining the snippet contract.
How it works
On every request, the snippets:
- Skip static assets and any path matching the configured exclusion regex in
recv.vcl. - POST request metadata to
validator.centinelanalytica.com/validatewithUser-Agent: centinel-fastly-vcl/2.0.0. Because pure Fastly VCL cannot set a custom POST body, the payload is sent asX-Centinel-*request headers (URL, method, IP, cookie, referrer, and original UA/Accept headers). - Read the validator's decision from
x-centinel-*response headers. No JSON parsing happens in VCL. - Apply the decision:
allow/not_matched→ forward to originblock→ 403 + custom HTML body (decoded from base64)redirect→ 200 + verification page
- Forward
Set-Cookiefrom the validator and a whitelisted set of six response headers (Content-Type,Cache-Control,X-Content-Type-Options,X-Frame-Options,Content-Security-Policy,Referrer-Policy).
If the validator returns a non-2xx response or is unreachable, requests fail-open to origin.
Method 1: Terraform deployment
Step 1 — Install prerequisites
# macOS
brew install terraform
terraform --version # >= 1.0Step 2 — Set up authentication
export FASTLY_API_KEY="your-fastly-api-token"Create an API token in your Fastly dashboard under Account → Personal API Tokens. The token needs full-service access.
Step 3 — Configure variables
In the snippets/ directory from the downloaded zip, create terraform.tfvars:
centinel_api_key = "your-centinel-api-key"
domain_name = "www.example.com"
origin_address = "origin.example.com"
service_name = "Centinel Production"
# Optional:
# origin_port = 443
# origin_use_ssl = true
# debug = false
# validator_host = "validator.centinelanalytica.com"Step 4 — Initialize and deploy
terraform init
terraform plan
terraform applyThis creates:
- A Fastly service with your domain
- An
originbackend pointing atorigin_address - A
centinelbackend pointing atvalidator.centinelanalytica.com - An Edge Dictionary named
centinel_configholding the API key and debug flag. Non-private by default. To make it private, setdictionary_write_only = truein tfvars and populate items via the Fastly API after apply (the Terraform provider can't read write_only dictionaries). - 7 VCL snippets at priority 50 (
init,recv,pass,miss,fetch,deliver,error)
Step 5 — Verify
terraform output service_id
terraform output service_domain
# Hit your domain
curl -i https://www.example.com/
# Expect: 200 OK, Set-Cookie: _centinel=...; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=86400
# and: Server-Timing: validator;dur=<ms>Method 2: Manual installation via Fastly Web UI
Step 1 — Prepare your service
- Log into your Fastly dashboard.
- Select your service or create a new one.
- Click Clone version to create an editable draft (you can't edit active versions).
Step 2 — Create the origin backend
In Origins → Hosts, add your origin backend (name it origin).
Step 3 — Create the centinel backend
In Origins → Hosts, add a second backend:
- 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:
3000ms - First byte timeout:
5000ms - Between bytes timeout:
2000ms
Step 4 — Create the Edge Dictionary
In Edge Dictionaries → Create:
- Name:
centinel_config - Add items:
secret_key→<your-centinel-api-key>debug→false
- Save.
Want a private dictionary?
Private (write_only) dictionaries can only be created via the Fastly API, not the Web UI. After creating the regular dictionary above, you can recreate it as private via:
fastly dictionary create --service-id=<SID> --version=<active>+1 \
--name=centinel_config --write-onlythen re-add items via fastly dictionary-item create. Private dictionaries hide values from list endpoints but Fastly still stores them in plaintext.
Step 5 — Upload the seven VCL snippets
In VCL Snippets → Create snippet, repeat for each file in the snippets/ directory from the downloaded zip:
| File | Snippet type | Name | Priority |
|---|---|---|---|
init.vcl | init | centinel_init | 50 |
recv.vcl | recv | centinel_recv | 50 |
pass.vcl | pass | centinel_pass | 50 |
miss.vcl | miss | centinel_miss | 50 |
fetch.vcl | fetch | centinel_fetch | 50 |
deliver.vcl | deliver | centinel_deliver | 50 |
error.vcl | error | centinel_error | 50 |
Paste each file's contents verbatim. You do not edit a placeholder API key. The snippets read the key from the dictionary at runtime via table.lookup(centinel_config, "secret_key", "").
Step 6 — Activate the service
- Review all snippets and backends.
- Click Activate on the new version.
- Wait 2-3 minutes for the new VCL to propagate globally.
Step 7 — Verify
curl -i https://www.example.com/
# Expect:
# HTTP/1.1 200 OK
# Set-Cookie: _centinel=...; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=86400
# Server-Timing: validator;dur=<ms>Operating the integration
Customizing the path filter
The static-asset exclusion regex is hardcoded in recv.vcl because Fastly VCL does not allow regex patterns to be sourced from variables. To customize, either:
- Edit
snippets/recv.vcldirectly before upload, OR - Use Terraform's
replace()/templatefile()to substitute the regex at deploy time.
The default excludes common static-asset extensions: avi, avif, bmp, css, eot, flac, flv, gif, gz, ico, jpeg, jpg, js, json, less, map, mka, mkv, mov, mp3, mp4, mpeg, mpg, ogg, ogm, opus, otf, png, svg, svgz, swf, ttf, wav, webm, webp, woff, woff2, xml, zip.
Self-hosted validator
If you run a self-hosted Centinel validator, set the Terraform variable validator_host = "<your-host>". For UI installs, edit the centinel backend you created in Step 3.
Debug mode
Set debug = "true" on the dictionary item (or debug = true in Terraform). The response then includes x-centinel-decision, x-centinel-request-id, x-centinel-crawler-name, and x-centinel-crawler-category headers. Useful for support and curl debugging. Turn off in production.
Disabling temporarily
Delete the secret_key dictionary item. The validator returns 401, fetch.vcl fails open, and all requests flow to origin until you re-add the key. Alternatively, delete the centinel_recv snippet to bypass validation entirely.
Rotating the API key
# Terraform path
terraform apply -replace=fastly_service_dictionary_items.centinel_config
# UI path: edit the `secret_key` item in the centinel_config dictionary.Validator outage handling
The integration is fail-open: if the validator returns a non-2xx response, times out, or is unreachable, the request goes through to origin. You'll see x-centinel-failed: 1 in responses when debug=true, and the response still includes Server-Timing: validator;dur=<ms>.
Configuration reference
Terraform variables
| Variable | Type | Required | Default | Description |
|---|---|---|---|---|
centinel_api_key | string | yes | – | Centinel validator API key. Stored in the dictionary; required for validation. |
domain_name | string | yes | – | Your service domain (e.g. www.example.com). |
origin_address | string | yes | – | Origin backend address. |
service_name | string | no | centinel_protected_service | Fastly service name. |
origin_port | number | no | 443 | Origin port. |
origin_use_ssl | bool | no | true | Use TLS to origin. |
validator_host | string | no | validator.centinelanalytica.com | Override the validator hostname (self-hosted setups). |
validator_ssl_check_cert | bool | no | true | Strict cert validation on the validator backend. Set false only for test setups behind shared-cert proxies. |
debug | bool | no | false | Echo x-centinel-* debug headers on the client response. |
dictionary_write_only | bool | no | false | Mark centinel_config as private (write_only). When true, populate items via the Fastly API after terraform apply; the Terraform provider cannot read write_only dictionaries. |
Edge Dictionary items (centinel_config)
| Key | Required | Description |
|---|---|---|
secret_key | yes | Centinel validator API key. |
debug | no | "true" or "false" (string). When "true", debug headers leak to client responses. |
Snippet contract
Upload all snippets at priority 50. Names must match the centinel_<type> convention so future updates can find them.
Limitations
The integration is constrained by what pure Fastly VCL supports. Known caps for v2.0.0:
- Cookies cap = 1. VCL extracts only the first cookie from the validator's
x-centinel-cookies-jsonresponse header (Fastly VCL has no JSON parser; regex extraction handles only one). Today the validator returns at most one cookie (_centinel), so this is a non-issue in practice. - Out-header whitelist is fixed. VCL forwards a hardcoded set of 6 response headers (
Content-Type,Cache-Control,X-Content-Type-Options,X-Frame-Options,Content-Security-Policy,Referrer-Policy). Adding more requires editingdeliver.vcl. - No collector script injection. Body modification is fragile in pure VCL. Add the Centinel collector
<script>tag directly in your HTML, or use the Fastly Compute (Rust) integration which supports injection. - API key is plaintext on Fastly infra. The
centinel_configEdge Dictionary stores values in plaintext by default; this is visible in the dashboard and via the Fastly API. Marking the dictionary as private (write_only=true, API-only) hides the value from list/read endpoints, but Fastly still stores it in plaintext on its infrastructure. For encryption-at-rest, switch to the Fastly Compute (Rust) integration which uses Fastly Secret Store. - Backend health-pre-check is not available. Pure VCL cannot inspect a named backend's
.healthyfrom outside its currently-assigned scope. Outages incur one validator timeout per request before fail-open kicks in.
Troubleshooting
Requests return 200 from origin but no Set-Cookie is set
The validator was unreachable. Look for:
Server-Timing: validator;dur=<ms>in the response confirms the validator was attempted.x-centinel-failed: 1(whendebug=true) confirms fail-open fired.- Check network connectivity from Fastly to
validator.centinelanalytica.com. - Verify the
secret_keydictionary item is set (a missing key causes 401 from validator → fail-open).
Requests blocked unexpectedly
- Set
debug=trueand inspectx-centinel-decisionandx-centinel-request-id. - Check your Centinel dashboard for the corresponding request ID.
terraform apply fails with "Not allowed to read contents of write_only dictionary"
You set dictionary_write_only = true. The Fastly Terraform provider cannot read items from write_only dictionaries. Either:
- Set
dictionary_write_only = false(default) and accept that dictionary items appear in plan output, OR - Set
dictionary_write_only = trueAND populate items via the Fastly API after first apply, then remove thefastly_service_dictionary_itemsresource from the Terraform module.
Static asset paths still get validated
Your file extension isn't in the default regex in recv.vcl. Edit the regex literal to add it, then redeploy.
Changelog
- 2.0.0 — Rewrite. Header-mode response from validator. Edge Dictionary for the API key (optionally private via API). Backoff via Fastly's backend health probe +
vcl_fetchfail-open. End-to-end verified on real Fastly. - 1.0.0 — Initial release. Superseded by v2.