Rolling main documentation
Built from commit f80b604a992ba1144e6c45229a8165c81e52dae0. Content may describe unreleased behavior.
Fastly Setup
This guide covers setting up your Fastly account and Compute service for Trusted Server.
Support status
| Adapter | Release status | Health | Startup status | Startup health | Provider fan-out | Trusted-client-IP handling | Request normalization |
|---|---|---|---|---|---|---|---|
fastly | production | pre router | 500 | yes | multiple | entry-point resolve + sanitize | none |
The row above summarizes the adapter-support contract. A healthy response does not prove that configuration loaded: Fastly serves /health before it constructs the application.
Create a Fastly Account
- Go to manage.fastly.com and create an account if you don't have one
Create an API Token
- Log in to the Fastly control panel
- Go to Account > API tokens > Personal tokens
- Click Create token
- Configure the token:
- Name the token (e.g., "Trusted Server Deploy")
- Choose User Token
- Choose Global API Access
- Choose what makes sense for your organization in terms of Service Access
- Click Create Token
- Copy the key to a secure location - you will not be able to see it again
Create a Compute Service
- Click Compute in the navigation
- Click Create Service
- Click Create Empty Service (below the main options)
- Configure the service:
- Add your domain of the website you'll be testing or using
- Click Update
Configure Origins
Origins are the backend servers that Trusted Server will communicate with (ad servers, SSPs, etc.).
- In your Compute service, click on the Origins section
- For each backend you need to add:
- Enter the FQDN or IP address
- Click Add
- Enter a Name in the first field - this name will be referenced in your code (e.g.,
my_ad_integration_1) - Configure port numbers and TLS settings as needed
TIP
After saving origin information, you can select port numbers and toggle TLS on/off.
Configure Fastly CLI Profile
After installing the Fastly CLI, create a profile with your API token:
fastly profile createFollow the interactive prompts to paste your API token.
Domain Configuration
TIP
With a dev account, Fastly gives you a test domain by default (e.g., xxx.edgecompute.app). You can use this for testing before configuring your own domain.
Using Your Own Domain
When you're ready to use your own domain:
- In the Fastly control panel, add your domain to the service
- Create a CNAME record at your DNS provider pointing to your Fastly domain
- Fastly provides 2 free TLS certificates (non-wildcard) per account
TLS Requirements
- Fastly Compute only accepts client traffic via TLS (HTTPS)
- Origins and backends can be non-TLS if needed
CDN-fronted Client IP
When another CDN or Fastly service fronts Trusted Server, the Fastly Compute client address identifies the immediate edge node rather than the original reader. Geolocation, EC identity derivation, and consent jurisdiction all read that single value, so all three describe the fronting POP instead of the reader. Nothing errors: pages render and ads serve while the derived values are wrong.
Trusted Server can instead consume a reader-IP header, but only when the same request carries a shared secret that only the front door knows. Trusting the header on its own would be worse than the problem it solves, because any caller could then choose the address used for geolocation, EC identity derivation, and bot protection.
This is opt-in. With no [trusted_client_ip] section, Trusted Server keeps using the immediate peer address.
Choose dedicated header names
Prefer header names that nothing else in the fronting service uses:
[trusted_client_ip]
ip_header = "x-ts-client-ip"
auth_header = "x-ts-client-ip-auth"
shared_secret = "trusted_client_ip_shared_secret"ip_header may also be fastly-client-ip, which suits a VCL service dedicated to Trusted Server. On a service that also carries other traffic, a dedicated x- name is safer: the front-door VCL then never modifies Fastly-Client-IP, so security rules, rate limiters, logging formats, and vendor snippets that read it keep working unchanged.
Both names are validated at startup. ip_header must be fastly-client-ip or begin with x-, auth_header must begin with x-, the two must differ, and neither may reuse a Trusted Server internal header name such as x-forwarded-for or x-ts-ec. Use lowercase in the TOML.
Configure the front door
Set both headers in the fronting VCL service, reading the secret from a private (write-only) edge dictionary rather than from VCL source:
sub vcl_recv {
# Trusted Server reader-IP handoff.
if (fastly.ff.visits_this_service == 0) {
# Client-supplied copies never survive, on any route.
unset req.http.X-TS-Client-IP;
unset req.http.X-TS-Client-IP-Auth;
# Stamp only on the Trusted Server route, so the secret never reaches
# another backend.
if (req.http.host == "www.example.com") {
set req.http.X-TS-Client-IP = client.ip;
set req.http.X-TS-Client-IP-Auth =
table.lookup(ts_private_config, "trusted_client_ip_secret");
}
}
}Attach an edge dictionary named ts_private_config to the fronting service and store the secret under the key trusted_client_ip_secret. Create the dictionary as write-only so the value cannot be read back through the API or the web interface. Store the identical value in the physical store mapped from trusted_server_secrets, under the key named by trusted_client_ip.shared_secret (trusted_client_ip_shared_secret above). If either key is absent, authentication cannot succeed and Trusted Server falls back to the peer address.
Four details in that example carry weight:
fastly.ff.visits_this_service == 0restricts the whole block to the first entry into this service. A shielded request enters the same service twice, and on the second entryclient.ipis the first POP rather than the reader. Keeping theunsetlines inside this guard also lets the values stamped on the first entry survive the second.unsetbeforesetremoves every copy of each header, including one a client sent. Trusted Server ignores a forwarded address whenever either header carries more than one value, so without theunseta reader could send its own copy, force the fallback, and keep its real address out of geolocation and bot protection.- The route condition wraps only the
setlines. Client-supplied values are removed on every route, while the secret is added only on the route that reaches Trusted Server. Match onreq.http.hostorreq.urlrather than on the selected backend: those are available from the start ofvcl_recvand do not change when other VCL restarts the request. - There is no
req.restartsguard. A restart can change which backend a request reaches. Re-running the block on each pass re-evaluates the route condition, so a stamp made before a restart cannot follow the request to a different backend. On the ordinary path the host is unchanged and re-running writes the same values.
Configure the identical header names and the secret-store key name in Trusted Server. Use a cryptographically random value of at least 32 ASCII graphic bytes, encoded as hex or base64url with no whitespace, for the two store entries. The app-config blob contains only trusted_client_ip_shared_secret, not that value.
What Trusted Server does with the result
Trusted Server accepts the forwarded address only when the request carries exactly one authentication value matching the resolved shared secret byte for byte and exactly one IP value that parses directly as IPv4 or IPv6. Values are not trimmed. Both headers are removed before routing.
| Request state | Address used |
|---|---|
| One matching auth value and one bare IP value | Forwarded reader |
| Auth value missing, empty, wrong, duplicated, or not UTF-8 | Immediate peer |
| IP value missing, duplicated, not UTF-8, or not a bare IP address | Immediate peer |
| Request bypassed the front door | Immediate peer |
No [trusted_client_ip] section configured | Immediate peer |
No combination rejects the request. A misconfigured front door, a rotated secret, or a renamed header degrades to the current behavior rather than causing an outage.
Confirm the topology first
The VCL example assumes the reader connects directly to the fronting Fastly service.
| Topology | client.ip at the front door | Result |
|---|---|---|
| Reader to fronting Fastly service to Trusted Server | The reader | Correct reader address |
| Reader to another CDN to Fastly to Trusted Server | That CDN's node | Wrong address, and authenticated as valid |
| Reader directly to Trusted Server | Not applicable | Falls back to the immediate peer |
| Fastly no-code request routing | No injection point | Mechanism unavailable |
The second row is the one topology that fails with a wrong value instead of falling back. If another CDN precedes Fastly, restrict direct access to the Fastly front door and derive ip_header from that CDN's protected reader-IP value instead of from client.ip.
No-code request routing limitation
Fastly no-code request routing does not provide a point to inject these headers. If that routing path does not preserve the original reader IP, Trusted Server cannot recover it with this mechanism. Use a fronting service that can set both headers before forwarding the request.
Verify before trusting it
Geolocation response headers are the observable signal. From a client whose real location differs from the fronting POP, compare a request through the front door against one sent directly to the Compute service, and confirm x-geo-city and x-geo-coordinates agree.
Then confirm the anti-evasion path holds by sending duplicate and junk trust headers through the front door:
curl -sD - -o /dev/null 'https://www.example.com/' \
-H 'X-TS-Client-IP: 198.51.100.7' \
-H 'X-TS-Client-IP: 203.0.113.10' \
-H 'X-TS-Client-IP-Auth: not-the-secret'The geolocation headers should still describe the reader. If they describe the fronting POP, something upstream is leaving a client-supplied copy in place. IPv6 readers are worth testing separately, because Trusted Server requires a bare address and rejects bracketed, ported, or zone-suffixed forms.
Effect on origin requests
Trusted Server treats fastly-client-ip as client-spoofable and strips it at request entry whether or not [trusted_client_ip] is configured, so it no longer forwards an inbound Fastly-Client-IP to the publisher origin. Check whether the origin reads that header for geolocation, fraud checks, or logging before deploying. Reconstructing a trustworthy client-address header for origin requests is tracked separately from this mechanism.
Create Config and Secret Stores
For features like request signing, you'll need to create Fastly stores:
Config Store
Used for storing public configuration (e.g., public keys, key metadata):
fastly config-store create --name jwks_storeSecret Stores
Trusted Server keeps static app-config credentials under logical store ID trusted_server_secrets. The physical Fastly store can use another name, such as ts_secrets. Request-signing private keys remain in their separate, runtime-managed store.
Create or select the service before provisioning a non-default mapping. Set its ID in fastly.toml or FASTLY_SERVICE_ID; if both are set, they must agree. Provisioning rejects non-default mappings without a service ID. Set the physical mapping before provisioning, alongside any config-store or KV-store overrides:
export EDGEZERO__STORES__SECRETS__TRUSTED_SERVER_SECRETS__NAME=ts_secrets
ts provision --adapter fastlyProvisioning creates or reuses the physical store and persists this runtime mapping in Fastly Config Store edgezero_runtime_env, scoped to the current Fastly service:
EDGEZERO__SERVICES__<SERVICE_ID>__STORES__SECRETS__TRUSTED_SERVER_SECRETS__NAME=ts_secretsThe runtime ignores legacy unscoped entries. The Fastly service must link both ts_secrets and edgezero_runtime_env to the active service version. The custom streaming entry point reads the service-scoped mapping before loading app config, so every startup and reload resolves static credentials from ts_secrets while the portable manifest continues to declare trusted_server_secrets.
The same runtime mapping mechanism applies to app-config stores. See Fastly runtime config stores for the initial provisioning and linking sequence, subsequent CLI pushes, and precautions when changing a live mapping. A process-environment override alone does not configure the Fastly runtime.
Create the separate request-signing store when that feature is enabled:
fastly secret-store create --name signing_keysDo not copy the same app credential store under a second hardcoded trusted_server_secrets Fastly link. Configure the mapping instead.
Create EC KV Store
Edge Cookie flows require one KV store:
- Identity graph store (
ec_store) - EC identity graph, source-domain keyed partner UIDs, minimal consent metadata, and withdrawal tombstones
Partners are configured statically in [[ec.partners]] and loaded into an in-memory registry at startup. There is no separate consent KV store. Consent is interpreted from live request cookies, headers, geolocation, and policy defaults.
Create it:
fastly kv-store create --name ec_identity_storeConfigure the secret-store key name in trusted-server.toml:
[ec]
passphrase = "ec_passphrase"
ec_store = "ec_identity_store"Store the high-entropy passphrase under that key in ts_secrets. The resolved value, rather than the key name, must contain at least 32 characters. Pipe the value over standard input so it never appears in the process argument list:
openssl rand -hex 32 | tr -d '\n' | fastly secret-store-entry create \
--store-id=<ts-secrets-store-id> \
--name=ec_passphrase \
--stdinVerify stores exist:
fastly kv-store listVerify stores are linked to your active service version:
fastly resource-link list --service-id <service-id> --version <active-version>If EC sync returns kv_unavailable or identify responses are degraded, first check that the identity store is present and linked to the active version.
Before upgrading a deployment that used the legacy consent store, remove [consent].consent_store from TOML or JSON/app-config; strict configuration loading rejects the removed field. Remove its local Viceroy fixture and active Fastly resource link as well. The old records are not read or migrated into ec.ec_store and must not be copied there. You may retain the old store unchanged for a defined rollback window, then delete the store and its records. Withdrawals processed after the upgrade do not actively delete retained legacy records; they remain until their original TTL expires or an operator deletes them. If a rollback uses an older release that reads the legacy store, account for those stale records and keep the rollback window short.
Verify the complete local handoff
Run the repository smoke from a clean shell:
./scripts/smoke-fastly.shThe script creates an isolated application config, applies its publisher-origin overrides, and runs strict validation. It then executes ts config push --adapter fastly --local, adds all three required entries to [local_server.secret_stores.ts_secrets], and starts fastly compute serve through Viceroy. The required keys are handler_password, publisher_proxy_secret, and ec_passphrase.
The check deliberately proves both halves of startup. With no config entry, it requires /health to return 200 while the publisher route returns 500 with the missing trusted_server_config diagnostic. It then removes each required secret independently and requires the corresponding setting path to fail. Finally, the publisher request must return 200, retain the stub-origin sentinel, rewrite an origin URL to the Fastly listener, and omit the original URL. A green health response cannot satisfy that final assertion.
The script copies edgezero.toml and fastly.toml into a per-run temporary project before pushing local configuration or adding synthetic secrets. The tracked manifests remain untouched, including when smoke runs overlap. Its trap stops Viceroy and the stub origin and removes the temporary project. For a deployed service, provision and link the stores described above and write the three secrets through Fastly's secret-store interface.
Next Steps
- Return to Getting Started to continue setup
- See Configuration for detailed configuration options
- See EC Setup Guide for end-to-end EC verification
- See Request Signing for setting up cryptographic signing
- Compare the Cloudflare, Spin, and Axum adapter journeys