Skip to content

Add pluggable credential provider for calls to the local Temporal server - #309

Merged
hehaifengcn merged 3 commits into
mainfrom
haifengh/authZ
Oct 8, 2026
Merged

hehaifengcn merged 3 commits into
mainfrom
haifengh/authZ

Conversation

@hehaifengcn

Copy link
Copy Markdown
Collaborator

Add auth.CredentialProvider, provided by auth.Module and defaulting to
EmptyCredentialProvider. Embedders can replace it with
auth.WithCredentialProvider to attach per-RPC credentials, such as a
bearer token, to every call the proxy makes to its local Temporal server
(AdminService, WorkflowService, OperatorService, and replication streams).

The credentials are applied only to the local (inbound) client, never to
the remote side. createClient fails at startup if the provider returns no
credentials, the local connection is not TCP, or the credentials require
TLS and tcpClient.tls is not configured.

Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com

What was changed

Why?

Checklist

  1. Closes

  2. How was this tested:

  1. Any docs updates needed?

hehaifengcn and others added 3 commits October 7, 2026 15:03
Add auth.CredentialProvider, provided by auth.Module and defaulting to
EmptyCredentialProvider. Embedders can replace it with
auth.WithCredentialProvider to attach per-RPC credentials, such as a
bearer token, to every call the proxy makes to its local Temporal server
(AdminService, WorkflowService, OperatorService, and replication streams).

The credentials are applied only to the local (inbound) client, never to
the remote side. createClient fails at startup if the provider returns no
credentials, the local connection is not TCP, or the credentials require
TLS and tcpClient.tls is not configured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add examples/bearer-token, a runnable program that starts s2s-proxy with a
custom auth.CredentialProvider. The provider attaches
"authorization: Bearer <token>" to every call the proxy makes to its local
Temporal server, reading the token from S2S_PROXY_EXAMPLE_BEARER_TOKEN on
each call so rotated tokens take effect without a restart. The credentials
require TLS, and the sample config uses a TCP local connection with TLS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CredentialProvider's credentials are now attached to calls to the local
Temporal server only when the config sets local.credentials.enabled. The
binary supplies how to get credentials; the config decides whether to send
them.

The proxy refuses to start when credentials are enabled but no
CredentialProvider is configured, so a stock binary never calls an
authZ-enabled server without a token. Config validation rejects
credentials on a non-TCP local connection and any remote.credentials
block, keeping the local credential off the remote side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hehaifengcn
hehaifengcn marked this pull request as ready for review October 7, 2026 23:05
@hehaifengcn
hehaifengcn requested a review from a team as a code owner October 7, 2026 23:05

@pseudomuto pseudomuto left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a few comments, but nothing blocking. Code LGTM, logic is right, tests are good. :shipit:

// WithCredentialProvider replaces the default CredentialProvider with the given one.
// Pass it to app.New as an extra fx option.
func WithCredentialProvider(provider CredentialProvider) fx.Option {
return fx.Decorate(func(CredentialProvider) CredentialProvider { return provider })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is clever 👍


// IsEmptyCredentialProvider reports whether the provider supplies no credentials.
func IsEmptyCredentialProvider(provider CredentialProvider) bool {
if provider == nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/nit I don't think you need this. _, ok := provider.(EmptyCredentialProvider) will return false for a nil provider.

Comment thread proxy/proxy.go
func NewProxy(
configProvider config.ConfigProvider,
logProvider logging.LoggerProvider,
credentialProvider auth.CredentialProvider,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth noting: this is a potentially breaking change for anyone who embeds the proxy. I don't imagine that's common, but it might come up at some point, and I just wanted to flag it.

if auth.IsEmptyCredentialProvider(credentialProvider) {
return nil, fmt.Errorf("%s client: credentials are enabled but no CredentialProvider is configured", directionLabel)
}
clientOptions.PerRPCCredentials = credentialProvider.Get()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StreamForwarder.Run forwards the incoming metadata unchanged to the local server. If the remote peer sends an auth header, the local server gets two values (this token and the remote's).

I think grpc-go writes per-RPC creds first, so authHeaders[0] would still be ours, but the remote's token still reaches the local server. Is it worth deleting authorization from the forwarded metadata when credentials are enabled?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will strip the header in a follow-up PR.

@hehaifengcn
hehaifengcn merged commit b38e09e into main Oct 8, 2026
6 checks passed
@hehaifengcn
hehaifengcn deleted the haifengh/authZ branch October 8, 2026 15:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants