diff --git a/docs/production-deployment/self-hosted-guide/security.mdx b/docs/production-deployment/self-hosted-guide/security.mdx index 3a13685c5b..a2bc4efe90 100644 --- a/docs/production-deployment/self-hosted-guide/security.mdx +++ b/docs/production-deployment/self-hosted-guide/security.mdx @@ -339,6 +339,94 @@ 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, err error) +} +``` + +`GetToken` is called on every outbound cross-cluster RPC. +`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. + +The Temporal Service does not cache the returned token. +Implementations are responsible for caching and refresh. +A provider that fetches tokens from an external service must cache internally, otherwise every outbound RPC triggers a network round-trip to that service. +OAuth 2.0 client libraries typically handle this caching, and a `TokenProvider` can often be a thin wrapper around one. + +#### 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. + +#### 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. + +#### 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 db8ad2228a..648d496240 100644 --- a/docs/references/configuration.mdx +++ b/docs/references/configuration.mdx @@ -210,6 +210,77 @@ 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. + +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 +``` + ## 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..b39c8e39d0 100644 --- a/docs/references/server-options.mdx +++ b/docs/references/server-options.mdx @@ -19,11 +19,11 @@ We recommend this approach for a limited number of situations. ```go s, err := temporal.NewServer() if err != nil { - log.Fatal(err) + log.Fatal(err) } err = s.Start() if err != nil{ - log.Fatal(err) + log.Fatal(err) } ``` @@ -40,7 +40,7 @@ structure, refer to the [official configuration documentation](https://pkg.go.de ```go s, err := temporal.NewServer( - temporal.WithConfig(cfg), + temporal.WithConfig(cfg), ) ``` @@ -50,7 +50,7 @@ Load a custom configuration from a file. ```go s, err := temporal.NewServer( - temporal.WithConfigLoader(configDir, env, zone), + temporal.WithConfigLoader(configDir, env, zone), ) ``` @@ -61,7 +61,7 @@ The default can be used from the `go.temporal.io/server/temporal` package. ```go s, err := temporal.NewServer( - temporal.ForServices(temporal.Services), + temporal.ForServices(temporal.Services), ) ``` @@ -76,7 +76,7 @@ This option provides a channel that interrupts the server on the signal from tha ```go s, err := temporal.NewServer( - temporal.InterruptOn(temporal.InterruptCh()), + temporal.InterruptOn(temporal.InterruptCh()), ) ``` @@ -86,7 +86,7 @@ Sets a low level [authorization mechanism](/self-hosted-guide/security#authorize ```go s, err := temporal.NewServer( - temporal.WithAuthorizer(myAuthorizer), + temporal.WithAuthorizer(myAuthorizer), ) ``` @@ -97,7 +97,7 @@ Overrides the default TLS configuration provider. ```go s, err := temporal.NewServer( - temporal.WithTLSConfigFactory(yourTLSConfigProvider), + temporal.WithTLSConfigFactory(yourTLSConfigProvider), ) ``` @@ -108,12 +108,23 @@ Configures a [mechanism to map roles](/self-hosted-guide/security#claim-mapper) ```go s, err := temporal.NewServer( temporal.WithClaimMapper(func(cfg *config.Config) authorization.ClaimMapper { - logger := getYourLogger() // Replace with how you retrieve or initialize your logger - return authorization.NewDefaultJWTClaimMapper( - authorization.NewDefaultTokenKeyProvider(cfg, logger), - cfg - ) - }), + logger := getYourLogger() // Replace with how you retrieve or initialize your logger + return authorization.NewDefaultJWTClaimMapper( + authorization.NewDefaultTokenKeyProvider(cfg, logger), + cfg + ) + }), +) +``` + +### 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), ) ``` @@ -123,7 +134,7 @@ Sets a custom tally metric reporter. ```go s, err := temporal.NewServer( - temporal.WithCustomMetricsReporter(myReporter), + temporal.WithCustomMetricsReporter(myReporter), ) ``` diff --git a/vale/styles/Temporal/WordList.yml b/vale/styles/Temporal/WordList.yml index 55222d5c1c..3e504b4574 100644 --- a/vale/styles/Temporal/WordList.yml +++ b/vale/styles/Temporal/WordList.yml @@ -13,7 +13,7 @@ swap: "(?:e-mail|Email|E-mail)": email "(?:file ?path|path ?name)": path "(?:kill|terminate|abort)": stop|exit|cancel|end - "(?:OAuth ?2|Oauth)": OAuth 2.0 + '(?:OAuth ?2(?!\.\d)|Oauth)': OAuth 2.0 "(?:ok|Okay)": OK|okay "(?:WiFi|wifi)": Wi-Fi '[\.]+apk': APK