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
20 changes: 20 additions & 0 deletions api/v1alpha1/connection_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,26 @@ type SecretReference struct {
Name string `json:"name"`
}

// ConnectionTLSConfig defines TLS settings for a Connection.
type ConnectionTLSConfig struct {
// ServerName overrides the server name used to verify the Temporal server
// certificate when TLS is enabled.
// +optional
// +kubebuilder:validation:Pattern=`^[a-zA-Z0-9.-]+$`
ServerName string `json:"serverName,omitempty"`
}

// ConnectionSpec defines the desired state of Connection
// +kubebuilder:validation:XValidation:rule="!(has(self.mutualTLSSecretRef) && has(self.apiKeySecretRef))",message="Only one of mutualTLSSecretRef or apiKeySecretRef may be set"
type ConnectionSpec struct {
// The host and port of the Temporal server.
// +kubebuilder:validation:Pattern=`^[a-zA-Z0-9.-]+:[0-9]+$`
HostPort string `json:"hostPort"`

// TLS configures TLS behavior for the Temporal server connection.
// +optional
TLS *ConnectionTLSConfig `json:"tls,omitempty"`

// MutualTLSSecretRef is the name of the Secret that contains the TLS certificate and key
// for mutual TLS authentication. The secret must be `type: kubernetes.io/tls` or
// `type: Opaque` and exist in the same Kubernetes namespace as the Connection
Expand All @@ -46,6 +59,13 @@ type ConnectionSpec struct {
APIKeySecretRef *corev1.SecretKeySelector `json:"apiKeySecretRef,omitempty"`
}

func (s ConnectionSpec) TLSServerName() string {
if s.TLS == nil {
return ""
}
return s.TLS.ServerName
}

// ConnectionStatus defines the observed state of Connection
type ConnectionStatus struct {
// TODO(jlegrone): Add additional status fields following Kubernetes API conventions
Expand Down
20 changes: 20 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ workerOptions:

### Connection Configuration

Reference a `Connection` resource that defines server details. You can use either mutual TLS (mTLS) or API key authentication, but not both.
Reference a `Connection` resource that defines server details. You can use either mutual TLS (mTLS) or API key authentication, but not both. If the address in `hostPort` differs from the hostname on the server certificate, set `tls.serverName` to the certificate hostname.

**Using mTLS Authentication:**

Expand All @@ -167,6 +167,9 @@ metadata:
name: production-temporal
spec:
hostPort: "production.abc123.tmprl.cloud:7233"
# Optional: override the TLS server name used for certificate verification
tls:
serverName: "production.abc123.tmprl.cloud"
mutualTLSSecretRef:
name: temporal-cloud-mtls # Optional: for mTLS
```
Expand Down Expand Up @@ -234,6 +237,9 @@ metadata:
name: production-temporal
spec:
hostPort: "production.abc123.tmprl.cloud:7233"
# Optional: override the TLS server name used for certificate verification
tls:
serverName: "production.abc123.tmprl.cloud"
apiKeySecretRef:
name: temporal-api-key # Name of the Secret
key: api-key # Key within the Secret containing the API key token
Expand Down Expand Up @@ -291,6 +297,7 @@ echo -n "your-api-key-token-here" | base64
- Both secrets must be created in the same Kubernetes namespace as the `Connection` resource
- Only one authentication method can be specified per `Connection` (either `mutualTLSSecretRef` or `apiKeySecretRef`)
- The secret name and key in `apiKeySecretRef` must match the actual Secret resource and data key
- `tls.serverName` affects TLS certificate verification by the controller and is injected into Worker Pods as `TEMPORAL_TLS_SERVER_NAME` for SDK envconfig users.
- For mTLS secrets, the keys must be named exactly `tls.crt` and `tls.key`

## Gate Configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ spec:
required:
- name
type: object
tls:
properties:
serverName:
pattern: ^[a-zA-Z0-9.-]+$
type: string
type: object
required:
- hostPort
type: object
Expand Down
45 changes: 28 additions & 17 deletions internal/controller/clientpool/clientpool.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ const (
)

type ClientPoolKey struct {
HostPort string
Namespace string // Temporal namespace
SecretName string // Include secret name in key to invalidate cache when the secret name changes
AuthMode AuthMode // Include auth mode in key to invalidate cache when the auth mode changes for the secret
HostPort string
TLSServerName string
Namespace string // Temporal namespace
SecretName string // Include secret name in key to invalidate cache when the secret name changes
AuthMode AuthMode // Include auth mode in key to invalidate cache when the auth mode changes for the secret
}

type MTLSAuth struct {
Expand Down Expand Up @@ -123,6 +124,7 @@ type NewClientOptions struct {
}

func (cp *ClientPool) fetchClientUsingMTLSSecret(secret corev1.Secret, opts NewClientOptions) (*sdkclient.Options, *ClientPoolKey, *ClientAuth, error) {
tlsServerName := opts.Spec.TLSServerName()
clientOpts := sdkclient.Options{
Logger: cp.logger,
HostPort: opts.Spec.HostPort,
Expand Down Expand Up @@ -155,6 +157,7 @@ func (cp *ClientPool) fetchClientUsingMTLSSecret(secret corev1.Secret, opts NewC
}
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{cert},
ServerName: tlsServerName,
}
// If the secret contains a CA certificate, append it to the system CA pool for
// server certificate verification. This enables connecting to Temporal servers whose
Expand All @@ -177,10 +180,11 @@ func (cp *ClientPool) fetchClientUsingMTLSSecret(secret corev1.Secret, opts NewC
expiryTime = exp

key := ClientPoolKey{
HostPort: opts.Spec.HostPort,
Namespace: opts.TemporalNamespace,
SecretName: opts.Spec.MutualTLSSecretRef.Name,
AuthMode: AuthModeTLS,
HostPort: opts.Spec.HostPort,
TLSServerName: tlsServerName,
Namespace: opts.TemporalNamespace,
SecretName: opts.Spec.MutualTLSSecretRef.Name,
AuthMode: AuthModeTLS,
}
auth := ClientAuth{
mode: AuthModeTLS,
Expand All @@ -190,13 +194,14 @@ func (cp *ClientPool) fetchClientUsingMTLSSecret(secret corev1.Secret, opts NewC
}

func (cp *ClientPool) fetchClientUsingAPIKeySecret(opts NewClientOptions) (*sdkclient.Options, *ClientPoolKey, *ClientAuth, error) {
tlsServerName := opts.Spec.TLSServerName()
clientOpts := sdkclient.Options{
Logger: cp.logger,
HostPort: opts.Spec.HostPort,
Namespace: opts.TemporalNamespace,
Identity: opts.Identity,
ConnectionOptions: sdkclient.ConnectionOptions{
TLS: &tls.Config{},
TLS: &tls.Config{ServerName: tlsServerName},
},
}

Expand All @@ -208,10 +213,11 @@ func (cp *ClientPool) fetchClientUsingAPIKeySecret(opts NewClientOptions) (*sdkc
})

key := ClientPoolKey{
HostPort: opts.Spec.HostPort,
Namespace: opts.TemporalNamespace,
SecretName: opts.Spec.APIKeySecretRef.Name,
AuthMode: AuthModeAPIKey,
HostPort: opts.Spec.HostPort,
TLSServerName: tlsServerName,
Namespace: opts.TemporalNamespace,
SecretName: opts.Spec.APIKeySecretRef.Name,
AuthMode: AuthModeAPIKey,
}
auth := ClientAuth{
mode: AuthModeAPIKey,
Expand All @@ -222,18 +228,23 @@ func (cp *ClientPool) fetchClientUsingAPIKeySecret(opts NewClientOptions) (*sdkc
}

func (cp *ClientPool) fetchClientUsingNoCredentials(opts NewClientOptions) (*sdkclient.Options, *ClientPoolKey, *ClientAuth, error) {
tlsServerName := opts.Spec.TLSServerName()
clientOpts := sdkclient.Options{
Logger: cp.logger,
HostPort: opts.Spec.HostPort,
Namespace: opts.TemporalNamespace,
Identity: opts.Identity,
}
if tlsServerName != "" {
clientOpts.ConnectionOptions.TLS = &tls.Config{ServerName: tlsServerName}
}

key := ClientPoolKey{
HostPort: opts.Spec.HostPort,
Namespace: opts.TemporalNamespace,
SecretName: "",
AuthMode: AuthModeNoCredentials,
HostPort: opts.Spec.HostPort,
TLSServerName: tlsServerName,
Namespace: opts.TemporalNamespace,
SecretName: "",
AuthMode: AuthModeNoCredentials,
}
auth := ClientAuth{
mode: AuthModeNoCredentials,
Expand Down
72 changes: 72 additions & 0 deletions internal/controller/clientpool/clientpool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,78 @@ func TestFetchMTLS_ValidCert_Succeeds(t *testing.T) {
assert.Len(t, clientOpts.ConnectionOptions.TLS.Certificates, 1)
}

func TestFetchMTLS_TLSServerNameOverride(t *testing.T) {
now := time.Now()
caCert, caKey, _ := generateSelfSignedCACert(t, now.Add(-time.Hour), now.Add(time.Hour))
_, certPEM, keyPEM := generateLeafCert(t, caCert, caKey, "test.example.com", now.Add(-time.Hour), now.Add(time.Hour))

cp := newTestPool()
secret := makeTLSSecret(certPEM, keyPEM, nil)
opts := makeOpts("temporal-nlb.example.com:443")
opts.Spec.TLS = &temporaliov1alpha1.ConnectionTLSConfig{
ServerName: "temporal-cloud.example.com",
}

clientOpts, key, _, err := cp.fetchClientUsingMTLSSecret(secret, opts)

require.NoError(t, err)
require.NotNil(t, clientOpts.ConnectionOptions.TLS)
assert.Equal(t, "temporal-cloud.example.com", clientOpts.ConnectionOptions.TLS.ServerName)
assert.Equal(t, "temporal-cloud.example.com", key.TLSServerName)
}

func TestFetchAPIKey_TLSServerNameOverride(t *testing.T) {
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "api-key-secret", Namespace: "test-ns"},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{"apikey": []byte("test-api-key-value")},
}
cp := newTestPoolWithFakeClient(&secret)
apiKeySelector := &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "api-key-secret"},
Key: "apikey",
}
opts := NewClientOptions{
TemporalNamespace: "default",
K8sNamespace: "test-ns",
Spec: temporaliov1alpha1.ConnectionSpec{
HostPort: "temporal-nlb.example.com:443",
TLS: &temporaliov1alpha1.ConnectionTLSConfig{
ServerName: "temporal-cloud.example.com",
},
APIKeySecretRef: apiKeySelector,
},
}

clientOpts, key, _, err := cp.fetchClientUsingAPIKeySecret(opts)

require.NoError(t, err)
require.NotNil(t, clientOpts.ConnectionOptions.TLS)
assert.Equal(t, "temporal-cloud.example.com", clientOpts.ConnectionOptions.TLS.ServerName)
assert.Equal(t, "temporal-cloud.example.com", key.TLSServerName)
}

func TestFetchNoCredentials_TLSServerNameOverride(t *testing.T) {
cp := newTestPool()
opts := NewClientOptions{
TemporalNamespace: "default",
Spec: temporaliov1alpha1.ConnectionSpec{
HostPort: "temporal-nlb.example.com:443",
TLS: &temporaliov1alpha1.ConnectionTLSConfig{
ServerName: "temporal-cloud.example.com",
},
},
}

clientOpts, key, auth, err := cp.fetchClientUsingNoCredentials(opts)

require.NoError(t, err)
require.NotNil(t, clientOpts.ConnectionOptions.TLS)
assert.Equal(t, "temporal-cloud.example.com", clientOpts.ConnectionOptions.TLS.ServerName)
assert.Equal(t, "temporal-cloud.example.com", key.TLSServerName)
assert.Equal(t, AuthModeNoCredentials, auth.mode)
}

func newTestPoolWithFakeClient(objects ...runtime.Object) *ClientPool {
scheme := runtime.NewScheme()
_ = corev1.AddToScheme(scheme)
Expand Down
18 changes: 10 additions & 8 deletions internal/controller/worker_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,11 @@ func (r *WorkerDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Req

// Get or update temporal client for connection
clientPoolKey := clientpool.ClientPoolKey{
HostPort: connection.Spec.HostPort,
Namespace: workerDeploy.Spec.WorkerOptions.TemporalNamespace,
SecretName: secretName,
AuthMode: authMode,
HostPort: connection.Spec.HostPort,
TLSServerName: connection.Spec.TLSServerName(),
Namespace: workerDeploy.Spec.WorkerOptions.TemporalNamespace,
SecretName: secretName,
AuthMode: authMode,
}
temporalClient, ok := r.TemporalClientPool.GetSDKClient(clientPoolKey)
if !ok {
Expand Down Expand Up @@ -576,10 +577,11 @@ func (r *WorkerDeploymentReconciler) handleDeletion(
}

temporalClient, ok := r.TemporalClientPool.GetSDKClient(clientpool.ClientPoolKey{
HostPort: temporalConnection.Spec.HostPort,
Namespace: workerDeploy.Spec.WorkerOptions.TemporalNamespace,
SecretName: secretName,
AuthMode: authMode,
HostPort: temporalConnection.Spec.HostPort,
TLSServerName: temporalConnection.Spec.TLSServerName(),
Namespace: workerDeploy.Spec.WorkerOptions.TemporalNamespace,
SecretName: secretName,
AuthMode: authMode,
})
if !ok {
clientOpts, key, clientAuth, err := r.TemporalClientPool.ParseClientSecret(ctx, secretName, authMode, clientpool.NewClientOptions{
Expand Down
11 changes: 11 additions & 0 deletions internal/k8s/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ func ComputeConnectionSpecHash(connection temporaliov1alpha1.ConnectionSpec) str

// Hash connection spec fields in deterministic order
_, _ = hasher.Write([]byte(connection.HostPort))
_, _ = hasher.Write([]byte(connection.TLSServerName()))
if connection.MutualTLSSecretRef != nil {
_, _ = hasher.Write([]byte(connection.MutualTLSSecretRef.Name))
} else if connection.APIKeySecretRef != nil {
Expand Down Expand Up @@ -370,6 +371,16 @@ func ApplyControllerPodSpecModifications(
podSpec.Containers[i] = container
}

if tlsServerName := connection.TLSServerName(); tlsServerName != "" {
for i, container := range podSpec.Containers {
container.Env = append(container.Env, corev1.EnvVar{
Name: "TEMPORAL_TLS_SERVER_NAME",
Value: tlsServerName,
})
podSpec.Containers[i] = container
}
}

// Add TLS config if mTLS is enabled
if connection.MutualTLSSecretRef != nil {
for i, container := range podSpec.Containers {
Expand Down
Loading
Loading