Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions auth/credential_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ type (
EmptyCredentialProvider struct{}
)

// CredentialHeaders are the headers that carry a caller's credentials. With credential identity "proxy" or
// "strip", they are removed from every call the proxy forwards to the cluster.
var CredentialHeaders = []string{"authorization", "authorization-extras"}

// Module provides the CredentialProvider. It defaults to EmptyCredentialProvider; override it with
// WithCredentialProvider.
var Module = fx.Options(
Expand Down
46 changes: 36 additions & 10 deletions config/cluster_conn_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,20 @@ type (
TcpServer TCPTLSInfo `yaml:"tcpServer"`
MuxCount int `yaml:"muxCount"`
MuxAddressInfo TCPTLSInfo `yaml:"muxAddressInfo"`
// Credentials controls whether calls on this connection carry the auth.CredentialProvider's credentials.
// Credentials controls which identity calls on this connection present to the cluster.
// Only the local cluster definition supports it.
Credentials *CredentialsConfig `yaml:"credentials"`
}

CredentialsConfig struct {
// Enabled attaches the CredentialProvider's credentials to every call made to this cluster.
Enabled bool `yaml:"enabled"`
// Identity is whose credentials the cluster sees. Empty means CredentialIdentityDefault.
Identity CredentialIdentity `yaml:"identity"`
}

// CredentialIdentity is whose credentials a cluster sees on calls from the proxy. The credential headers are
// "authorization" and "authorization-extras".
CredentialIdentity string

TCPTLSInfo struct {
ConnectionString string `yaml:"address"`
TLSConfig encryption.TLSConfig `yaml:"tls"`
Expand All @@ -74,6 +78,16 @@ type (
}
)

const (
// CredentialIdentityDefault forwards the caller's credentials: whatever the caller sent is passed through
// unchanged, and the proxy adds none. It applies when identity is not set.
CredentialIdentityDefault CredentialIdentity = "default"
// CredentialIdentityProxy drops forwarded credentials and sends the auth.CredentialProvider's instead.
CredentialIdentityProxy CredentialIdentity = "proxy"
// CredentialIdentityStrip drops forwarded credentials and sends none.
CredentialIdentityStrip CredentialIdentity = "strip"
)

const (
ConnTypeTCP ConnectionType = "tcp"
ConnTypeMuxServer ConnectionType = "mux-server"
Expand All @@ -98,9 +112,12 @@ func (config *StringTranslator) AsLocalToRemoteBiMap() (collect.StaticBiMap[stri
return config.cachedBiMap, nil
}

// CredentialsEnabled reports whether calls on this connection carry the CredentialProvider's credentials.
func (c ClusterDefinition) CredentialsEnabled() bool {
return c.Credentials != nil && c.Credentials.Enabled
// CredentialIdentity returns whose credentials this cluster sees, defaulting to CredentialIdentityDefault.
func (c ClusterDefinition) CredentialIdentity() CredentialIdentity {
if c.Credentials == nil || c.Credentials.Identity == "" {
return CredentialIdentityDefault
}
return c.Credentials.Identity
}

// Validate reports problems in this connection's config. Only the encryption
Expand All @@ -109,11 +126,20 @@ func (c *ClusterConnConfig) Validate() error {
return validation.Validate(
"",
validation.Nested("encryption", &c.EncryptionConfig),
validation.Field("local.credentials", c.Local, func(local ClusterDefinition) error {
if local.CredentialsEnabled() && local.ConnectionType != ConnTypeTCP {
return fmt.Errorf("credentials require connectionType %q, got %q", ConnTypeTCP, local.ConnectionType)
validation.Field("local.credentials.identity", c.Local, func(local ClusterDefinition) error {
switch identity := local.CredentialIdentity(); identity {
case CredentialIdentityDefault, CredentialIdentityStrip:
return nil
case CredentialIdentityProxy:
if local.ConnectionType != ConnTypeTCP {
return fmt.Errorf("identity %q requires connectionType %q, got %q",
identity, ConnTypeTCP, local.ConnectionType)
}
return nil
default:
return fmt.Errorf("unsupported identity %q: must be %q, %q or %q",
identity, CredentialIdentityDefault, CredentialIdentityProxy, CredentialIdentityStrip)
}
return nil
}),
validation.Field("remote.credentials", c.Remote.Credentials, func(credentials *CredentialsConfig) error {
if credentials != nil {
Expand Down
42 changes: 29 additions & 13 deletions config/credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,43 @@ import (
)

func TestCredentialsConfigValidate(t *testing.T) {
enabled := &CredentialsConfig{Enabled: true}
identity := func(identity CredentialIdentity) *CredentialsConfig {
return &CredentialsConfig{Identity: identity}
}
tests := []struct {
name string
conn ClusterConnConfig
wantError string
}{
{
name: "local tcp with credentials",
conn: ClusterConnConfig{Local: ClusterDefinition{ConnectionType: ConnTypeTCP, Credentials: enabled}},
name: "no credentials block",
conn: ClusterConnConfig{Local: ClusterDefinition{ConnectionType: ConnTypeMuxClient}},
},
{
name: "local tcp with proxy identity",
conn: ClusterConnConfig{Local: ClusterDefinition{ConnectionType: ConnTypeTCP, Credentials: identity(CredentialIdentityProxy)}},
},
{
name: "local mux with default identity",
conn: ClusterConnConfig{Local: ClusterDefinition{ConnectionType: ConnTypeMuxClient, Credentials: identity(CredentialIdentityDefault)}},
},
{
name: "local mux with strip identity",
conn: ClusterConnConfig{Local: ClusterDefinition{ConnectionType: ConnTypeMuxClient, Credentials: identity(CredentialIdentityStrip)}},
},
{
name: "local mux with credentials disabled",
conn: ClusterConnConfig{Local: ClusterDefinition{ConnectionType: ConnTypeMuxClient, Credentials: &CredentialsConfig{}}},
name: "local mux with proxy identity",
conn: ClusterConnConfig{Local: ClusterDefinition{ConnectionType: ConnTypeMuxClient, Credentials: identity(CredentialIdentityProxy)}},
wantError: `identity "proxy" requires connectionType "tcp", got "mux-client"`,
},
{
name: "local mux with credentials enabled",
conn: ClusterConnConfig{Local: ClusterDefinition{ConnectionType: ConnTypeMuxClient, Credentials: enabled}},
wantError: `credentials require connectionType "tcp", got "mux-client"`,
name: "unsupported identity",
conn: ClusterConnConfig{Local: ClusterDefinition{ConnectionType: ConnTypeTCP, Credentials: identity("everyone")}},
wantError: `unsupported identity "everyone"`,
},
{
name: "remote with credentials",
conn: ClusterConnConfig{Remote: ClusterDefinition{ConnectionType: ConnTypeTCP, Credentials: enabled}},
conn: ClusterConnConfig{Remote: ClusterDefinition{ConnectionType: ConnTypeTCP, Credentials: identity(CredentialIdentityProxy)}},
wantError: "credentials are only supported on the local cluster definition",
},
}
Expand All @@ -44,8 +59,9 @@ func TestCredentialsConfigValidate(t *testing.T) {
}
}

func TestCredentialsEnabled(t *testing.T) {
require.False(t, ClusterDefinition{}.CredentialsEnabled())
require.False(t, ClusterDefinition{Credentials: &CredentialsConfig{}}.CredentialsEnabled())
require.True(t, ClusterDefinition{Credentials: &CredentialsConfig{Enabled: true}}.CredentialsEnabled())
func TestCredentialIdentityDefault(t *testing.T) {
require.Equal(t, CredentialIdentityDefault, ClusterDefinition{}.CredentialIdentity())
require.Equal(t, CredentialIdentityDefault, ClusterDefinition{Credentials: &CredentialsConfig{}}.CredentialIdentity())
require.Equal(t, CredentialIdentityProxy,
ClusterDefinition{Credentials: &CredentialsConfig{Identity: CredentialIdentityProxy}}.CredentialIdentity())
}
23 changes: 17 additions & 6 deletions examples/bearer-token/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ app.New("s2s-proxy-bearer-example", "dev",
)
```

The provider is used only where the config turns it on, with `local.credentials.enabled`:
The provider is used only where the config turns it on, with `local.credentials.identity: proxy`:

```yaml
local:
Expand All @@ -30,13 +30,24 @@ local:
address: temporal-frontend.example.invalid:7233
tls: { ... }
credentials:
enabled: true
identity: proxy
```

The program supplies how to get a token, and the config decides whether to send it. If the config enables
credentials but the binary has no provider, for example the stock `s2s-proxy`, the proxy refuses to start rather than
call the server without a token. A provider in the binary does nothing for a connection that does not enable it.
`credentials` is only accepted on the local cluster definition.
`identity` is whose credentials the local server sees, in the `authorization` and `authorization-extras` headers:

| `identity` | Credentials the caller sent | Proxy's credentials |
| --- | --- | --- |
| `default` (when not set) | forwarded | not sent |
| `proxy` | dropped | sent |
| `strip` | dropped | not sent |

With `proxy`, a request forwarded from the remote side, such as a replication stream, reaches the local server with
the proxy's token and never with a token the remote side sent.

The program supplies how to get a token, and the config decides whether to send it. If the config sets
`identity: proxy` but the binary has no provider, for example the stock `s2s-proxy`, the proxy refuses to start rather
than call the server without a token. A provider in the binary does nothing for a connection that does not set
`identity: proxy`. `credentials` is only accepted on the local cluster definition.

`Get` returns a gRPC `credentials.PerRPCCredentials`. gRPC calls its `GetRequestMetadata` for every unary call and every
new stream, so a rotated token takes effect on the next call without restarting the proxy or recreating connections.
Expand Down
16 changes: 11 additions & 5 deletions examples/bearer-token/config.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# s2s-proxy configuration for the bearer-token example.
#
# local.credentials.enabled attaches the bearer token to every call the proxy makes to the local Temporal frontend
# (local.tcpClient). It is never sent to the remote side. The token is read from S2S_PROXY_EXAMPLE_BEARER_TOKEN, not
# from this file.
# local.credentials.identity: proxy attaches the bearer token to every call the proxy makes to the local Temporal
# frontend (local.tcpClient). It is never sent to the remote side. The token is read from
# S2S_PROXY_EXAMPLE_BEARER_TOKEN, not from this file.
#
# Hostnames use the reserved .invalid TLD and certificate paths are placeholders; replace both for a real deployment.
clusterConnections:
Expand All @@ -16,9 +16,15 @@ clusterConnections:
remoteCAPath: /etc/s2s-proxy/tls/frontend-ca.pem
caServerName: temporal-frontend.example.invalid
skipCAVerification: false
# Send the CredentialProvider's credentials on calls to the local Temporal server.
# Whose credentials the local Temporal server sees on calls from the proxy, carried in the "authorization" and
# "authorization-extras" headers:
# default - (when identity is not set) forward the caller's credentials unchanged; the proxy adds nothing.
# proxy - drop any credentials the caller sent and send the proxy's own, from its CredentialProvider. The
# proxy refuses to start if the binary has no CredentialProvider.
# strip - drop any credentials the caller sent and send none.
# Only the local cluster definition accepts credentials.
credentials:
enabled: true
identity: proxy
tcpServer:
address: 0.0.0.0:9002
tls:
Expand Down
4 changes: 2 additions & 2 deletions examples/bearer-token/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ func TestExampleConfiguration(t *testing.T) {
require.NoError(t, proxyConfig.Validate())
require.Len(t, proxyConfig.ClusterConnections, 1)

// Credentials are enabled on the local client, which must be TCP with TLS because the token requires it.
// The local client presents the proxy's identity, so it must be TCP with TLS because the token requires it.
local := proxyConfig.ClusterConnections[0].Local
require.True(t, local.CredentialsEnabled())
require.Equal(t, config.CredentialIdentityProxy, local.CredentialIdentity())
require.Equal(t, config.ConnTypeTCP, local.ConnectionType)
require.True(t, local.TcpClient.TLSConfig.IsEnabled())
}
16 changes: 12 additions & 4 deletions proxy/cluster_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func NewClusterConnection(lifetime context.Context, connConfig config.ClusterCon
}

// newClusterConnection is NewClusterConnection with a CredentialProvider. Its credentials are attached to calls made to
// the local Temporal server when local.credentials.enabled is set, and never to the remote side.
// the local Temporal server when local.credentials.identity is "proxy", and never to the remote side.
func newClusterConnection(
lifetime context.Context,
connConfig config.ClusterConnConfig,
Expand Down Expand Up @@ -258,10 +258,16 @@ func createClient(
credentialProvider auth.CredentialProvider,
) (closableClientConn, error) {
var clientOptions grpcutil.ClientOptions
if transportCfg.CredentialsEnabled() {
switch identity := transportCfg.CredentialIdentity(); identity {
case config.CredentialIdentityDefault:
// Forward the caller's credentials.
case config.CredentialIdentityStrip:
clientOptions.StripOutgoingMetadataKeys = auth.CredentialHeaders
case config.CredentialIdentityProxy:
if auth.IsEmptyCredentialProvider(credentialProvider) {
return nil, fmt.Errorf("%s client: credentials are enabled but no CredentialProvider is configured", directionLabel)
return nil, fmt.Errorf("%s client: credentials identity %q but no CredentialProvider is configured", directionLabel, identity)
}
clientOptions.StripOutgoingMetadataKeys = auth.CredentialHeaders
clientOptions.PerRPCCredentials = credentialProvider.Get()
if clientOptions.PerRPCCredentials == nil {
return nil, fmt.Errorf("%s client: credential provider returned no credentials", directionLabel)
Expand All @@ -272,6 +278,8 @@ func createClient(
if clientOptions.PerRPCCredentials.RequireTransportSecurity() && !transportCfg.TcpClient.TLSConfig.IsEnabled() {
return nil, fmt.Errorf("%s client: credentials require TLS, but tcpClient.tls is not configured", directionLabel)
}
default:
return nil, fmt.Errorf("%s client: unsupported credentials identity %q", directionLabel, identity)
}

switch transportCfg.ConnectionType {
Expand All @@ -280,7 +288,7 @@ func createClient(
case config.ConnTypeMuxClient, config.ConnTypeMuxServer:
return grpcutil.NewMultiClientConn(lifetime, fmt.Sprintf("client-conn-%s", connectionName),
// TLS is handled by the mux connection, so tlsConfig will always be nil
grpcutil.MakeDialOptions(nil, metrics.GetGRPCClientMetrics(directionLabel))...)
grpcutil.MakeDialOptions(nil, metrics.GetGRPCClientMetrics(directionLabel), clientOptions)...)
default:
return nil, errors.New("invalid connection type")
}
Expand Down
Loading
Loading