Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/temporal-sa/temporal-cloud-proxy/llms.txt

Use this file to discover all available pages before exploring further.

Temporal Cloud Proxy uses Uber Fx for dependency injection. At startup, cmd/main.go constructs an fx.App that wires together six modules — config, auth, codec, metrics, proxy, and transport — into a running gRPC service. Understanding this structure helps when extending the proxy with new authentication providers or KMS backends, or when debugging unexpected startup failures.

Module Overview

Each module is declared as an fx.Provide call inside a package-level Module variable. Fx resolves the dependency graph automatically and starts each provider in dependency order.

config

Reads the YAML configuration file (path supplied via the --config CLI flag), validates the result, and exposes a ConfigProvider interface. Every other module depends on ConfigProvider to read server port, workload definitions, and encryption caching parameters.

auth

Constructs an AuthenticatorFactory during startup. The factory holds a providers map keyed on authenticator type string ("jwt", "spiffe"). When the proxy module calls NewAuthenticator(AuthConfig), the factory dispatches to the correct constructor and initialises the authenticator — including connecting to the SPIRE workload API socket for SpiffeAuthenticator.

codec

Constructs an EncryptionCodecFactory that holds a providers map keyed on encryption type ("aws-kms", "gcp-kms"). Per-workload codecs are created lazily by the proxy module via NewEncryptionCodec(EncryptionCodecOptions). Each codec wraps a Cipher which in turn wraps a CachingMaterialsManager wrapping the underlying KMS provider.

metrics

Initialises a Prometheus registry backed by OpenTelemetry, registers all metric instruments (counters, histograms), and starts an HTTP server on the configured metrics port (default 9090) that serves the /metrics scrape endpoint. The MetricsProvider is invoked by cmd/main.go to ensure the server starts even if no other module directly depends on it.

proxy

Implements ProxyProvider, which exposes a grpc.ClientConnInterface. During newProxyProvider, one namespaceConnection is created per workload: a grpc.ClientConn to the Temporal Cloud endpoint (with TLS), an optional payload encryption interceptor, and an optional API key or mTLS namespace auth interceptor. All connections are stored in a connectionMux map keyed by workload_id and protected by a sync.RWMutex.

transport

Creates a grpc.Server, registers the WorkflowService handler using the Temporal SDK’s client.NewWorkflowServiceProxyServer, and listens on the configured host and port. The handler is backed by a workflowservice.WorkflowServiceClient that wraps the ProxyProvider’s ClientConnInterface, so every inbound RPC is forwarded through the proxy routing logic.

Crypto Layer

The encryption subsystem is structured in layers to separate concerns — KMS I/O, caching, and AES-GCM cipher operations — while keeping each layer independently testable.
Codec (converter.PayloadCodec)
  └── Cipher
        └── CachingMaterialsManager
              └── MaterialsManager  (AWSKMSProvider | GCPKMSProvider)

MaterialsManager Interface

type MaterialsManager interface {
    GetMaterial(ctx context.Context, cryptoCtx CryptoContext) (*Material, error)
    DecryptMaterial(ctx context.Context, cryptoCtx CryptoContext, material *Material) (*Material, error)
}
  • GetMaterial — generates a new data key (used during encryption). The KMS provider calls GenerateDataKey (AWS) or GenerateRandomBytes + Encrypt (GCP) and returns both the plaintext key and the KMS-encrypted key blob.
  • DecryptMaterial — recovers a plaintext key from the encrypted key blob stored in payload metadata (used during decryption). The KMS provider calls Decrypt and returns the recovered plaintext key.
CryptoContext is a map[string]string that carries purpose, encryption key ID, and namespace — it is passed directly to AWS KMS as the encryption context and to GCP KMS as additional authenticated data (AAD), binding the key to the specific operation context.

CachingMaterialsManager

CachingMaterialsManager wraps either KMS provider with an LRU cache (backed by github.com/hashicorp/golang-lru) to avoid a KMS call on every payload. Cache entries are validated against three independent expiry conditions:
  • MaxAge — wall-clock age of the cached Material since CreatedAt
  • MaxMessagesUsed — number of times the cached key has been used (incremented atomically inside a sync.RWMutex)
  • MaxCache — maximum number of entries in the LRU; the LRU itself evicts the least-recently-used entry when the limit is reached
Encryption and decryption use separate cache keys: encryption keys are keyed on a SHA-256 hash of the CryptoContext; decryption keys are keyed on a hash of both the CryptoContext and the encrypted key blob itself.

Cipher

Cipher holds a MaterialsManager and provides two methods:
  • Encrypt(ctx, EncryptInput) (ciphertext, encryptedKey, error) — fetches a material via GetMaterial, generates a random 12-byte nonce, and encrypts the plaintext with AES-GCM using the PayloadContext as additional authenticated data (AAD). Returns the nonce || ciphertext byte slice and the KMS-encrypted key blob.
  • Decrypt(ctx, DecryptInput) (plaintext, error) — calls DecryptMaterial to recover the plaintext key, then opens the AES-GCM sealed message using the PayloadContext as AAD.

Codec

Codec (implements converter.PayloadCodec) wraps the Cipher and is the integration point with the Temporal SDK:
  • Encode — serialises each *commonpb.Payload, calls Cipher.Encrypt, and wraps the result in a new payload with metadata fields encoding: binary/encrypted, encryption-key-id, and encrypted-data-key.
  • Decode — skips payloads whose encoding is not binary/encrypted, extracts encryption-key-id and encrypted-data-key from metadata, calls Cipher.Decrypt, and unmarshals the recovered bytes back into a *commonpb.Payload.

Authentication Layer

Worker-to-proxy authentication is handled by a two-level abstraction: a factory that selects the authenticator type, and an Authenticator interface that the proxy calls on every request.

Authenticator Interface

type Authenticator interface {
    Type() string
    Init(ctx context.Context, config map[string]interface{}) error
    Authenticate(ctx context.Context, credentials interface{}) (*AuthenticationResult, error)
    Close() error
}

JwtAuthenticator

Initialised with a jwks-url and a list of expected audiences. On Init, it fetches the JWKS from the URL and starts a background goroutine (via keyfunc) that refreshes the key set every 5 minutes. On Authenticate, it:
  1. Strips the Bearer prefix from the authorization header value
  2. Parses and validates the JWT signature using the live JWKS
  3. Checks that at least one audience in the token’s aud claim matches a configured audience
  4. Verifies the exp claim has not elapsed
  5. Returns an AuthenticationResult with Authenticated: true, the sub claim as Subject, and the full claims map

SpiffeAuthenticator

Initialised with a list of allowed spiffe_ids, expected audiences, and a SPIRE agent endpoint (typically a Unix socket path). On Init, it connects to the SPIRE workload API using workloadapi.NewJWTSource — this call blocks until the agent is reachable. On Authenticate, it:
  1. Strips the Bearer prefix
  2. Calls jwtsvid.ParseAndValidate against the live JWTSource (which handles SVID signature verification and audience matching)
  3. Checks that the validated SVID’s SPIFFE ID appears in the configured spiffe_ids allow-list
  4. Returns an AuthenticationResult with Authenticated: true and the SPIFFE ID as Subject
Close releases the JWTSource gRPC connection to the SPIRE agent.

Request Flow

The following describes the path of a single unary RPC from a Temporal worker to Temporal Cloud:
Worker
  │  gRPC call (unary) with metadata:
  │    workload-id: <workload_id>
  │    authorization: Bearer <token>


[transport / grpc.Server]
  │  WorkflowServiceProxyServer forwards call to:

[proxy / ProxyProvider.Invoke]
  │  1. Read metadata from incoming context
  │  2. Extract workload-id header
  │  3. RLock → connectionMux[workload_id] lookup → RUnlock
  │  4. If authenticator configured:
  │       Authenticator.Authenticate(ctx, authorization header)
  │       → return codes.Unauthenticated on failure
  │  5. Forward via namespaceConnection.Invoke(ctx, method, req, reply)


[namespaceConnection grpc.ClientConn with interceptor chain]
  │  Interceptor 1 (if encryption enabled):
  │    PayloadCodec.Encode(outbound payloads)
  │      → Cipher.Encrypt → CachingMaterialsManager → KMS
  │  Interceptor 2 (if API key auth):
  │    Strip inbound authorization header
  │    Append temporal-namespace + authorization: Bearer <api_key>


Temporal Cloud gRPC endpoint (TLS)


[response travels back through interceptor chain]
  │  Interceptor 1:
  │    PayloadCodec.Decode(inbound payloads)
  │      → Cipher.Decrypt → CachingMaterialsManager → KMS


Worker receives decrypted response

Extension Points

Adding a New Authenticator

1

Implement the Authenticator interface

Create a new file in the auth package. Implement all four methods: Type() string, Init(ctx, config), Authenticate(ctx, credentials), and Close().
type MyAuthenticator struct { /* fields */ }

func (m *MyAuthenticator) Type() string { return "my-auth" }
func (m *MyAuthenticator) Init(ctx context.Context, config map[string]interface{}) error { /* ... */ }
func (m *MyAuthenticator) Authenticate(ctx context.Context, credentials interface{}) (*AuthenticationResult, error) { /* ... */ }
func (m *MyAuthenticator) Close() error { /* ... */ }
2

Register in the factory

Add a new entry to the providers map inside newAuthenticatorFactoryProvider in auth/authenticator.go:
af.providers["my-auth"] = func(config map[string]interface{}) (Authenticator, error) {
    authenticator := &MyAuthenticator{}
    err := authenticator.Init(ctx, config)
    return authenticator, err
}
3

Configure a workload

Reference the new type in the workload’s authentication block in config.yaml:
authentication:
  type: "my-auth"
  config:
    my-param: "value"

Adding a New KMS Provider

1

Implement the MaterialsManager interface

Create a new file in the crypto package. Implement GetMaterial (generate + encrypt a data key) and DecryptMaterial (decrypt the stored key blob):
type MyKMSProvider struct { /* kmsClient, keyID, etc. */ }

func (m *MyKMSProvider) GetMaterial(ctx context.Context, cryptoCtx CryptoContext) (*Material, error) { /* ... */ }
func (m *MyKMSProvider) DecryptMaterial(ctx context.Context, cryptoCtx CryptoContext, material *Material) (*Material, error) { /* ... */ }
2

Register in the codec factory

Add a new entry to the providers map inside newCodecFactoryProvider in codec/codec.go:
cf.providers["my-kms"] = func(args EncryptionCodecOptions) (converter.PayloadCodec, error) {
    keyID := args.LocalEncryptionConfig.Config["key-id"].(string)
    myProvider := crypto.NewMyKMSProvider(keyID)
    return NewEncryptionCodecWithCaching(
        myProvider,
        args.CodecContext,
        keyID,
        args.MetricsHandler,
        cf.cachingConfig,
    ), nil
}
3

Configure a workload

Reference the new type in the workload’s encryption block in config.yaml:
encryption:
  type: "my-kms"
  config:
    key-id: "my-key-identifier"
The proxy only supports unary gRPC calls. The NewStream method on ProxyProvider returns codes.Unimplemented unconditionally. Because the Temporal workflow service API is entirely unary, this is not a practical limitation for any standard Temporal workload.

Build docs developers (and LLMs) love