Skip to content

Rolling main documentation

Built from commit 3d54bbd5905df26165f29b8bcbc056ded8ad7718. Content may describe unreleased behavior.

Fastly Setup ​

This guide covers setting up your Fastly account and Compute service for Trusted Server.

Support status ​

AdapterRelease statusHealthStartup statusStartup healthProvider fan-outTrusted-client-IP handlingRequest normalization
fastlyproductionpre router500yesmultipleentry-point resolve + sanitizenone

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 ​

  1. Go to manage.fastly.com and create an account if you don't have one

Create an API Token ​

  1. Log in to the Fastly control panel
  2. Go to Account > API tokens > Personal tokens
  3. Click Create token
  4. 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
  5. Click Create Token
  6. Copy the key to a secure location - you will not be able to see it again

Create a Compute Service ​

  1. Click Compute in the navigation
  2. Click Create Service
  3. Click Create Empty Service (below the main options)
  4. 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.).

  1. In your Compute service, click on the Origins section
  2. 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:

bash
fastly profile create

Follow 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:

  1. In the Fastly control panel, add your domain to the service
  2. Create a CNAME record at your DNS provider pointing to your Fastly domain
  3. 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:

toml
[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:

vcl
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 == 0 restricts the whole block to the first entry into this service. A shielded request enters the same service twice, and on the second entry client.ip is the first POP rather than the reader. Keeping the unset lines inside this guard also lets the values stamped on the first entry survive the second.
  • unset before set removes 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 the unset a 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 set lines. Client-supplied values are removed on every route, while the secret is added only on the route that reaches Trusted Server. Match on req.http.host or req.url rather than on the selected backend: those are available from the start of vcl_recv and do not change when other VCL restarts the request.
  • There is no req.restarts guard. 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 stateAddress used
One matching auth value and one bare IP valueForwarded reader
Auth value missing, empty, wrong, duplicated, or not UTF-8Immediate peer
IP value missing, duplicated, not UTF-8, or not a bare IP addressImmediate peer
Request bypassed the front doorImmediate peer
No [trusted_client_ip] section configuredImmediate 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.

Topologyclient.ip at the front doorResult
Reader to fronting Fastly service to Trusted ServerThe readerCorrect reader address
Reader to another CDN to Fastly to Trusted ServerThat CDN's nodeWrong address, and authenticated as valid
Reader directly to Trusted ServerNot applicableFalls back to the immediate peer
Fastly no-code request routingNo injection pointMechanism 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:

bash
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):

bash
fastly config-store create --name jwks_store

Trusted Server's app config lives under logical store ID trusted_server_config. Select its physical store in each deployment environment, the same way as the secret store below. Production and staging isolate their app config by selecting different physical stores, because the config entry key is the logical ID on every target:

bash
export EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME=<physical-config-store>

Secret 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 in the selected deployment environment before provisioning and deploying, alongside any config-store or KV-store overrides:

bash
export EDGEZERO__STORES__SECRETS__TRUSTED_SERVER_SECRETS__NAME=ts_secrets
ts provision --adapter fastly

Provisioning creates or reuses the physical store. A managed deployment reads the selector from its deployment environment and links the selected physical store to the target service version under the logical ID trusted_server_secrets. The runtime opens the store by that logical ID; no selector is stored in a Config Store and no service ID appears in the variable name.

Each GitHub Environment or deploy process uses the same canonical names and may select different physical resources. Production and staging can therefore run identical package bytes against different stores. A staged deploy links the staging environment's selected stores into only the staged version. Selected resources must already exist before deployment.

The custom streaming entry point opens trusted_server_secrets before loading app config, so every startup and reload resolves static credentials from the physical store linked under that ID while the portable manifest continues to declare trusted_server_secrets. Local Viceroy configuration exposes the store under the logical ID directly; see [local_server.secret_stores] in fastly.toml.

The app-config store is selected the same way, through EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME in the deployment environment. No runtime mapping is stored in a Config Store.

Create the separate request-signing store when that feature is enabled:

bash
fastly secret-store create --name signing_keys

The deployment creates the trusted_server_secrets link itself from the selected physical store. Do not create that link by hand.

Upgrading from EdgeZero v0.0.8 ​

Services deployed before this change link the physical store directly, such as ts_secrets, together with an edgezero_runtime_env Config Store. This binary opens trusted_server_secrets instead, and only a managed deployment creates that link. A plain fastly compute publish clones the active version's links, so it activates a version whose link is missing: app config fails to load and every publisher route returns 500 while /health keeps answering 200.

When upgrading an existing service:

  • Deploy only through the managed path, so the trusted_server_secrets link exists before activation: run ts deploy --adapter fastly --application-release <release-root> with EDGEZERO_MANIFEST set to the release's edgezero.toml, or use EdgeZero's deploy-fastly action.
  • Move each physical-name selector into the deploy environment before the first managed deploy. Copy every service-scoped EDGEZERO__SERVICES__<SERVICE_ID>__STORES__<KIND>__<ID>__NAME entry from edgezero_runtime_env to the unscoped EDGEZERO__STORES__<KIND>__<ID>__NAME variable. An unset selector falls back to the logical ID, so the deploy links whichever account-level store carries that name, which can belong to another service.
  • Select a KV store for logical ID trusted_server_kv. The managed deploy links every declared store, and this one is usually absent on Fastly. Set EDGEZERO__STORES__KV__TRUSTED_SERVER_KV__NAME to an existing KV store or run ts provision --adapter fastly; otherwise the deploy stops with "selected Fastly KV store trusted_server_kv does not exist".
  • Re-push staging app config under the logical key into the staging Config Store, with EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME set to that store. Entries previously pushed under <logical-store-id>_staging are ignored. Do not rerun an existing ts config push --adapter fastly --staging runbook unchanged: --staging no longer selects a separate key, so without the __NAME selector it writes the production entry in the logical-ID store, and production instances load it on their next start.
  • Remove any EDGEZERO__STORES__CONFIG__<ID>__KEY selectors. On Fastly the CLI rejects any config key other than the logical store ID, so a leftover selector makes ts config push and ts deploy fail until it is removed.
  • Delete the stale edgezero_runtime_env link and any *_staging config entries only after no active, staged, or rollback version depends on them. A version-linked selector on an older staged version still reads <logical-store-id>_staging, so removing the entry while that version is deployed makes its publisher routes return 500. This release no longer reads that store. If you set the optional TS__SANDBOX__* reuse bounds there, move them into the app-config store under the logical ID trusted_server_config, without the EDGEZERO__SERVICES__<SERVICE_ID>__ prefix.

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:

bash
fastly kv-store create --name ec_identity_store

Configure the secret-store key name in trusted-server.toml:

toml
[ec]
passphrase = "ec_passphrase"
ec_store = "ec_identity_store"

Store the high-entropy passphrase under that key in the physical store selected for trusted_server_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:

bash
openssl rand -hex 32 | tr -d '\n' | fastly secret-store-entry create \
  --store-id=<ts-secrets-store-id> \
  --name=ec_passphrase \
  --stdin

Verify stores exist:

bash
fastly kv-store list

Verify stores are linked to your active service version:

bash
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:

bash
./scripts/smoke-fastly.sh

The 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.trusted_server_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 ​

Released under the Apache License 2.0.