From 9efbd3dfaeae5a13cc3120801f4ff682568d4dd3 Mon Sep 17 00:00:00 2001 From: Quinn Klassen Date: Sat, 27 Apr 2024 16:16:34 -0700 Subject: [PATCH] Add dynamic mtls samples --- README.md | 2 ++ dynamicmtls/README.md | 19 +++++++++++ dynamicmtls/dynamicmtls.go | 63 +++++++++++++++++++++++++++++++++++++ dynamicmtls/starter/main.go | 44 ++++++++++++++++++++++++++ dynamicmtls/worker/main.go | 34 ++++++++++++++++++++ 5 files changed, 162 insertions(+) create mode 100644 dynamicmtls/README.md create mode 100644 dynamicmtls/dynamicmtls.go create mode 100644 dynamicmtls/starter/main.go create mode 100644 dynamicmtls/worker/main.go diff --git a/README.md b/README.md index 6899607b..d2af814e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/dynamicmtls/README.md b/dynamicmtls/README.md new file mode 100644 index 00000000..53a93350 --- /dev/null +++ b/dynamicmtls/README.md @@ -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 diff --git a/dynamicmtls/dynamicmtls.go b/dynamicmtls/dynamicmtls.go new file mode 100644 index 00000000..b3fc34e8 --- /dev/null +++ b/dynamicmtls/dynamicmtls.go @@ -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 +} diff --git a/dynamicmtls/starter/main.go b/dynamicmtls/starter/main.go new file mode 100644 index 00000000..430c1157 --- /dev/null +++ b/dynamicmtls/starter/main.go @@ -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) +} diff --git a/dynamicmtls/worker/main.go b/dynamicmtls/worker/main.go new file mode 100644 index 00000000..76e7456d --- /dev/null +++ b/dynamicmtls/worker/main.go @@ -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) + } +}