diff --git a/docs/production-deployment/self-hosted-guide/security.mdx b/docs/production-deployment/self-hosted-guide/security.mdx index 47f7f572a3..7c701d1a73 100644 --- a/docs/production-deployment/self-hosted-guide/security.mdx +++ b/docs/production-deployment/self-hosted-guide/security.mdx @@ -339,6 +339,105 @@ Related read: - [How to secure a Temporal Service](/security) +### What is a TokenProvider Plugin? {#token-provider} + +The Token Provider component is a pluggable component that attaches an authentication token to outbound cross-cluster RPCs. +Where the `ClaimMapper` and `Authorizer` plugins protect the receiving end of a request, `TokenProvider` protects the sending end. +When a Temporal Service replicates Workflows, Schedules, or Namespaces to a peer cluster, `TokenProvider` supplies the bearer token that the peer's `ClaimMapper` validates. + +A typical approach pairs a JWT-emitting `TokenProvider` on the sender with the [default JWT `ClaimMapper`](#default-jwt-claimmapper) on the receiver. + +`TokenProvider` is a single-method interface: + +```go +type TokenProvider interface { + GetToken(ctx context.Context, rpcAddress string) (token string, expiresAt time.Time, err error) +} +``` + +`GetToken` is called for each outbound cross-cluster connection. + +- `rpcAddress` is the receiver cluster's `host:port`. Providers commonly use this value as the JWT's `aud` claim to scope each token to a specific receiver. +- `expiresAt` lets the credential cache rotate tokens before they expire. Return `time.Time{}` if the token never expires. + +#### Required claims on the receiver + +Cross-cluster RPCs target the receiver's `AdminService`, which the default `Authorizer` only admits with `Claims{System: RoleAdmin}`. +Tokens carrying lower roles are rejected. + +With the default JWT `ClaimMapper`, the token must carry a `permissions` claim with the entry `temporal-system:admin`. + +``` +{ + "permissions":[ + "temporal-system:admin" + ] +} +``` + +A custom `ClaimMapper` can recognize any JWT shape, such as an OAuth-style `scp` scope or a custom claim, and translate it into the same `Claims{System: RoleAdmin}` result. + +#### Stream lifecycle + +Cross-cluster replication runs over long-lived gRPC streams. +The token is attached when a stream is opened, and the receiver's `ClaimMapper` and `Authorizer` evaluate it at that point. +Already-open streams are not re-checked when the token later expires. +Rotation and revocation therefore only take effect when a stream is reestablished, for example after a reconnect or a process restart. + +#### TokenCredentials caching + +Tokens returned by `GetToken` are cached per outbound connection by `auth.TokenCredentials`. +The credential refreshes proactively within a configurable grace window before expiry. +If a fetch fails before the token is hard-expired, the credential falls back to the last cached token. + +Configure the refresh behavior through `global.authorization.remoteClusterAuth.graceWindow`, a Go duration string such as `30s` or `5m`. +The default is `30s`. + +#### Transport security + +`auth.TokenCredentials` requires transport security, so the gRPC runtime refuses to attach the credential to a plaintext dial. +Configuring `WithTokenProvider` therefore requires also configuring TLS for the destination, either through [`global.tls.remoteClusters`](/references/configuration) in the YAML config or by passing a custom provider through [`temporal.WithTLSConfigFactory`](/references/server-options#withtlsconfigfactory). + +:::note + +If `WithTokenProvider` is set but no remote-cluster TLS source is configured, the Temporal Service fails to boot with a directed error message. +This catches the misconfiguration at startup rather than on the first cross-cluster RPC. + +::: + +#### Fail-closed mode + +Set `global.authorization.remoteClusterAuth.require: true` to require a non-empty token on every outbound remote-cluster RPC. +When `require` is `true`, the Temporal Service refuses to start if no `TokenProvider` is configured, and the credential returns `Unauthenticated` rather than sending an empty `authorization` header. + +#### JWT helpers + +For providers that emit JWTs, `auth.ParseJWTExpiry(token string) time.Time` extracts the `exp` claim without verifying the signature. +This lets a `TokenProvider` return the JWT's own expiry as the `expiresAt` so cache rotation tracks the token's real lifetime. + +#### Configuration + +Configure your `TokenProvider` with the [`temporal.WithTokenProvider`](/references/server-options#withtokenprovider) server option. +Pair it with the receiver-side plugins so peers validate what the sender attaches. + +```go +temporalServer, err := temporal.NewServer( + temporal.WithTokenProvider(myTokenProvider), + temporal.WithAuthorizer(authorization.NewDefaultAuthorizer()), + temporal.WithClaimMapper(func(cfg *config.Config) authorization.ClaimMapper { + logger := getYourLogger() + return authorization.NewDefaultJWTClaimMapper( + authorization.NewDefaultTokenKeyProvider(cfg, logger), + cfg, + logger, + ) + }), +) +``` + +When `TokenProvider` is not configured, outbound cross-cluster RPCs carry no `authorization` header. +This is the default for deployments that don't replicate to peers, or that rely on transport-level (mTLS) authentication alone. + ## Data Converter {#data-converter} Each Temporal SDK provides a [Data Converter](/dataconversion) that can be customized with a custom [Payload Codec](/payload-codec) to encode and secure your data. diff --git a/docs/references/configuration.mdx b/docs/references/configuration.mdx index 4614a891d8..95cfec0eee 100644 --- a/docs/references/configuration.mdx +++ b/docs/references/configuration.mdx @@ -210,6 +210,81 @@ global: - `internode.client.rootCaFiles` - `frontend.server.clientCaFiles` +### authorization + +The `authorization` section configures the Temporal Service's authentication. +It selects the pluggable components that validate incoming gRPC tokens, the signing keys to trust, and the credentials this server attaches to outbound cross-cluster replication RPCs. + +It contains two structural subsections that mirror the direction of traffic: + +- [`jwtKeyProvider`](#jwtkeyprovider): Supplies the signing keys used to verify inbound JWTs. +- [`remoteClusterAuth`](#remoteclusterauth): Controls the bearer tokens attached to outbound cross-cluster RPCs. + +The top-level fields select and tune the inbound plugins: + +- `authorizer` - _string_ - _Default:_ `""`. + Selects the inbound authorizer. Empty string disables authorization (the no-op authorizer permits every request); `default` enables Temporal's built-in role-based authorizer. The value is case-insensitive. +- `claimMapper` - _string_ - _Default:_ `""`. + Selects the `ClaimMapper` that extracts roles from a verified token. Empty string disables claim mapping; `default` enables the built-in JWT `ClaimMapper`. +- `audience` - _string_ - _Default:_ `""`. + Required `aud` claim value that inbound JWTs must contain. When empty, audience validation is skipped. +- `authHeaderName` - _string_ - _Default:_ `authorization`. + gRPC metadata header from which the `ClaimMapper` reads the bearer token. + +See [How to secure a Temporal Service](/self-hosted-guide/security) for the conceptual model behind `ClaimMapper`, `Authorizer`, and `TokenProvider` and how the plugins fit together. + +A minimal example that enables JWT-based inbound auth using Temporal's defaults: + +```yaml +global: + authorization: + jwtKeyProvider: + keySourceURIs: + - https://idp.example.com/.well-known/jwks.json + refreshInterval: 1m + authorizer: default + claimMapper: default + audience: temporal-frontend +``` + +#### jwtKeyProvider + +Configures the source of signing keys used by the default `ClaimMapper` to verify inbound JWTs. + +- `keySourceURIs` - _list of strings_. + URLs to fetch JWKS-formatted public keys from. The default `ClaimMapper` fetches and caches the union of keys returned by each URI. +- `refreshInterval` - _Go duration string_ (for example `1m`, `5m`) - _Default:_ `0`. + How often the key set is refetched. Zero (the default) disables periodic refresh, so keys are loaded once at startup and never rotated. + +#### remoteClusterAuth + +Controls outbound bearer tokens carried on cross-cluster RPCs by a [`TokenProvider`](/self-hosted-guide/security#token-provider). +This block has no effect unless a `TokenProvider` is also configured via [`temporal.WithTokenProvider`](/references/server-options#withtokenprovider). + +- `require` - _boolean_ - _Default:_ `false`. + When `true`, every outbound cross-cluster RPC must carry a non-empty token; the credential returns `Unauthenticated` rather than sending an empty `authorization` header. + The Temporal Service refuses to start when `require` is `true` but no `TokenProvider` is configured. +- `graceWindow` - _Go duration string_ (for example `30s`, `5m`) - _Default:_ `30s`. + How long before a token's `expiresAt` the credential cache refreshes proactively. + If a refresh fails, the credential falls back to the last cached token until its hard expiry. + +A combined example pairing inbound JWT validation with outbound replication-stream auth on a sender cluster: + +```yaml +global: + authorization: + jwtKeyProvider: + keySourceURIs: + - https://idp.example.com/.well-known/jwks.json + refreshInterval: 1m + authorizer: default + claimMapper: default + audience: temporal-frontend + remoteClusterAuth: + require: true + graceWindow: 30s +``` + ## persistence The `persistence` section holds configuration for the data store/persistence layer. diff --git a/docs/references/server-options.mdx b/docs/references/server-options.mdx index 0521a0ecd7..20d62131d7 100644 --- a/docs/references/server-options.mdx +++ b/docs/references/server-options.mdx @@ -117,6 +117,17 @@ s, err := temporal.NewServer( ) ``` +### WithTokenProvider + +Configures a [`TokenProvider`](/self-hosted-guide/security#token-provider) that supplies bearer tokens for outbound cross-cluster RPCs. +`TokenProvider` is defined in the `go.temporal.io/server/common/rpc/auth` package. + +```go +s, err := temporal.NewServer( + temporal.WithTokenProvider(myTokenProvider), +) +``` + ### WithCustomMetricsReporter Sets a custom tally metric reporter.