|
| 1 | +--- |
| 2 | +id: index |
| 3 | +title: Temporal Proxy |
| 4 | +sidebar_label: Temporal Proxy |
| 5 | +description: |
| 6 | + Route requests between Temporal SDK clients and Workers and one or more upstream Temporal Services with the Temporal |
| 7 | + Proxy, a gRPC proxy that handles namespace translation, TLS, and authentication. |
| 8 | +slug: /production-deployment/temporal-proxy |
| 9 | +toc_max_heading_level: 4 |
| 10 | +keywords: |
| 11 | + - guide-context |
| 12 | + - how-to |
| 13 | + - production-readiness |
| 14 | +tags: |
| 15 | + - Temporal Service |
| 16 | + - Self-hosting |
| 17 | +--- |
| 18 | + |
| 19 | +:::caution Pre-release |
| 20 | + |
| 21 | +Temporal Proxy is under active development and evolving quickly. It is not ready for production use. Behavior and |
| 22 | +configuration can change between releases. See the |
| 23 | +[temporal-proxy repository](https://github.com/temporalio/temporal-proxy) for the current status and the definitive |
| 24 | +configuration schema. |
| 25 | + |
| 26 | +::: |
| 27 | + |
| 28 | +The Temporal Proxy is a gRPC proxy that sits between your Temporal SDK Clients, Workers, and the Temporal Web UI on one |
| 29 | +side and one or more upstream Temporal Services on the other. It handles Namespace translation and TLS termination so |
| 30 | +your applications can target a single local endpoint while the proxy routes each request to the right upstream, whether |
| 31 | +that is a local development Service, a self-hosted Service, or Temporal Cloud. |
| 32 | + |
| 33 | +## Why use it |
| 34 | + |
| 35 | +Without the proxy, connection details leak into your application code. Every Worker and Client has to know the |
| 36 | +upstream's host, TLS material, credentials, and the exact Namespace name the upstream expects. That couples your code to |
| 37 | +an environment: moving between a local Service, a self-hosted deployment, and Temporal Cloud becomes a code change. |
| 38 | + |
| 39 | +The proxy owns that concern instead. Workers talk plaintext to a single local endpoint using a short Namespace name, and |
| 40 | +the proxy adds TLS, credentials, and Namespace translation on the way out. Point a Worker at a different Namespace and |
| 41 | +it reaches a different upstream, with no change to the Worker. |
| 42 | + |
| 43 | +## How it works |
| 44 | + |
| 45 | +The proxy is built from a gateway and one proxy per upstream, connected by unix sockets: |
| 46 | + |
| 47 | +- The **gateway** is the single inbound endpoint that every Worker, SDK Client, and the Web UI connects to. |
| 48 | +- Each **upstream** has its own proxy that handles communication with that destination. |
| 49 | + |
| 50 | +```mermaid |
| 51 | +flowchart LR |
| 52 | + Worker[Worker] |
| 53 | + Client[SDK Client] |
| 54 | + UI[Web UI] |
| 55 | +
|
| 56 | + subgraph Proxy[Temporal Proxy] |
| 57 | + direction LR |
| 58 | + Gateway[Gateway] |
| 59 | + ProxyA[Per-upstream proxy A] |
| 60 | + ProxyB[Per-upstream proxy B] |
| 61 | + Gateway -->|unix socket| ProxyA |
| 62 | + Gateway -->|unix socket| ProxyB |
| 63 | + end |
| 64 | +
|
| 65 | + Cloud[Temporal Cloud] |
| 66 | + SelfHosted[Self-hosted Temporal Service] |
| 67 | +
|
| 68 | + Worker --> Gateway |
| 69 | + Client --> Gateway |
| 70 | + UI --> Gateway |
| 71 | + ProxyA --> Cloud |
| 72 | + ProxyB --> SelfHosted |
| 73 | +``` |
| 74 | + |
| 75 | +For each request, the gateway: |
| 76 | + |
| 77 | +1. peeks the target Namespace without parsing the payload; it is codec-transparent and relays raw frames in both |
| 78 | + directions. |
| 79 | +2. picks an upstream: the first matching routing rule, otherwise the system upstream for Namespace-less calls, otherwise |
| 80 | + the default. |
| 81 | +3. hands the request to that upstream's proxy over a unix socket. |
| 82 | + |
| 83 | +The per-upstream proxy then rewrites the local Namespace to the name the upstream expects, attaches that upstream's TLS |
| 84 | +and credentials, forwards to the Temporal Service, and translates the Namespace back on responses. |
| 85 | + |
| 86 | +### Terms |
| 87 | + |
| 88 | +| Term | Meaning | |
| 89 | +| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
| 90 | +| gateway | The single inbound gRPC endpoint that every SDK Client, Worker, and the Web UI connects to. It routes each request to an upstream by Namespace and request metadata, and never parses payloads. | |
| 91 | +| upstream | A configured destination the proxy forwards to: a Temporal Service (local dev, self-hosted, or Temporal Cloud), or another Temporal Proxy. | |
| 92 | +| system upstream | The upstream that handles Namespace-less requests, such as the SDK's `GetSystemInfo` call on connect. | |
| 93 | +| Temporal Service | A Temporal frontend the proxy connects to. | |
| 94 | + |
| 95 | +## Prerequisites |
| 96 | + |
| 97 | +- One or more upstream Temporal Services to route to, such as a local development Service, a self-hosted Service, or |
| 98 | + Temporal Cloud. |
| 99 | +- The `hostPort` address for each upstream. |
| 100 | +- Any credentials the upstreams require, such as a Temporal Cloud API key or mTLS certificates. |
| 101 | +- Go installed, if you build the proxy from source. The container image and Helm chart do not require a local Go |
| 102 | + toolchain. |
| 103 | + |
| 104 | +## Install the proxy |
| 105 | + |
| 106 | +Install the `proxy` binary with Go: |
| 107 | + |
| 108 | +```bash |
| 109 | +go install github.com/temporalio/temporal-proxy/cmd/proxy@latest |
| 110 | +``` |
| 111 | + |
| 112 | +Pin an explicit version instead of `@latest`: |
| 113 | + |
| 114 | +```bash |
| 115 | +go install github.com/temporalio/temporal-proxy/cmd/proxy@v0.1.0 |
| 116 | +``` |
| 117 | + |
| 118 | +Pull the container image: |
| 119 | + |
| 120 | +```bash |
| 121 | +docker pull temporalio/temporal-proxy:latest |
| 122 | +``` |
| 123 | + |
| 124 | +Install with Helm from the Temporal Helm repo: |
| 125 | + |
| 126 | +```bash |
| 127 | +helm install temporal-proxy temporal-proxy \ |
| 128 | + --repo https://go.temporal.io/helm-charts |
| 129 | +``` |
| 130 | + |
| 131 | +Supply the proxy configuration under the `config` key of a Helm values file. The chart renders it into a ConfigMap |
| 132 | +mounted at `/etc/temporal-proxy/config.yaml`. |
| 133 | + |
| 134 | +Run the proxy with a configuration file passed through the `-c` (or `--config`) flag: |
| 135 | + |
| 136 | +```bash |
| 137 | +proxy serve -c config.yaml |
| 138 | +``` |
| 139 | + |
| 140 | +## Configure the proxy |
| 141 | + |
| 142 | +The proxy reads a single YAML file with three top-level sections: the gateway listener (`hostPort`), `routing`, and the |
| 143 | +`upstreams` it forwards to. Values support `${VAR}` and `$VAR` environment variable expansion, and an upstream's |
| 144 | +`hostPort` can be a template that resolves per request (for example `{{ .RemoteNamespace }}`). |
| 145 | + |
| 146 | +The example below is the proxy's |
| 147 | +[Temporal Cloud example](https://github.com/temporalio/temporal-proxy/tree/main/examples/cloud), which connects a Worker |
| 148 | +to Temporal Cloud with an API key. The Worker carries no Cloud configuration: it talks plaintext to `127.0.0.1:7233`, |
| 149 | +and the proxy adds TLS, the API key, and the Namespace rewrite on the way out. |
| 150 | + |
| 151 | +```yaml |
| 152 | +# Gateway: the local endpoint your Workers and Clients connect to (plaintext). |
| 153 | +hostPort: 127.0.0.1:7233 |
| 154 | + |
| 155 | +routing: |
| 156 | + default: cloud # Namespaced requests. |
| 157 | + system: system # Namespace-less requests (for example GetSystemInfo on connect). |
| 158 | + |
| 159 | +upstreams: |
| 160 | + # Namespaced traffic. The host is derived per request from the translated |
| 161 | + # Namespace, so one entry serves any number of Namespaces. |
| 162 | + - name: cloud |
| 163 | + hostPort: '{{ .RemoteNamespace }}.tmprl.cloud:7233' |
| 164 | + tls: {} # Enable outbound TLS with defaults. |
| 165 | + namespaces: |
| 166 | + rules: |
| 167 | + suffix: .$TEMPORAL_ACCOUNT # quickstart becomes quickstart.<account> |
| 168 | + credentials: |
| 169 | + static: |
| 170 | + apiKey: $TEMPORAL_API_KEY |
| 171 | + |
| 172 | + # Namespace-less calls have no Namespace to derive a host from, so they use a |
| 173 | + # fixed endpoint. Any Namespace endpoint in the account answers them. |
| 174 | + - name: system |
| 175 | + hostPort: ${TEMPORAL_NAMESPACE}.${TEMPORAL_ACCOUNT}.tmprl.cloud:7233 |
| 176 | + tls: {} |
| 177 | + credentials: |
| 178 | + static: |
| 179 | + apiKey: ${TEMPORAL_API_KEY} |
| 180 | +``` |
| 181 | +
|
| 182 | +The [chart `values.yaml`](https://github.com/temporalio/helm-charts/tree/main/charts/temporal-proxy) and the |
| 183 | +[temporal-proxy repository](https://github.com/temporalio/temporal-proxy) hold the complete, current set of options. |
| 184 | + |
| 185 | +### Route requests |
| 186 | + |
| 187 | +The `routing` section selects an upstream for each request: |
| 188 | + |
| 189 | +- `default` is the fallback when no rule matches. It is optional; omit it to reject unmatched requests with an error. |
| 190 | +- `system` is the upstream for Namespace-less requests, such as the SDK's `GetSystemInfo` and `GetClusterInfo` calls. It |
| 191 | + is optional; when unset, those requests fall back to `default`. |
| 192 | +- `rules` is an ordered list, evaluated top to bottom. The first match wins. |
| 193 | + |
| 194 | +Every upstream named by `default`, `system`, or a rule must exist in `upstreams`. |
| 195 | + |
| 196 | +```yaml |
| 197 | +routing: |
| 198 | + default: local # Fallback when no rule matches. |
| 199 | + system: cloud # Namespace-less requests. |
| 200 | + rules: |
| 201 | + - match: |
| 202 | + namespace: 'prod-*' |
| 203 | + metadata: |
| 204 | + x-tier: gold |
| 205 | + upstream: cloud |
| 206 | + - match: |
| 207 | + namespace: '*-test' |
| 208 | + upstream: local |
| 209 | +``` |
| 210 | + |
| 211 | +A rule matches when its Namespace matches and every metadata condition matches (AND logic). A `match` must set at least |
| 212 | +one of `namespace` or `metadata`; an empty match is a configuration error, since that is what `default` is for. Routing |
| 213 | +runs on the local Namespace, before translation. |
| 214 | + |
| 215 | +`namespace` is a string literal or a simple glob with a single leading or trailing `*`: |
| 216 | + |
| 217 | +| Pattern | Matches | |
| 218 | +| ---------- | --------------------------- | |
| 219 | +| `payments` | exactly `payments` | |
| 220 | +| `prod-*` | names starting with `prod-` | |
| 221 | +| `*-test` | names ending with `-test` | |
| 222 | +| `*-test-*` | names containing `-test-` | |
| 223 | +| `*` | any Namespace | |
| 224 | + |
| 225 | +A `*` in any other position, such as `a*b`, is invalid. |
| 226 | + |
| 227 | +`metadata` matches gRPC request metadata (headers). Keys are case-insensitive and do not support wildcards; values use |
| 228 | +the same glob syntax as `namespace`. A key matches when any of the request's values for it match. |
| 229 | + |
| 230 | +### Translate Namespaces |
| 231 | + |
| 232 | +Applications connected to the proxy use short, local Namespace names. Each upstream rewrites those names to the ones its |
| 233 | +Temporal Service expects, under `namespaces.rules`. The rewrite applies to requests and is reversed on responses, so |
| 234 | +callers only ever see the local name. |
| 235 | + |
| 236 | +```yaml |
| 237 | +upstreams: |
| 238 | + - name: cloud |
| 239 | + hostPort: '{{ .RemoteNamespace }}.tmprl.cloud:7233' |
| 240 | + tls: {} |
| 241 | + namespaces: |
| 242 | + rules: |
| 243 | + prefix: '' # Optional string prepended to the local name. |
| 244 | + suffix: .acct # payments becomes payments.acct |
| 245 | + overrides: # Explicit pairs that bypass prefix and suffix. |
| 246 | + - local: billing |
| 247 | + remote: payments.acct |
| 248 | +``` |
| 249 | + |
| 250 | +- `prefix` and `suffix` wrap every local Namespace: the remote name is `prefix + local + suffix`, and responses are |
| 251 | + unwrapped back to the local name. |
| 252 | +- `overrides` lists explicit `local` and `remote` pairs for names that do not follow the prefix and suffix convention. |
| 253 | + An override takes precedence over the prefix and suffix rules. Each local name and each remote name may appear only |
| 254 | + once. |
| 255 | + |
| 256 | +An upstream's `hostPort` and `tls.serverName` can be Go templates resolved per request, so one upstream can serve many |
| 257 | +Namespaces. Available variables: |
| 258 | + |
| 259 | +- `{{ .LocalNamespace }}`: the Namespace before translation. |
| 260 | +- `{{ .RemoteNamespace }}`: the Namespace after translation. |
| 261 | +- `{{ .Metadata.<key> }}` or `{{ index .Metadata "<key>" }}`: a request metadata value. |
| 262 | + |
| 263 | +Upstreams with a static `hostPort` connect eagerly at startup; templated ones connect lazily on first use. |
| 264 | + |
| 265 | +### Authenticate inbound requests |
| 266 | + |
| 267 | +Inbound authentication runs on the gateway and is off by default: omit the top-level `auth` block to accept all |
| 268 | +requests. When present, `auth` must select exactly one authenticator, `staticToken` or `jwks`. The gateway validates the |
| 269 | +credential on each request and strips it before forwarding upstream. |
| 270 | + |
| 271 | +Compare an inbound bearer token against a fixed value with `staticToken`: |
| 272 | + |
| 273 | +```yaml |
| 274 | +auth: |
| 275 | + staticToken: |
| 276 | + token: ${GATEWAY_TOKEN} # Required. The expected token value. |
| 277 | + header: authorization # Header to read the token from. |
| 278 | + scheme: Bearer # Scheme prefix to strip before comparing. |
| 279 | +``` |
| 280 | + |
| 281 | +Or verify a JWT's signature and claims against a JWKS endpoint with `jwks`: |
| 282 | + |
| 283 | +```yaml |
| 284 | +auth: |
| 285 | + jwks: |
| 286 | + url: https://issuer.example.com/.well-known/jwks.json # Required. Absolute https URL. |
| 287 | + audiences: |
| 288 | + - temporal-proxy |
| 289 | + issuer: https://issuer.example.com/ |
| 290 | + header: authorization |
| 291 | + scheme: Bearer |
| 292 | +``` |
| 293 | + |
| 294 | +`token` (for `staticToken`) and `url` (for `jwks`) are required; the remaining fields are optional. Only `staticToken` |
| 295 | +or `jwks` may be set, not both. |
| 296 | + |
| 297 | +### Present credentials to upstreams |
| 298 | + |
| 299 | +Each upstream can present its own credential to the Temporal Service, set under `credentials`. `static` is the only |
| 300 | +variant today; it injects a fixed API key as a bearer header on every outbound request, which is how you connect to |
| 301 | +Temporal Cloud: |
| 302 | + |
| 303 | +```yaml |
| 304 | +upstreams: |
| 305 | + - name: cloud |
| 306 | + hostPort: my-ns.acct.tmprl.cloud:7233 |
| 307 | + tls: {} # Required whenever credentials are set. |
| 308 | + credentials: |
| 309 | + static: |
| 310 | + apiKey: ${TEMPORAL_API_KEY} # Required. |
| 311 | + header: authorization # Optional header override. |
| 312 | + scheme: Bearer # Optional scheme override. |
| 313 | +``` |
| 314 | + |
| 315 | +Credentials require TLS to the upstream. If you set `credentials` without a `tls` block, the configuration fails to |
| 316 | +load. |
| 317 | + |
| 318 | +### Configure TLS |
| 319 | + |
| 320 | +TLS is terminated in two independent places, both using the same keys: `ca`, `cert`, `key`, and `serverName`. |
| 321 | + |
| 322 | +**Inbound, on the gateway.** The top-level `tls` block secures connections from your applications. Set `cert` and `key` |
| 323 | +for server TLS, and add `ca` to enforce mutual TLS, which requires each client to present a certificate signed by that |
| 324 | +CA. Local development commonly omits `tls` and connects in plaintext. |
| 325 | + |
| 326 | +**Outbound, per upstream.** Each upstream's `tls` block secures the connection from its proxy to the Temporal Service: |
| 327 | + |
| 328 | +- `tls: {}` verifies the upstream against the system root certificate pool and presents no client certificate. This is |
| 329 | + what Temporal Cloud with an API key needs. |
| 330 | +- `ca` alone verifies the upstream against a private trust anchor, still presenting no client certificate. |
| 331 | +- `cert` and `key` together select mutual TLS and require `ca`. They must be set as a pair. |
| 332 | + |
| 333 | +Set `serverName` when the host you dial does not match the common name or SAN on the server's certificate. |
| 334 | + |
| 335 | +## Related |
| 336 | + |
| 337 | +- [Temporal Proxy repository](https://github.com/temporalio/temporal-proxy) |
| 338 | +- [Temporal Proxy Helm chart](https://github.com/temporalio/helm-charts/tree/main/charts/temporal-proxy) |
| 339 | +- [Temporal Cloud example](https://github.com/temporalio/temporal-proxy/tree/main/examples/cloud) |
| 340 | +- [Codecs and Encryption](/production-deployment/data-encryption) |
| 341 | +- [Self-hosted guide: Security](/self-hosted-guide/security) |
0 commit comments