Skip to content

Commit 75e5ae2

Browse files
scr-oathSheridan C Rawlinskmoe
authored
[fixes 31700] Add mTLS support for http backend by way of client cert & key, as well as enterprise cacert. (#31699)
* Add mTLS support for http backend by way of client cert & key, as well as enterprise cacert. * Fix style. * Skip cert validation to be sure error is related to missing client cert; not untrusted server cert. * Remove misplaced err check. * Fix the size of test using http backend. * Just for correctness, include all certs in the pem encoded cert - sometimes certs come with a chain of their signers. * Adjusted names as recommended in PR comments. * Adjusted names to be full-length and more descriptive. * Added full-fledged testing with mTLS http server * Fix goimports. * Fix the names of the backend config. * Exclusive lock for write and delete. * Revert "Fix goimports." This reverts commit 7d40f60. * goimports just for server test. * Added the go:generation for the mock. * Move the TLS configuration out to make it more readable - don't replace the HTTPClient as the retryablehttp already creates one - just configure its TLS. * Just switch the client/data params - felt more natural this way. * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/testdata/gencerts.sh Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * the location of the file name is not sensitive. * Added error if only one of client_certificate_pem and client_private_key_pem are set. * Remove testify from test cases; use t.Error* for assert and t.Fatal* for require. * Fixed import consistency * Just use default openssl. * Since file(...) is so trivial to use, changed the client cert, key, and ca cert to be the data. See also hashicorp/terraform-provider-http#211 Co-authored-by: Sheridan C Rawlins <scr@ouryahoo.com> Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com>
1 parent 7d2afaa commit 75e5ae2

File tree

15 files changed

+933
-26
lines changed

15 files changed

+933
-26
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ require (
169169
github.com/shopspring/decimal v1.3.1 // indirect
170170
github.com/spf13/cast v1.5.0 // indirect
171171
github.com/spf13/pflag v1.0.5 // indirect
172-
github.com/stretchr/objx v0.1.1 // indirect
172+
github.com/stretchr/objx v0.5.0 // indirect
173173
github.com/ulikunitz/xz v0.5.8 // indirect
174174
github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect
175175
github.com/vmihailenco/tagparser v0.1.1 // indirect

go.sum

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -595,14 +595,17 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
595595
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
596596
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
597597
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
598-
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
599-
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
598+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
599+
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
600+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
600601
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
601602
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
602603
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
603604
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
604605
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
605606
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
607+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
608+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
606609
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
607610
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.194/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
608611
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.232 h1:kwsWbh4rEw42ZDe9/812ebhbwNZxlQyZ2sTmxBOKhN4=

internal/backend/remote-state/http/backend.go

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ package http
33
import (
44
"context"
55
"crypto/tls"
6+
"crypto/x509"
7+
"errors"
68
"fmt"
79
"log"
810
"net/http"
911
"net/url"
1012
"time"
1113

12-
"github.com/hashicorp/go-cleanhttp"
1314
"github.com/hashicorp/go-retryablehttp"
15+
1416
"github.com/hashicorp/terraform/internal/backend"
1517
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
1618
"github.com/hashicorp/terraform/internal/logging"
@@ -93,6 +95,24 @@ func New() backend.Backend {
9395
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_WAIT_MAX", 30),
9496
Description: "The maximum time in seconds to wait between HTTP request attempts.",
9597
},
98+
"client_ca_certificate_pem": &schema.Schema{
99+
Type: schema.TypeString,
100+
Optional: true,
101+
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_CA_CERTIFICATE_PEM", ""),
102+
Description: "A PEM-encoded CA certificate chain used by the client to verify server certificates during TLS authentication.",
103+
},
104+
"client_certificate_pem": &schema.Schema{
105+
Type: schema.TypeString,
106+
Optional: true,
107+
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_CERTIFICATE_PEM", ""),
108+
Description: "A PEM-encoded certificate used by the server to verify the client during mutual TLS (mTLS) authentication.",
109+
},
110+
"client_private_key_pem": &schema.Schema{
111+
Type: schema.TypeString,
112+
Optional: true,
113+
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_PRIVATE_KEY_PEM", ""),
114+
Description: "A PEM-encoded private key, required if client_certificate_pem is specified.",
115+
},
96116
},
97117
}
98118

@@ -107,6 +127,50 @@ type Backend struct {
107127
client *httpClient
108128
}
109129

130+
// configureTLS configures TLS when needed; if there are no conditions requiring TLS, no change is made.
131+
func (b *Backend) configureTLS(client *retryablehttp.Client, data *schema.ResourceData) error {
132+
// If there are no conditions needing to configure TLS, leave the client untouched
133+
skipCertVerification := data.Get("skip_cert_verification").(bool)
134+
clientCACertificatePem := data.Get("client_ca_certificate_pem").(string)
135+
clientCertificatePem := data.Get("client_certificate_pem").(string)
136+
clientPrivateKeyPem := data.Get("client_private_key_pem").(string)
137+
if !skipCertVerification && clientCACertificatePem == "" && clientCertificatePem == "" && clientPrivateKeyPem == "" {
138+
return nil
139+
}
140+
if clientCertificatePem != "" && clientPrivateKeyPem == "" {
141+
return fmt.Errorf("client_certificate_pem is set but client_private_key_pem is not")
142+
}
143+
if clientPrivateKeyPem != "" && clientCertificatePem == "" {
144+
return fmt.Errorf("client_private_key_pem is set but client_certificate_pem is not")
145+
}
146+
147+
// TLS configuration is needed; create an object and configure it
148+
var tlsConfig tls.Config
149+
client.HTTPClient.Transport.(*http.Transport).TLSClientConfig = &tlsConfig
150+
151+
if skipCertVerification {
152+
// ignores TLS verification
153+
tlsConfig.InsecureSkipVerify = true
154+
}
155+
if clientCACertificatePem != "" {
156+
// trust servers based on a CA
157+
tlsConfig.RootCAs = x509.NewCertPool()
158+
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(clientCACertificatePem)) {
159+
return errors.New("failed to append certs")
160+
}
161+
}
162+
if clientCertificatePem != "" && clientPrivateKeyPem != "" {
163+
// attach a client certificate to the TLS handshake (aka mTLS)
164+
certificate, err := tls.X509KeyPair([]byte(clientCertificatePem), []byte(clientPrivateKeyPem))
165+
if err != nil {
166+
return fmt.Errorf("cannot load client certificate: %w", err)
167+
}
168+
tlsConfig.Certificates = []tls.Certificate{certificate}
169+
}
170+
171+
return nil
172+
}
173+
110174
func (b *Backend) configure(ctx context.Context) error {
111175
data := schema.FromContextBackendConfig(ctx)
112176

@@ -149,21 +213,14 @@ func (b *Backend) configure(ctx context.Context) error {
149213

150214
unlockMethod := data.Get("unlock_method").(string)
151215

152-
client := cleanhttp.DefaultPooledClient()
153-
154-
if data.Get("skip_cert_verification").(bool) {
155-
// ignores TLS verification
156-
client.Transport.(*http.Transport).TLSClientConfig = &tls.Config{
157-
InsecureSkipVerify: true,
158-
}
159-
}
160-
161216
rClient := retryablehttp.NewClient()
162-
rClient.HTTPClient = client
163217
rClient.RetryMax = data.Get("retry_max").(int)
164218
rClient.RetryWaitMin = time.Duration(data.Get("retry_wait_min").(int)) * time.Second
165219
rClient.RetryWaitMax = time.Duration(data.Get("retry_wait_max").(int)) * time.Second
166220
rClient.Logger = log.New(logging.LogOutput(), "", log.Flags())
221+
if err = b.configureTLS(rClient, data); err != nil {
222+
return err
223+
}
167224

168225
b.client = &httpClient{
169226
URL: updateURL,

internal/backend/remote-state/http/mock_server_test.go

Lines changed: 95 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)