Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 82 additions & 46 deletions docs/guide/ts-dev-proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,44 +33,68 @@ logic, CMP/consent flows, first-party context — before any DNS cutover.
**Non-goals.** Not a production proxy or load-testing tool. Does not modify any
Fastly service. Local only, developer-facing.

## Build and run
## Install and run

`ts dev proxy` is part of `crates/trusted-server-cli`, a workspace member that
builds for the host. Because the workspace default target is `wasm32-wasip1`
(where the CLI is an empty shell), build and run it with an explicit native
target:
`ts dev proxy` is a subcommand of the `ts` CLI, and is **macOS-only** — its
dependencies are scoped to macOS, so the command is not present in the CLI on
other platforms. On macOS, install (or update) the `ts` binary from the
repository root (see [the CLI guide](./cli.md#install-from-source) for details):

```bash
cargo run --manifest-path crates/trusted-server-cli/Cargo.toml \
--target "$(rustc -vV | sed -n 's/host: //p')" -- dev proxy --help
cargo install-cli
```

Rerun `cargo install-cli` after pulling CLI changes to refresh the installed
binary. Then trust the dev CA once (see
[the development CA](#the-development-ca)), map the production hostname to your
upstream, and launch a browser:

```bash
ts dev proxy ca install
ts dev proxy \
--map www.example-publisher.com=trusted-server-example.edgecompute.app \
--rewrite-host \
--launch chrome
```

`--rewrite-host` sends the upstream `Host: <TO>` so upstreams that reject the
default `Host: <FROM>` — most dev and staging services, which aren't configured
for the production hostname — still serve a page. Trade-off: against a real
Trusted Server adapter, first-party URLs then render on the upstream host, not the
production domain. To keep them on the production domain, point at an upstream that
accepts `Host: <FROM>` and omit `--rewrite-host` — see
[Host header behavior](#host-header-behavior).

Run `ts dev proxy --help` to list every flag.

### Passing the rewrite rule

The upstream is always passed explicitly — there is no inference from
`trusted-server.toml` or any config file. Give a single rule with the `-f`/`-t`
shorthand, or one or more `--map FROM=TO` rules:

```bash
cargo run --manifest-path crates/trusted-server-cli/Cargo.toml \
--target "$(rustc -vV | sed -n 's/host: //p')" -- dev proxy \
-f www.example-publisher.com -t trusted-server-example.edgecompute.app
ts dev proxy -f www.example-publisher.com -t trusted-server-example.edgecompute.app
```

With no `--map`/`-f`/`-t`, the proxy exits with
`no rewrite rule: pass --map FROM=TO (or -f/--from with -t/--to)`.

Connection options — `--rewrite-host`, `--basic-auth`/`--basic-auth-file`,
`--insecure`, and `--upstream-plaintext` — apply to every mapping, not per-rule.

### Explicit rule and browser launch

```bash
cargo run --manifest-path crates/trusted-server-cli/Cargo.toml \
--target "$(rustc -vV | sed -n 's/host: //p')" -- dev proxy \
ts dev proxy \
--map www.example-publisher.com=trusted-server-example.edgecompute.app \
--launch chrome,firefox,safari
```

`--launch` takes a comma list (`chrome`, `firefox`, `safari`) or `all`. When
omitted the proxy runs without opening any browser.
omitted the proxy runs without opening any browser. With multiple `--map` rules,
every mapping is proxied, but `--launch` opens only the first rule's `FROM`
hostname — navigate to the others manually.

### Other examples

Expand All @@ -79,7 +103,8 @@ omitted the proxy runs without opening any browser.
ts dev proxy \
-f www.example-publisher.com \
-t staging.example.net \
--basic-auth dev:secret \
--rewrite-host \
--basic-auth-file ./staging-creds.txt \
--launch firefox

# Local instance over plain HTTP, no browser:
Expand All @@ -89,23 +114,25 @@ ts dev proxy \
--upstream-plaintext
```

## First run: CA setup
## The development CA

On first run the proxy generates a per-machine Certificate Authority and prints:
The proxy relies on a per-machine Certificate Authority that your browser must
trust. `ts dev proxy ca install` (in the quick start above) generates it if
needed and trusts it in one step, so you don't have to run the proxy first.
Running the proxy also generates the CA on first use, printing:

```
generated dev CA at ~/Library/Application Support/trusted-server/dev-proxy/ca-cert.pem
— run `ts dev proxy ca install` to trust it
```

The CA key is stored with mode `0600` outside the repository and is never
committed.
The CA key is written with mode `0600` and, by default, is stored outside the
repository; it is never committed.

### Trust the CA on macOS (Chrome and Safari)

```bash
cargo run --manifest-path crates/trusted-server-cli/Cargo.toml \
--target "$(rustc -vV | sed -n 's/host: //p')" -- dev proxy ca install
ts dev proxy ca install
```

This adds the CA to the macOS login keychain (no `sudo` required; prompts for
Expand All @@ -115,23 +142,23 @@ will trust the proxy's certificates immediately.
### Trust the CA in Firefox

Firefox does not reliably consult the macOS login keychain. When you use
`--launch firefox`, the proxy automatically imports the CA into the temporary
Firefox profile's NSS database using `certutil`. If you are pointing an existing
Firefox profile at the proxy manually, run:
`--launch firefox`, the proxy imports the CA into the temporary Firefox profile's
NSS database using `certutil`. `certutil` is not built into macOS — install it
with `brew install nss`; without it the proxy prints a warning and Firefox opens
with certificate errors. If you are pointing an existing Firefox profile at the
proxy manually, run:

```bash
certutil -A -n "Trusted Server DEV-ONLY Proxy CA" \
certutil -A -n "Trusted Server DEV-ONLY Proxy CA — DO NOT TRUST IN PRODUCTION" \
-t "CT,," \
-i "$(cargo run --manifest-path crates/trusted-server-cli/Cargo.toml \
--target "$(rustc -vV | sed -n 's/host: //p')" -- dev proxy ca path)" \
-d "$HOME/Library/Application Support/Firefox/Profiles/<profile>"
-i "$(ts dev proxy ca path)" \
-d "sql:$HOME/Library/Application Support/Firefox/Profiles/<profile>"
```

### Revoking trust when done

```bash
cargo run --manifest-path crates/trusted-server-cli/Cargo.toml \
--target "$(rustc -vV | sed -n 's/host: //p')" -- dev proxy ca uninstall
ts dev proxy ca uninstall
```

This removes the CA from the macOS keychain. Run it when you are finished —
Expand Down Expand Up @@ -172,11 +199,14 @@ trust the new CA.
The proxy always sends `X-Forwarded-Host: <FROM>` (the production hostname) — the
standard "original host" header for a forward proxy. Trusted Server core anchors
all HTML/URL rewriting to it (it prefers `X-Forwarded-Host`, then `Host`), so
**first-party URLs always stay on the production domain regardless of the `Host`
header**. That decouples routing (`Host`) from the first-party host.
**first-party URLs stay on the production domain regardless of the `Host` header —
as long as the upstream preserves `X-Forwarded-Host`**. That decouples routing
(`Host`) from the first-party host. (Real Trusted Server adapters strip inbound
`X-Forwarded-Host`; the caveat below covers what that means with `--rewrite-host`.)

By default `Host: <FROM>` too, which works against a Trusted Server Compute
upstream because Fastly routes by SNI (`= TO`) and passes `Host` through unchanged.
By default `Host: <FROM>` too. Fastly routes by SNI (`= TO`) and passes `Host`
through unchanged, so `Host: <FROM>` reaches the upstream — but the upstream still
has to be configured to accept it, which a dev or staging service usually isn't.

**Targeting a specific server by IP.** To point at a particular server or load
balancer — for example when the `TO` hostname isn't in DNS yet — keep `--to` a
Expand Down Expand Up @@ -208,9 +238,12 @@ hostname (e.g. a Fastly Deliver service that rejects an unconfigured `Host`), pa
> **Caveat with real Trusted Server adapters.** The Fastly and Spin adapter
> request paths strip inbound `X-Forwarded-Host` before routing, so with
> `--rewrite-host` a real Trusted Server upstream falls back to `Host` (`TO`) and
> emits first-party URLs on `TO`, not `FROM`. Prefer the default (no
> `--rewrite-host`) with Trusted Server upstreams; reach for `--rewrite-host`
> only when the upstream rejects an unconfigured `Host`.
> emits first-party URLs on `TO`, not `FROM`. Even so, `--rewrite-host` is the
> right choice for most upstreams — dev and staging services rarely have the
> production hostname configured and would reject the plain `Host: <FROM>`. Drop
> it only when the upstream is configured to accept `Host: <FROM>` and you
> specifically need first-party URLs anchored to `FROM` — the `--resolve` example
> above is exactly that case.

The TLS SNI is always the `TO` host either way:

Expand Down Expand Up @@ -257,11 +290,14 @@ Options:
--basic-auth-file <PATH> Read USER:PASS from a file
--insecure Skip upstream TLS certificate verification
--upstream-plaintext Connect to upstream over plain HTTP
--connect-timeout <SECONDS> Upstream connect timeout in seconds [default: 10]
--ca-dir <PATH> CA cert/key directory [default: ~/Library/Application Support/
trusted-server/dev-proxy on macOS]
```

The tool is flags-only; there are no environment variable overrides.
The tool is flags-only; there are no environment variable overrides. The
`[COMMAND]` slot is the `ca` subcommand — see
[CA companion commands](#ca-companion-commands).

## Browser details

Expand All @@ -281,7 +317,7 @@ the terminal (only that command is elevated; the proxy keeps running as you). If
`sudo` is declined or there is no terminal (e.g. the proxy is backgrounded), it
prints the exact `networksetup` command and the System Settings path so you can
set the PAC manually. The change is system-wide (all apps) but PAC-scoped to the
`FROM` hosts, and only while the proxy runs. The prior setting — including
`FROM` hosts, and — on a clean exit — only while the proxy runs. The prior setting — including
whether auto-proxy was enabled or disabled — is saved and restored. On a clean
exit (Ctrl-C) the restore uses `sudo` and may prompt once more if a long run
outlived the cached credential. After a hard kill (`SIGKILL`) the next
Expand All @@ -291,11 +327,11 @@ command.

## Troubleshooting

| Symptom | Cause | Fix |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| "unknown domain" or `404` from upstream | The upstream service does not accept `Host: <FROM>` (the default). A domain can be active on only one Fastly service at a time, so you cannot add the production hostname to a dev service. | Use a Trusted Server Compute upstream (routes by SNI, not `Host`), or pass `--rewrite-host` to send `Host: <TO>`. |
| Upstream returns `401` | Upstream is behind Basic auth. | Pass `--basic-auth user:pass` or `--basic-auth-file ./creds.txt`. |
| Upstream unreachable (`502` / `503`) | Upstream service is down or the domain is not provisioned. | Verify the upstream URL and its Fastly service health. |
| Browser shows an untrusted-certificate warning | The dev CA is not trusted in the browser. | Run `ts dev proxy ca install` for Chrome and Safari. For Firefox, use `--launch firefox` (auto-imports) or run `certutil` manually (see above). After `ca regenerate`, re-trust with `ca install`. |
| Listen address already in use | Another process holds port 18080. | Pass `--listen 127.0.0.1:18081` (or another free port). |
| `--listen` rejected as non-loopback | A non-loopback address was given without the required flag. | Add `--allow-non-loopback`. |
| Symptom | Cause | Fix |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| "unknown domain" or `404` from upstream | The upstream service does not accept `Host: <FROM>` (the default). A domain can be active on only one Fastly service at a time, so you cannot add the production hostname to a dev service. | Use a Trusted Server Compute upstream (routes by SNI, not `Host`), or pass `--rewrite-host` to send `Host: <TO>`. |
| Upstream returns `401` | Upstream is behind Basic auth. | Pass `--basic-auth user:pass` or `--basic-auth-file ./creds.txt`. |
| Upstream unreachable (`502` / `503`) | Upstream service is down or the domain is not provisioned. | Verify the upstream URL and its Fastly service health. |
| Browser shows an untrusted-certificate warning | The dev CA is not trusted in the browser. | Run `ts dev proxy ca install` for Chrome and Safari. For Firefox, use `--launch firefox` (auto-imports when `certutil` is installed — `brew install nss`) or run `certutil` manually (see above). After `ca regenerate`, re-trust with `ca install`. |
| Listen address already in use | Another process holds port 18080. | Pass `--listen 127.0.0.1:18081` (or another free port). |
| `--listen` rejected as non-loopback | A non-loopback address was given without the required flag. | Add `--allow-non-loopback`. |
Loading