Skip to content

Commit

Permalink
Add dynamic mtls samples (#349)
Browse files Browse the repository at this point in the history
  • Loading branch information
Quinn-With-Two-Ns authored Apr 30, 2024
1 parent ea23602 commit 065f233
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Each sample demonstrates one feature of the SDK, together with tests.
- [**Basic mTLS hello world**](./helloworldmtls): Simple example of a
Workflow Definition and an Activity Definition using mTLS like Temporal Cloud.

- [**Dynamic mTLS hello world**](./dynamicmtls): Simple example showing how to refresh mTLS credentials. This allows for credentials to be refreshed without restarting the worker.

### API demonstrations

- **Async activity completion**: Example of
Expand Down
19 changes: 19 additions & 0 deletions dynamicmtls/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
This sample shows how to connect a client to Temporal using mtls where the certificates are dynamically loaded. This allows the credentials to be replaced without restarting the worker.

### Steps to run this sample:
1) Configure a [Temporal Server](https://github.com/temporalio/samples-go/tree/main/#how-to-use) (such as Temporal Cloud) with mTLS.

2) Run the following command to start the worker
```
go run ./dynamicmtls/worker -target-host my.namespace.tmprl.cloud:7233 -namespace my.namespace -client-cert path/to/cert.pem -client-key path/to/key.pem
```
3) Run the following command to start the example
```
go run ./dynamicmtls/starter -target-host my.namespace.tmprl.cloud:7233 -namespace my.namespace -client-cert path/to/cert.pem -client-key path/to/key.pem
```

Note:

If the server uses self-signed certificates and does not have the SAN set to the actual host, pass one of the following two options when starting the worker or the example above:
1. `-server-name` and provide the common name contained in the self-signed server certificate
2. `-insecure-skip-verify` which disables certificate and host name validation
63 changes: 63 additions & 0 deletions dynamicmtls/dynamicmtls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package dynamicmtls

import (
"crypto/tls"
"crypto/x509"
"flag"
"fmt"
"os"

"go.temporal.io/sdk/client"
)

// ParseClientOptionFlags parses the given arguments into client options. In
// some cases a failure will be returned as an error, in others the process may
// exit with help info.
func ParseClientOptionFlags(args []string) (client.Options, error) {
// Parse args
set := flag.NewFlagSet("hello-world-mtls", flag.ExitOnError)
targetHost := set.String("target-host", "localhost:7233", "Host:port for the server")
namespace := set.String("namespace", "default", "Namespace for the server")
serverRootCACert := set.String("server-root-ca-cert", "", "Optional path to root server CA cert")
clientCert := set.String("client-cert", "", "Required path to client cert, will be dynamically loaded when server requests a certificate")
clientKey := set.String("client-key", "", "Required path to client key, will be dynamically loaded when server requests a certificate")
serverName := set.String("server-name", "", "Server name to use for verifying the server's certificate")
insecureSkipVerify := set.Bool("insecure-skip-verify", false, "Skip verification of the server's certificate and host name")
if err := set.Parse(args); err != nil {
return client.Options{}, fmt.Errorf("failed parsing args: %w", err)
} else if *clientCert == "" || *clientKey == "" {
return client.Options{}, fmt.Errorf("-client-cert and -client-key are required")
}

// Load server CA if given
var serverCAPool *x509.CertPool
if *serverRootCACert != "" {
serverCAPool = x509.NewCertPool()
b, err := os.ReadFile(*serverRootCACert)
if err != nil {
return client.Options{}, fmt.Errorf("failed reading server CA: %w", err)
} else if !serverCAPool.AppendCertsFromPEM(b) {
return client.Options{}, fmt.Errorf("server CA PEM file invalid")
}
}

return client.Options{
HostPort: *targetHost,
Namespace: *namespace,
ConnectionOptions: client.ConnectionOptions{
TLS: &tls.Config{
GetClientCertificate: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
cert, err := tls.LoadX509KeyPair(*clientCert, *clientKey)
if err != nil {
return nil, fmt.Errorf("failed loading client cert and key: %w", err)
}
return &cert, nil

},
RootCAs: serverCAPool,
ServerName: *serverName,
InsecureSkipVerify: *insecureSkipVerify,
},
},
}, nil
}
44 changes: 44 additions & 0 deletions dynamicmtls/starter/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package main

import (
"context"
"log"
"os"

"github.com/temporalio/samples-go/dynamicmtls"
"github.com/temporalio/samples-go/helloworld"
"go.temporal.io/sdk/client"
)

func main() {
// The client is a heavyweight object that should be created once per process.
clientOptions, err := dynamicmtls.ParseClientOptionFlags(os.Args[1:])
if err != nil {
log.Fatalf("Invalid arguments: %v", err)
}
c, err := client.Dial(clientOptions)
if err != nil {
log.Fatalln("Unable to create client", err)
}
defer c.Close()

workflowOptions := client.StartWorkflowOptions{
ID: "hello_world_workflowID",
TaskQueue: "hello-world-mtls",
}

we, err := c.ExecuteWorkflow(context.Background(), workflowOptions, helloworld.Workflow, "Temporal")
if err != nil {
log.Fatalln("Unable to execute workflow", err)
}

log.Println("Started workflow", "WorkflowID", we.GetID(), "RunID", we.GetRunID())

// Synchronously wait for the workflow completion.
var result string
err = we.Get(context.Background(), &result)
if err != nil {
log.Fatalln("Unable get workflow result", err)
}
log.Println("Workflow result:", result)
}
34 changes: 34 additions & 0 deletions dynamicmtls/worker/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package main

import (
"log"
"os"

"github.com/temporalio/samples-go/dynamicmtls"
"github.com/temporalio/samples-go/helloworld"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/worker"
)

func main() {
// The client and worker are heavyweight objects that should be created once per process.
clientOptions, err := dynamicmtls.ParseClientOptionFlags(os.Args[1:])
if err != nil {
log.Fatalf("Invalid arguments: %v", err)
}
c, err := client.Dial(clientOptions)
if err != nil {
log.Fatalln("Unable to create client", err)
}
defer c.Close()

w := worker.New(c, "hello-world-mtls", worker.Options{})

w.RegisterWorkflow(helloworld.Workflow)
w.RegisterActivity(helloworld.Activity)

err = w.Run(worker.InterruptCh())
if err != nil {
log.Fatalln("Unable to start worker", err)
}
}

0 comments on commit 065f233

Please sign in to comment.