Authentication & Authorization¶
This guide provides comprehensive authentication and authorization strategies for Temporal.io deployments, ensuring secure access control across all components including server, workers, and client applications.
Overview¶
Temporal.io security involves multiple layers: - Authentication: Verifying identity of users and services - Authorization: Controlling access to resources and operations - mTLS: Mutual TLS for service-to-service communication - RBAC: Role-based access control for fine-grained permissions - Integration: OIDC, LDAP, and custom authentication providers
Architecture¶
graph TB
subgraph "External Authentication"
OIDC[OIDC Provider]
LDAP[LDAP/AD]
CUSTOM[Custom Auth]
end
subgraph "Temporal Cluster"
FRONTEND[Frontend Service]
HISTORY[History Service]
MATCHING[Matching Service]
WORKER[Worker Service]
subgraph "Authorization"
AUTHZ[Authorizer Plugin]
RBAC[RBAC Rules]
CLAIMS[Claims Mapper]
end
end
subgraph "Clients & Workers"
SDK[SDK Client]
CLI[Temporal CLI]
UI[Web UI]
WORKERS[Application Workers]
end
subgraph "Certificate Authority"
CA[Root CA]
INTER[Intermediate CA]
CERTS[Client Certificates]
end
OIDC --> FRONTEND
LDAP --> FRONTEND
CUSTOM --> FRONTEND
FRONTEND --> AUTHZ
AUTHZ --> RBAC
AUTHZ --> CLAIMS
SDK --> FRONTEND
CLI --> FRONTEND
UI --> FRONTEND
WORKERS --> FRONTEND
CA --> INTER
INTER --> CERTS
CERTS --> SDK
CERTS --> WORKERS
Authentication Methods¶
1. API Key Authentication¶
Server Configuration¶
# config/auth-config.yaml
auth:
enabled: true
authorizer: "api-key"
token_key_id: "temporal-api-key"
global:
authorization:
jwtKeyProvider:
keySourceURIs:
- "https://auth.company.com/.well-known/jwks.json"
permissionsClaimName: "permissions"
authorizer: "api-key"
API Key Management¶
// auth/api-key-manager.go
package auth
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"time"
"github.com/golang-jwt/jwt/v4"
)
type APIKeyManager struct {
signingKey []byte
issuer string
audience string
}
type APIKeyClaims struct {
UserID string `json:"user_id"`
Permissions []string `json:"permissions"`
Namespaces []string `json:"namespaces"`
jwt.RegisteredClaims
}
func NewAPIKeyManager(signingKey []byte, issuer, audience string) *APIKeyManager {
return &APIKeyManager{
signingKey: signingKey,
issuer: issuer,
audience: audience,
}
}
func (m *APIKeyManager) GenerateAPIKey(userID string, permissions, namespaces []string, expiry time.Duration) (string, error) {
now := time.Now()
claims := APIKeyClaims{
UserID: userID,
Permissions: permissions,
Namespaces: namespaces,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: m.issuer,
Audience: jwt.ClaimStrings{m.audience},
Subject: userID,
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(expiry)),
NotBefore: jwt.NewNumericDate(now),
ID: generateJTI(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(m.signingKey)
}
func (m *APIKeyManager) ValidateAPIKey(tokenString string) (*APIKeyClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &APIKeyClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return m.signingKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*APIKeyClaims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}
func generateJTI() string {
bytes := make([]byte, 16)
rand.Read(bytes)
return base64.URLEncoding.EncodeToString(bytes)
}
Client Usage¶
// client/auth-client.go
package client
import (
"context"
"crypto/tls"
"go.temporal.io/sdk/client"
)
func NewAuthenticatedClient(hostPort, namespace, apiKey string) (client.Client, error) {
return client.Dial(client.Options{
HostPort: hostPort,
Namespace: namespace,
ConnectionOptions: client.ConnectionOptions{
TLS: &tls.Config{
ServerName: "temporal.company.com",
},
},
Credentials: client.NewAPIKeyStaticCredentials(apiKey),
})
}
2. OIDC Authentication¶
OIDC Configuration¶
# config/oidc-config.yaml
auth:
enabled: true
authorizer: "oidc"
global:
authorization:
jwtKeyProvider:
keySourceURIs:
- "https://auth.company.com/.well-known/jwks.json"
refreshInterval: "1h"
permissionsClaimName: "permissions"
authorizer: "oidc"
oidc:
issuer_url: "https://auth.company.com"
client_id: "temporal-cluster"
client_secret: "${OIDC_CLIENT_SECRET}"
scopes:
- "openid"
- "profile"
- "email"
- "temporal:read"
- "temporal:write"
redirect_urls:
- "https://temporal.company.com/auth/callback"
claims_mapping:
user_id: "sub"
email: "email"
groups: "groups"
permissions: "temporal_permissions"
OIDC Integration¶
// auth/oidc-provider.go
package auth
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
type OIDCProvider struct {
provider *oidc.Provider
oauth2Config oauth2.Config
verifier *oidc.IDTokenVerifier
}
type OIDCConfig struct {
IssuerURL string `json:"issuer_url"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
RedirectURL string `json:"redirect_url"`
Scopes []string `json:"scopes"`
}
func NewOIDCProvider(ctx context.Context, config OIDCConfig) (*OIDCProvider, error) {
provider, err := oidc.NewProvider(ctx, config.IssuerURL)
if err != nil {
return nil, fmt.Errorf("failed to create OIDC provider: %w", err)
}
oauth2Config := oauth2.Config{
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
RedirectURL: config.RedirectURL,
Endpoint: provider.Endpoint(),
Scopes: config.Scopes,
}
verifier := provider.Verifier(&oidc.Config{
ClientID: config.ClientID,
})
return &OIDCProvider{
provider: provider,
oauth2Config: oauth2Config,
verifier: verifier,
}, nil
}
func (p *OIDCProvider) GetAuthURL(state string) string {
return p.oauth2Config.AuthCodeURL(state, oauth2.AccessTypeOffline)
}
func (p *OIDCProvider) ExchangeCode(ctx context.Context, code string) (*oidc.IDToken, error) {
token, err := p.oauth2Config.Exchange(ctx, code)
if err != nil {
return nil, fmt.Errorf("failed to exchange code: %w", err)
}
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, fmt.Errorf("no id_token in response")
}
idToken, err := p.verifier.Verify(ctx, rawIDToken)
if err != nil {
return nil, fmt.Errorf("failed to verify ID token: %w", err)
}
return idToken, nil
}
type UserClaims struct {
Sub string `json:"sub"`
Email string `json:"email"`
Groups []string `json:"groups"`
Permissions []string `json:"temporal_permissions"`
}
func (p *OIDCProvider) ParseClaims(idToken *oidc.IDToken) (*UserClaims, error) {
var claims UserClaims
if err := idToken.Claims(&claims); err != nil {
return nil, fmt.Errorf("failed to parse claims: %w", err)
}
return &claims, nil
}
3. mTLS Authentication¶
Certificate Generation¶
#!/bin/bash
# scripts/generate-certs.sh
set -euo pipefail
CERT_DIR="certs"
CA_KEY="$CERT_DIR/ca-key.pem"
CA_CERT="$CERT_DIR/ca-cert.pem"
DAYS_VALID=3650
log() {
echo -e "\033[0;32m[$(date +'%Y-%m-%d %H:%M:%S')] $1\033[0m"
}
error() {
echo -e "\033[0;31m[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1\033[0m"
exit 1
}
# Create certificate directory
mkdir -p "$CERT_DIR"
# Generate CA private key
if [[ ! -f "$CA_KEY" ]]; then
log "Generating CA private key..."
openssl genrsa -out "$CA_KEY" 4096
chmod 600 "$CA_KEY"
fi
# Generate CA certificate
if [[ ! -f "$CA_CERT" ]]; then
log "Generating CA certificate..."
openssl req -new -x509 -key "$CA_KEY" -sha256 -subj "/C=US/ST=CA/O=Company/CN=Temporal CA" -days $DAYS_VALID -out "$CA_CERT"
fi
# Function to generate client/server certificates
generate_cert() {
local name="$1"
local common_name="$2"
local san="${3:-}"
local key_file="$CERT_DIR/${name}-key.pem"
local csr_file="$CERT_DIR/${name}-csr.pem"
local cert_file="$CERT_DIR/${name}-cert.pem"
# Generate private key
log "Generating private key for $name..."
openssl genrsa -out "$key_file" 2048
chmod 600 "$key_file"
# Create certificate signing request
log "Creating CSR for $name..."
if [[ -n "$san" ]]; then
# Create config file for SAN
local config_file="$CERT_DIR/${name}.conf"
cat > "$config_file" << EOF
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
C = US
ST = CA
O = Company
CN = $common_name
[v3_req]
keyUsage = keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth, clientAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = $common_name
$san
EOF
openssl req -new -key "$key_file" -out "$csr_file" -config "$config_file"
# Sign certificate with CA
log "Signing certificate for $name..."
openssl x509 -req -in "$csr_file" -CA "$CA_CERT" -CAkey "$CA_KEY" -CAcreateserial -out "$cert_file" -days $DAYS_VALID -extensions v3_req -extfile "$config_file"
# Clean up
rm "$config_file" "$csr_file"
else
openssl req -new -key "$key_file" -out "$csr_file" -subj "/C=US/ST=CA/O=Company/CN=$common_name"
# Sign certificate with CA
log "Signing certificate for $name..."
openssl x509 -req -in "$csr_file" -CA "$CA_CERT" -CAkey "$CA_KEY" -CAcreateserial -out "$cert_file" -days $DAYS_VALID
# Clean up
rm "$csr_file"
fi
log "✓ Generated certificate for $name: $cert_file"
}
# Generate server certificate
generate_cert "server" "temporal.company.com" "DNS.2 = temporal-frontend
DNS.3 = temporal-frontend.temporal.svc.cluster.local
DNS.4 = localhost
IP.1 = 127.0.0.1"
# Generate client certificates
generate_cert "client" "temporal-client"
generate_cert "worker" "temporal-worker"
generate_cert "admin" "temporal-admin"
log "✓ All certificates generated successfully!"
log "CA Certificate: $CA_CERT"
log "Use these certificates for mTLS authentication with Temporal"
mTLS Configuration¶
# config/mtls-config.yaml
tls:
# Server TLS configuration
frontend:
server:
certFile: "/etc/temporal/certs/server-cert.pem"
keyFile: "/etc/temporal/certs/server-key.pem"
clientCaFiles:
- "/etc/temporal/certs/ca-cert.pem"
requireClientAuth: true
# Internal service communication
internode:
server:
certFile: "/etc/temporal/certs/server-cert.pem"
keyFile: "/etc/temporal/certs/server-key.pem"
clientCaFiles:
- "/etc/temporal/certs/ca-cert.pem"
requireClientAuth: true
client:
certFile: "/etc/temporal/certs/client-cert.pem"
keyFile: "/etc/temporal/certs/client-key.pem"
serverCaFiles:
- "/etc/temporal/certs/ca-cert.pem"
serverName: "temporal.company.com"
# Database TLS
persistence:
default:
sql:
tls:
enabled: true
caFile: "/etc/temporal/certs/ca-cert.pem"
certFile: "/etc/temporal/certs/client-cert.pem"
keyFile: "/etc/temporal/certs/client-key.pem"
serverName: "postgres.company.com"
Authorization Framework¶
1. Custom Authorizer Plugin¶
Plugin Interface¶
// auth/authorizer.go
package auth
import (
"context"
"go.temporal.io/server/common/authorization"
)
type TemporalAuthorizer struct {
rbacProvider RBACProvider
logger log.Logger
}
func NewTemporalAuthorizer(rbacProvider RBACProvider, logger log.Logger) authorization.Authorizer {
return &TemporalAuthorizer{
rbacProvider: rbacProvider,
logger: logger,
}
}
func (a *TemporalAuthorizer) Authorize(ctx context.Context, claims *authorization.Claims, target *authorization.CallTarget) (authorization.Result, error) {
// Extract user information from claims
userID := claims.Subject
if userID == "" {
return authorization.Result{Decision: authorization.DecisionDeny}, nil
}
// Get user permissions
permissions, err := a.rbacProvider.GetUserPermissions(ctx, userID)
if err != nil {
a.logger.Error("Failed to get user permissions", tag.Error(err), tag.WorkflowID(userID))
return authorization.Result{Decision: authorization.DecisionDeny}, err
}
// Check if user has required permission for the target
required := getRequiredPermission(target)
if !hasPermission(permissions, required) {
a.logger.Warn("User denied access",
tag.WorkflowID(userID),
tag.Value("required", required),
tag.Value("target", target.APIName))
return authorization.Result{Decision: authorization.DecisionDeny}, nil
}
return authorization.Result{Decision: authorization.DecisionAllow}, nil
}
func getRequiredPermission(target *authorization.CallTarget) string {
switch target.APIName {
case "StartWorkflowExecution":
return "temporal:workflow:start"
case "TerminateWorkflowExecution":
return "temporal:workflow:terminate"
case "DescribeWorkflowExecution":
return "temporal:workflow:read"
case "ListWorkflowExecutions":
return "temporal:workflow:list"
case "GetWorkflowExecutionHistory":
return "temporal:workflow:history"
case "SignalWorkflowExecution":
return "temporal:workflow:signal"
case "QueryWorkflow":
return "temporal:workflow:query"
case "CreateSchedule":
return "temporal:schedule:create"
case "UpdateSchedule":
return "temporal:schedule:update"
case "DeleteSchedule":
return "temporal:schedule:delete"
case "DescribeNamespace":
return "temporal:namespace:read"
case "ListNamespaces":
return "temporal:namespace:list"
default:
return "temporal:unknown"
}
}
func hasPermission(userPermissions []string, required string) bool {
for _, perm := range userPermissions {
if perm == required || perm == "temporal:admin" {
return true
}
// Check wildcard permissions
if matchesWildcard(perm, required) {
return true
}
}
return false
}
func matchesWildcard(pattern, permission string) bool {
// Simple wildcard matching for permissions like "temporal:workflow:*"
if !strings.HasSuffix(pattern, "*") {
return false
}
prefix := strings.TrimSuffix(pattern, "*")
return strings.HasPrefix(permission, prefix)
}
2. RBAC Provider¶
Role-Based Access Control¶
// auth/rbac.go
package auth
import (
"context"
"encoding/json"
"fmt"
"time"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/log/tag"
)
type RBACProvider interface {
GetUserPermissions(ctx context.Context, userID string) ([]string, error)
GetUserRoles(ctx context.Context, userID string) ([]string, error)
GetRolePermissions(ctx context.Context, role string) ([]string, error)
ValidateUserAccess(ctx context.Context, userID, namespace, action string) (bool, error)
}
type Role struct {
Name string `json:"name"`
Description string `json:"description"`
Permissions []string `json:"permissions"`
Namespaces []string `json:"namespaces"`
}
type User struct {
ID string `json:"id"`
Email string `json:"email"`
Roles []string `json:"roles"`
Namespaces []string `json:"namespaces"`
Active bool `json:"active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CachedRBACProvider struct {
storage StorageProvider
cache CacheProvider
logger log.Logger
}
func NewCachedRBACProvider(storage StorageProvider, cache CacheProvider, logger log.Logger) RBACProvider {
return &CachedRBACProvider{
storage: storage,
cache: cache,
logger: logger,
}
}
func (p *CachedRBACProvider) GetUserPermissions(ctx context.Context, userID string) ([]string, error) {
// Check cache first
cacheKey := fmt.Sprintf("user_permissions:%s", userID)
if cached, err := p.cache.Get(ctx, cacheKey); err == nil {
var permissions []string
if err := json.Unmarshal(cached, &permissions); err == nil {
return permissions, nil
}
}
// Get user roles
roles, err := p.GetUserRoles(ctx, userID)
if err != nil {
return nil, fmt.Errorf("failed to get user roles: %w", err)
}
// Aggregate permissions from all roles
permissionSet := make(map[string]bool)
for _, role := range roles {
rolePerms, err := p.GetRolePermissions(ctx, role)
if err != nil {
p.logger.Warn("Failed to get role permissions",
tag.Value("role", role),
tag.Error(err))
continue
}
for _, perm := range rolePerms {
permissionSet[perm] = true
}
}
// Convert set to slice
permissions := make([]string, 0, len(permissionSet))
for perm := range permissionSet {
permissions = append(permissions, perm)
}
// Cache the result
if data, err := json.Marshal(permissions); err == nil {
p.cache.Set(ctx, cacheKey, data, 5*time.Minute)
}
return permissions, nil
}
func (p *CachedRBACProvider) GetUserRoles(ctx context.Context, userID string) ([]string, error) {
user, err := p.storage.GetUser(ctx, userID)
if err != nil {
return nil, fmt.Errorf("failed to get user: %w", err)
}
if !user.Active {
return nil, fmt.Errorf("user account is inactive")
}
return user.Roles, nil
}
func (p *CachedRBACProvider) GetRolePermissions(ctx context.Context, roleName string) ([]string, error) {
// Check cache first
cacheKey := fmt.Sprintf("role_permissions:%s", roleName)
if cached, err := p.cache.Get(ctx, cacheKey); err == nil {
var permissions []string
if err := json.Unmarshal(cached, &permissions); err == nil {
return permissions, nil
}
}
role, err := p.storage.GetRole(ctx, roleName)
if err != nil {
return nil, fmt.Errorf("failed to get role: %w", err)
}
// Cache the result
if data, err := json.Marshal(role.Permissions); err == nil {
p.cache.Set(ctx, cacheKey, data, 10*time.Minute)
}
return role.Permissions, nil
}
func (p *CachedRBACProvider) ValidateUserAccess(ctx context.Context, userID, namespace, action string) (bool, error) {
permissions, err := p.GetUserPermissions(ctx, userID)
if err != nil {
return false, err
}
// Check if user has admin permission
for _, perm := range permissions {
if perm == "temporal:admin" {
return true, nil
}
}
// Check namespace-specific permissions
requiredPerm := fmt.Sprintf("temporal:%s:%s", namespace, action)
wildcardPerm := fmt.Sprintf("temporal:%s:*", namespace)
globalPerm := fmt.Sprintf("temporal:*:%s", action)
for _, perm := range permissions {
if perm == requiredPerm || perm == wildcardPerm || perm == globalPerm {
return true, nil
}
}
return false, nil
}
3. Default Roles Configuration¶
Predefined Roles¶
# config/rbac-roles.yaml
roles:
- name: "temporal-admin"
description: "Full administrative access to Temporal cluster"
permissions:
- "temporal:admin"
namespaces:
- "*"
- name: "temporal-developer"
description: "Developer access for workflow development and testing"
permissions:
- "temporal:workflow:start"
- "temporal:workflow:terminate"
- "temporal:workflow:read"
- "temporal:workflow:list"
- "temporal:workflow:history"
- "temporal:workflow:signal"
- "temporal:workflow:query"
- "temporal:activity:*"
- "temporal:schedule:read"
- "temporal:schedule:list"
- "temporal:namespace:read"
namespaces:
- "development"
- "testing"
- name: "temporal-operator"
description: "Operations team access for monitoring and maintenance"
permissions:
- "temporal:workflow:read"
- "temporal:workflow:list"
- "temporal:workflow:history"
- "temporal:workflow:terminate"
- "temporal:activity:read"
- "temporal:activity:list"
- "temporal:schedule:*"
- "temporal:namespace:read"
- "temporal:namespace:list"
- "temporal:cluster:read"
namespaces:
- "*"
- name: "temporal-viewer"
description: "Read-only access for monitoring and observability"
permissions:
- "temporal:workflow:read"
- "temporal:workflow:list"
- "temporal:workflow:history"
- "temporal:activity:read"
- "temporal:activity:list"
- "temporal:schedule:read"
- "temporal:schedule:list"
- "temporal:namespace:read"
- "temporal:namespace:list"
namespaces:
- "*"
- name: "temporal-service"
description: "Service account access for automated systems"
permissions:
- "temporal:workflow:start"
- "temporal:workflow:signal"
- "temporal:workflow:query"
- "temporal:activity:*"
namespaces:
- "production"
- "staging"
users:
- id: "admin@company.com"
email: "admin@company.com"
roles:
- "temporal-admin"
namespaces:
- "*"
active: true
- id: "developer@company.com"
email: "developer@company.com"
roles:
- "temporal-developer"
namespaces:
- "development"
- "testing"
active: true
- id: "ops@company.com"
email: "ops@company.com"
roles:
- "temporal-operator"
namespaces:
- "*"
active: true
Kubernetes Integration¶
1. Service Account Configuration¶
# k8s/rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: temporal-server
namespace: temporal
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT:role/temporal-server-role
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: temporal-server
rules:
- apiGroups: [""]
resources: ["secrets", "configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: temporal-server
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: temporal-server
subjects:
- kind: ServiceAccount
name: temporal-server
namespace: temporal
2. Secret Management Integration¶
# k8s/external-secrets.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: temporal-auth-secrets
namespace: temporal
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: temporal-auth-secrets
creationPolicy: Owner
data:
- secretKey: jwt-signing-key
remoteRef:
key: temporal/auth
property: jwt_signing_key
- secretKey: oidc-client-secret
remoteRef:
key: temporal/auth
property: oidc_client_secret
- secretKey: api-key-secret
remoteRef:
key: temporal/auth
property: api_key_secret
This comprehensive authentication and authorization guide provides enterprise-grade security patterns for Temporal.io deployments with support for multiple authentication methods, fine-grained authorization, and secure integration with modern identity providers.