Skip to content

Commit abcbfc9

Browse files
authored
Add mTLS sample. (#35)
Closes #7
1 parent 541f934 commit abcbfc9

7 files changed

Lines changed: 304 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Prerequisites:
2121
<!-- Keep this list in alphabetical order -->
2222
* [activity_simple](activity_simple) - Simple workflow that calls two activities.
2323
* [activity_worker](activity_worker) - Use Ruby activities from a workflow in another language.
24+
* [client_mtls](client_mtls) - Demonstrates how to use mutual TLS (mTLS) authentication with the Temporal Ruby SDK.
2425
* [coinbase_ruby](coinbase_ruby) - Demonstrate interoperability with the
2526
[Coinbase Ruby SDK](https://github.com/coinbase/temporal-ruby).
2627
* [context_propagation](context_propagation) - Use interceptors to propagate thread/fiber local data from clients

client_mtls/README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Client mTLS Sample
2+
3+
This sample demonstrates how to use mutual TLS (mTLS) authentication with the Temporal Ruby SDK.
4+
5+
## Overview
6+
7+
mTLS (mutual Transport Layer Security) provides secure, encrypted communication where both client and server authenticate each other. This is one way to connect to Temporal Cloud, or to a self-hosted Temporal deployment that is secured with TLS.
8+
9+
The sample includes:
10+
11+
- A simple workflow that executes an activity
12+
- A worker and starter that accept certificate parameters for mTLS authentication
13+
- Command-line options to configure connection parameters
14+
15+
## Prerequisites
16+
17+
Before running this sample, you'll need:
18+
19+
1. A Temporal server with mTLS enabled
20+
2. TLS certificates:
21+
- Client certificate and private key
22+
- Server root CA certificate (optional, depending on your setup)
23+
24+
## Running the Sample
25+
26+
### 1. Start the Worker
27+
28+
```bash
29+
ruby worker.rb \
30+
--client-cert /path/to/client.pem \
31+
--client-key /path/to/client.key \
32+
[--server-root-ca-cert /path/to/ca.pem] \
33+
[--target-host your-temporal-server:7233] \
34+
[--namespace your-namespace] \
35+
[--task-queue custom-task-queue]
36+
```
37+
38+
### 2. Execute the Workflow
39+
40+
In a separate terminal:
41+
42+
```bash
43+
ruby starter.rb \
44+
--client-cert /path/to/client.pem \
45+
--client-key /path/to/client.key \
46+
[--server-root-ca-cert /path/to/ca.pem] \
47+
[--target-host your-temporal-server:7233] \
48+
[--namespace your-namespace] \
49+
[--task-queue custom-task-queue]
50+
```
51+
52+
## Common Configurations
53+
54+
### Temporal Cloud with mTLS
55+
56+
When connecting to Temporal Cloud:
57+
58+
- **Address**: Use the mTLS endpoint from Temporal Cloud (e.g., `namespace.tmprl.cloud:7233`)
59+
- **Namespace**: Include the account identifier suffix (e.g., `my-namespace.abc123`)
60+
- **Server Root CA Certificate**: Not typically needed as Temporal Cloud uses well-known Root CAs
61+
62+
### Self-Hosted Temporal with mTLS
63+
64+
For a self-hosted Temporal cluster:
65+
66+
- **Server Root CA Certificate**: Required if your server uses a certificate signed by a private CA
67+
- You'll need both client certificate and key files
68+
69+
## Notes on Certificate Files
70+
71+
Certificate and key files should be in PEM format. The client certificate file may include the full certificate chain if needed.
72+
73+
## Troubleshooting
74+
75+
- If you see TLS handshake errors, verify your certificate paths are correct
76+
- Make sure certificates haven't expired
77+
- For Temporal Cloud, confirm you're using the correct endpoint and namespace

client_mtls/activities.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
require 'temporalio/activity'
4+
5+
module ClientMtls
6+
module Activities
7+
# Simple activity definition following SDK patterns
8+
class ComposeGreeting < Temporalio::Activity::Definition
9+
def execute(greeting, name)
10+
"#{greeting}, #{name}!"
11+
end
12+
end
13+
end
14+
end

client_mtls/greeting_workflow.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'activities'
4+
require 'temporalio/workflow'
5+
6+
module ClientMtls
7+
class GreetingWorkflow < Temporalio::Workflow::Definition
8+
def execute(name)
9+
# Execute activity and return result
10+
Temporalio::Workflow.execute_activity(
11+
Activities::ComposeGreeting,
12+
'Hello',
13+
name,
14+
start_to_close_timeout: 10
15+
)
16+
end
17+
end
18+
end

client_mtls/starter.rb

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'greeting_workflow'
4+
require 'optparse'
5+
require 'temporalio/client'
6+
7+
# Define options parser to handle certificate paths and other parameters
8+
options = {
9+
target_host: 'localhost:7233',
10+
namespace: 'default',
11+
task_queue: 'mtls-task-queue',
12+
server_root_ca_cert: nil,
13+
client_cert: nil,
14+
client_key: nil
15+
}
16+
17+
OptionParser.new do |opts|
18+
opts.banner = 'Usage: starter.rb [options]'
19+
20+
opts.on('--target-host HOST', 'Host:port for the server (default: localhost:7233)') do |v|
21+
options[:target_host] = v
22+
end
23+
24+
opts.on('--namespace NAMESPACE', 'Namespace to use (default: default)') do |v|
25+
options[:namespace] = v
26+
end
27+
28+
opts.on('--task-queue QUEUE', 'Task queue to use (default: mtls-task-queue)') do |v|
29+
options[:task_queue] = v
30+
end
31+
32+
opts.on('--server-root-ca-cert PATH', 'Path to the server root CA certificate') do |v|
33+
options[:server_root_ca_cert] = v
34+
end
35+
36+
opts.on('--client-cert PATH', 'Path to the client certificate (required for mTLS)') do |v|
37+
options[:client_cert] = v
38+
end
39+
40+
opts.on('--client-key PATH', 'Path to the client key (required for mTLS)') do |v|
41+
options[:client_key] = v
42+
end
43+
end.parse!
44+
45+
# Check for required certificates for mTLS
46+
unless options[:client_cert] && options[:client_key]
47+
puts 'Error: Client certificate and key are required for mTLS'
48+
puts 'Usage: ruby starter.rb --client-cert PATH --client-key PATH'
49+
exit 1
50+
end
51+
52+
puts "Connecting to Temporal Server at #{options[:target_host]} with mTLS..."
53+
puts "Using namespace: #{options[:namespace]}"
54+
55+
# Connect to Temporal server
56+
client = Temporalio::Client.connect(
57+
options[:target_host],
58+
options[:namespace],
59+
tls: Temporalio::Client::Connection::TLSOptions.new(
60+
client_cert: File.read(options[:client_cert]),
61+
client_private_key: File.read(options[:client_key]),
62+
server_root_ca_cert: options[:server_root_ca_cert] && File.read(options[:server_root_ca_cert])
63+
)
64+
)
65+
66+
# Execute the workflow
67+
puts 'Starting workflow with mTLS...'
68+
handle = client.start_workflow(
69+
ClientMtls::GreetingWorkflow,
70+
'World',
71+
id: "mtls-workflow-#{Time.now.to_i}",
72+
task_queue: options[:task_queue]
73+
)
74+
75+
puts "Started workflow. WorkflowID: #{handle.id}"
76+
77+
# Wait for workflow completion
78+
result = handle.result
79+
puts "Workflow completed with result: #{result}"

client_mtls/worker.rb

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'activities'
4+
require_relative 'greeting_workflow'
5+
require 'optparse'
6+
require 'temporalio/client'
7+
require 'temporalio/worker'
8+
9+
# Define options parser to handle certificate paths and other parameters
10+
options = {
11+
target_host: 'localhost:7233',
12+
namespace: 'default',
13+
task_queue: 'mtls-task-queue',
14+
server_root_ca_cert: nil,
15+
client_cert: nil,
16+
client_key: nil
17+
}
18+
19+
OptionParser.new do |opts|
20+
opts.banner = 'Usage: worker.rb [options]'
21+
22+
opts.on('--target-host HOST', 'Host:port for the server (default: localhost:7233)') do |v|
23+
options[:target_host] = v
24+
end
25+
26+
opts.on('--namespace NAMESPACE', 'Namespace to use (default: default)') do |v|
27+
options[:namespace] = v
28+
end
29+
30+
opts.on('--task-queue QUEUE', 'Task queue to use (default: mtls-task-queue)') do |v|
31+
options[:task_queue] = v
32+
end
33+
34+
opts.on('--server-root-ca-cert PATH', 'Path to the server root CA certificate') do |v|
35+
options[:server_root_ca_cert] = v
36+
end
37+
38+
opts.on('--client-cert PATH', 'Path to the client certificate (required for mTLS)') do |v|
39+
options[:client_cert] = v
40+
end
41+
42+
opts.on('--client-key PATH', 'Path to the client key (required for mTLS)') do |v|
43+
options[:client_key] = v
44+
end
45+
end.parse!
46+
47+
# Check for required certificates for mTLS
48+
if options[:client_cert].nil? || options[:client_key].nil?
49+
puts 'Error: Client certificate and key are required for mTLS'
50+
puts 'Usage: ruby worker.rb --client-cert PATH --client-key PATH'
51+
exit 1
52+
end
53+
54+
puts "Connecting to Temporal Server at #{options[:target_host]} with mTLS..."
55+
puts "Using namespace: #{options[:namespace]}"
56+
57+
# Create client with mTLS configuration
58+
tls_options = Temporalio::Client::Connection::TLSOptions.new(
59+
client_cert: File.read(options[:client_cert]),
60+
client_private_key: File.read(options[:client_key])
61+
)
62+
63+
# Add server root CA cert if provided
64+
if options[:server_root_ca_cert]
65+
tls_options = tls_options.with(server_root_ca_cert: File.read(options[:server_root_ca_cert]))
66+
end
67+
68+
# Connect to Temporal server
69+
client = Temporalio::Client.connect(
70+
options[:target_host],
71+
options[:namespace],
72+
tls: tls_options
73+
)
74+
75+
# Start worker with activities and workflows registered
76+
puts 'Starting worker connected with mTLS...'
77+
puts "Task queue: #{options[:task_queue]}"
78+
Temporalio::Worker.new(
79+
client:,
80+
task_queue: options[:task_queue],
81+
workflows: [ClientMtls::GreetingWorkflow],
82+
activities: [ClientMtls::Activities::ComposeGreeting]
83+
).run(shutdown_signals: ['SIGINT'])
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# frozen_string_literal: true
2+
3+
require 'test'
4+
require 'client_mtls/greeting_workflow'
5+
require 'securerandom'
6+
require 'temporalio/testing'
7+
require 'temporalio/worker'
8+
9+
module ClientMtls
10+
class GreetingWorkflowTest < Test
11+
def test_workflow
12+
# Run test server until completion of the block
13+
Temporalio::Testing::WorkflowEnvironment.start_local do |env|
14+
# Run worker until completion of the block
15+
worker = Temporalio::Worker.new(
16+
client: env.client,
17+
task_queue: "tq-#{SecureRandom.uuid}",
18+
activities: [Activities::ComposeGreeting],
19+
workflows: [GreetingWorkflow]
20+
)
21+
worker.run do
22+
# Run workflow
23+
assert_equal(
24+
'Hello, World!',
25+
env.client.execute_workflow(GreetingWorkflow, 'World', id: "wf-#{SecureRandom.uuid}",
26+
task_queue: worker.task_queue)
27+
)
28+
end
29+
end
30+
end
31+
end
32+
end

0 commit comments

Comments
 (0)