Skip to content

Commit

Permalink
Merge pull request #26 from caffalaughrey/mtls
Browse files Browse the repository at this point in the history
mTLS support
  • Loading branch information
ganievs authored Mar 19, 2024
2 parents 3ddc0d1 + 1141457 commit 24c0b50
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 8 deletions.
12 changes: 12 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,16 @@ provider "temporal" {
- `host` (String) The Temporal server host.
- `insecure` (Boolean) Use insecure connection
- `port` (String) The Temporal server port.
- `tls` (Block, Optional) TLS Configuration for the Temporal server (see [below for nested schema](#nestedblock--tls))
- `token_url` (String) Oauth2 server URL to fetch token from

<a id="nestedblock--tls"></a>
### Nested Schema for `tls`

Optional:

- `ca` (String) CA certificates
- `cert` (String) Client certificate PEM
- `cert_reload_time` (Number) Certificate reload time
- `key` (String) Private key PEM
- `server_name` (String) Used to verify the hostname and included in handshake
27 changes: 27 additions & 0 deletions examples/provider/tls/tls.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
terraform {
required_providers {
temporal = {
source = "platacard/temporal"
}
}
}

provider "temporal" {
host = "127.0.0.1"
port = "7233"

# Add certs for mTLS auth.
tls {
cert = sensitive(file("path/to/cert.pem"))
key = sensitive(file("path/to/key.pem"))
ca = sensitive(file("path/to/cacerts.pem"))
server_name = "server-name"
}
}

# Manage an example namespace.
resource "temporal_namespace" "example" {
name = "example"
description = "This is example namespace"
owner_email = "[email protected]"
}
9 changes: 5 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gogo/status v1.1.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golangci/golangci-lint v1.56.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
Expand Down Expand Up @@ -74,15 +75,15 @@ require (
github.com/yuin/goldmark-meta v1.1.0 // indirect
github.com/zclconf/go-cty v1.14.1 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect
golang.org/x/mod v0.15.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golangci/golangci-lint v1.56.2 h1:dgQzlWHgNbCqJjuxRJhFEnHDVrrjuTGQHJ3RIZMpp/o=
github.com/golangci/golangci-lint v1.56.2/go.mod h1:7CfNO675+EY7j84jihO4iAqDQ80s3HCjcc5M6B7SlZQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
Expand Down Expand Up @@ -229,6 +231,8 @@ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1m
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 h1:EDuYyU/MkFXllv9QF9819VlI9a4tzGuCbhG0ExK9o1U=
golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM=
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
Expand All @@ -238,6 +242,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down Expand Up @@ -288,6 +294,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
Expand All @@ -314,6 +322,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down Expand Up @@ -352,6 +361,8 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
148 changes: 144 additions & 4 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ package provider
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"strconv"
Expand Down Expand Up @@ -48,6 +50,7 @@ type temporalProviderModel struct {
TokenURL types.String `tfsdk:"token_url"`
Audience types.String `tfsdk:"audience"`
Insecure types.Bool `tfsdk:"insecure"`
TLS types.Object `tfsdk:"tls"`
}

// Metadata assigns the provider's name and version.
Expand All @@ -59,6 +62,33 @@ func (p *TemporalProvider) Metadata(ctx context.Context, req provider.MetadataRe
// Schema defines the configuration schema for the provider.
func (p *TemporalProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Blocks: map[string]schema.Block{
"tls": schema.SingleNestedBlock{
Description: "TLS Configuration for the Temporal server",
Attributes: map[string]schema.Attribute{
"cert": schema.StringAttribute{
Optional: true,
Description: "Client certificate PEM",
},
"key": schema.StringAttribute{
Optional: true,
Description: "Private key PEM",
},
"ca": schema.StringAttribute{
Optional: true,
Description: "CA certificates",
},
"cert_reload_time": schema.Int64Attribute{
Optional: true,
Description: "Certificate reload time",
},
"server_name": schema.StringAttribute{
Optional: true,
Description: "Used to verify the hostname and included in handshake",
},
},
},
},
Attributes: map[string]schema.Attribute{
"host": schema.StringAttribute{
Description: "The Temporal server host.",
Expand Down Expand Up @@ -231,6 +261,26 @@ func (p *TemporalProvider) Configure(ctx context.Context, req provider.Configure
insecure = config.Insecure.ValueBool()
}

var (
certString string
keyString string
caCerts string
serverName string
)

var useTLS bool = false

if !config.TLS.IsNull() {
useTLS = true

tlsAttributes := config.TLS.Attributes()

certString = normalizeCert(tlsAttributes["cert"].String())
keyString = normalizeCert(tlsAttributes["key"].String())
caCerts = normalizeCert(tlsAttributes["ca"].String())
serverName = stripQuotes(tlsAttributes["server_name"].String())
}

// If host and port not set use defaults
if host == "" {
host = "127.0.0.1"
Expand All @@ -250,7 +300,8 @@ func (p *TemporalProvider) Configure(ctx context.Context, req provider.Configure
endpoint := strings.Join([]string{host, port}, ":")

tflog.Debug(ctx, "Creating Temporal client")
client, err := CreateGRPCClient(clientID, clientSecret, tokenURL, audience, endpoint, insecure)
tflog.Debug(ctx, "Use TLS? "+strconv.FormatBool(useTLS))
client, err := CreateGRPCClient(clientID, clientSecret, tokenURL, audience, endpoint, insecure, useTLS, certString, keyString, caCerts, serverName)
if err != nil {
resp.Diagnostics.AddError(
"Unable to Create Temporal API Client",
Expand Down Expand Up @@ -320,20 +371,54 @@ func CreateAuthenticatedClient(endpoint string, token *oauth2.Token, credentials
))
}

// CreateSecureClient creates a gRPC client using mTLS without OAuth authentication.
func CreateSecureClient(endpoint string, credentials grpcCreds.TransportCredentials) (*grpc.ClientConn, error) {
return grpc.Dial(endpoint, grpc.WithTransportCredentials(credentials))
}

// CreateInsecureClient creates a gRPC client without any authentication.
func CreateInsecureClient(endpoint string, credentials grpcCreds.TransportCredentials) (*grpc.ClientConn, error) {
return grpc.Dial(endpoint, grpc.WithTransportCredentials(credentials))
}

// CreateGRPCClient decides which gRPC client to create based on clientID.
func CreateGRPCClient(clientID, clientSecret, tokenURL, audience, endpoint string, insecure bool) (*grpc.ClientConn, error) {
func CreateGRPCClient(clientID, clientSecret, tokenURL, audience, endpoint string, insecure bool, useTLS bool, certString string, keyString string, caCerts string, serverName string) (*grpc.ClientConn, error) {
var credentials grpcCreds.TransportCredentials

switch insecure {
case true:
credentials = grpcInsec.NewCredentials()
case false:
config := &tls.Config{}
credentials = grpcCreds.NewTLS(config)
switch useTLS {
case true:
// Parse the certificate from PEM format
cert, err := getCertificate(certString)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %v", err)
}

// Parse the private key from PEM format
key, err := getPrivateKey([]byte(keyString))
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %v", err)
}

// Create a tls.Certificate from the parsed certificate and private key
tlsCert := tls.Certificate{
Certificate: [][]byte{cert.Raw},
PrivateKey: key,
}

config := &tls.Config{
Certificates: []tls.Certificate{tlsCert},
RootCAs: getCA([]byte(caCerts)),
ServerName: serverName,
}
credentials = grpcCreds.NewTLS(config)
case false:
config := &tls.Config{}
credentials = grpcCreds.NewTLS(config)
}
}

if clientID != "" {
Expand All @@ -343,11 +428,56 @@ func CreateGRPCClient(clientID, clientSecret, tokenURL, audience, endpoint strin
}

return CreateAuthenticatedClient(endpoint, token, credentials)
} else if useTLS {
return CreateSecureClient(endpoint, credentials)
}

return CreateInsecureClient(endpoint, credentials)
}

// Function to parse the public key from PEM format.
func getCertificate(certPEM string) (*x509.Certificate, error) {
block, _ := pem.Decode([]byte(certPEM))

if block == nil {
return nil, fmt.Errorf("failed to decode cert PEM. certPEM bytes:\n%v", []byte(certPEM))
}

cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %v", err)
}

return cert, nil
}

// Function to parse the private key from PEM format.
func getPrivateKey(keyPEM []byte) (interface{}, error) {
block, _ := pem.Decode(keyPEM)
if block == nil {
return nil, fmt.Errorf("failed to decode private key PEM")
}

key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
// If parsing as PKCS1 fails, try parsing as PKCS8
pkcs8Key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %v", err)
}
return pkcs8Key, nil
}

return key, nil
}

// Function to get CA certificates.
func getCA(caCerts []byte) *x509.CertPool {
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCerts)
return caCertPool
}

func getBoolEnv(key string) (result bool, err error) {
val, exist := os.LookupEnv(key)
if !exist {
Expand All @@ -359,3 +489,13 @@ func getBoolEnv(key string) (result bool, err error) {
}
return result, err
}

// Helper function to strip quotes and remove line return escaping from cert.
func normalizeCert(value string) string {
return strings.Replace(stripQuotes(value), "\\n", "\n", -1)
}

// Helper function to strip quotes from string.
func stripQuotes(value string) string {
return strings.TrimPrefix(strings.TrimSuffix(value, "\""), "\"")
}

0 comments on commit 24c0b50

Please sign in to comment.