Skip to content

Commit

Permalink
aws/session: Add support for client TLS certs on HTTP client (#3654)
Browse files Browse the repository at this point in the history
Adds support for the SDK to automatically modify the HTTP client to include TLS configuration of custom Client TLS certificate. This configuration can be provide via the environment variable or directly in code via the `session.Options` struct.

These options are compatible with the AWS_CA_BUNDLE configuration.

#### Environment variable configuration

Both `AWS_SDK_GO_CLIENT_TLS_CERT`, and `AWS_SDK_GO_CLIENT_TLS_KEY` must be provided together, and must point to valid PEM encoded file containing the certificate, and key respectively.

```
AWS_SDK_GO_CLIENT_TLS_CERT=$HOME/my_client_cert
AWS_SDK_GO_CLIENT_TLS_KEY=$HOME/my_client_key
```

#### In code configuration via session.Options

Alternative configuration is to specify the `ClientTLSCert` and `ClientTLSKey` fields on the `session.Options` struction. These are `io.Reader`s that provide the PEM encoded content for the certificate and key files.

```go
  sess, err := session.NewSessionWithOptions(session.Options{
      ClientTLSCert: myCertFile,
      ClientTLSKey: myKeyFile,
  })
```
  • Loading branch information
jasdel authored Dec 8, 2020
1 parent e13de64 commit 039c0ff
Show file tree
Hide file tree
Showing 12 changed files with 685 additions and 55 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
### SDK Features

### SDK Enhancements
* `aws/session`: Add support for client TLS certs on HTTP client ([#3654](https://github.com/aws/aws-sdk-go/pull/3654))
* Adds support for the SDK to automatically modify the HTTP client to include TLS configuration of custom Client TLS certificate.

### SDK Bugs
191 changes: 191 additions & 0 deletions aws/session/client_tls_cert_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// +build go1.9

package session

import (
"crypto/x509"
"io"
"net/http"
"os"
"strings"
"testing"
"time"

"github.com/aws/aws-sdk-go/awstesting"
)

func TestNewSession_WithClientTLSCert(t *testing.T) {
type testCase struct {
// Params
setup func(certFilename, keyFilename string) (Options, func(), error)
ExpectErr string
}

cases := map[string]testCase{
"env": {
setup: func(certFilename, keyFilename string) (Options, func(), error) {
os.Setenv(useClientTLSCert[0], certFilename)
os.Setenv(useClientTLSKey[0], keyFilename)
return Options{}, func() {}, nil
},
},
"env file not found": {
setup: func(certFilename, keyFilename string) (Options, func(), error) {
os.Setenv(useClientTLSCert[0], "some-cert-file-not-exists")
os.Setenv(useClientTLSKey[0], "some-key-file-not-exists")
return Options{}, func() {}, nil
},
ExpectErr: "LoadClientTLSCertError",
},
"env cert file only": {
setup: func(certFilename, keyFilename string) (Options, func(), error) {
os.Setenv(useClientTLSCert[0], certFilename)
return Options{}, func() {}, nil
},
ExpectErr: "must both be provided",
},
"env key file only": {
setup: func(certFilename, keyFilename string) (Options, func(), error) {
os.Setenv(useClientTLSKey[0], keyFilename)
return Options{}, func() {}, nil
},
ExpectErr: "must both be provided",
},

"session options": {
setup: func(certFilename, keyFilename string) (Options, func(), error) {
certFile, err := os.Open(certFilename)
if err != nil {
return Options{}, nil, err
}
keyFile, err := os.Open(keyFilename)
if err != nil {
return Options{}, nil, err
}

return Options{
ClientTLSCert: certFile,
ClientTLSKey: keyFile,
}, func() {
certFile.Close()
keyFile.Close()
}, nil
},
},
"session cert load error": {
setup: func(certFilename, keyFilename string) (Options, func(), error) {
certFile, err := os.Open(certFilename)
if err != nil {
return Options{}, nil, err
}
keyFile, err := os.Open(keyFilename)
if err != nil {
return Options{}, nil, err
}

stat, _ := certFile.Stat()
return Options{
ClientTLSCert: io.LimitReader(certFile, stat.Size()/2),
ClientTLSKey: keyFile,
}, func() {
certFile.Close()
keyFile.Close()
}, nil
},
ExpectErr: "unable to load x509 key pair",
},
"session key load error": {
setup: func(certFilename, keyFilename string) (Options, func(), error) {
certFile, err := os.Open(certFilename)
if err != nil {
return Options{}, nil, err
}
keyFile, err := os.Open(keyFilename)
if err != nil {
return Options{}, nil, err
}

stat, _ := keyFile.Stat()
return Options{
ClientTLSCert: certFile,
ClientTLSKey: io.LimitReader(keyFile, stat.Size()/2),
}, func() {
certFile.Close()
keyFile.Close()
}, nil
},
ExpectErr: "unable to load x509 key pair",
},
}

for name, c := range cases {
t.Run(name, func(t *testing.T) {
// Asserts
restoreEnvFn := initSessionTestEnv()
defer restoreEnvFn()

certFilename, keyFilename, err := awstesting.CreateClientTLSCertFiles()
if err != nil {
t.Fatalf("failed to create client certificate files, %v", err)
}
defer func() {
if err := awstesting.CleanupTLSBundleFiles(certFilename, keyFilename); err != nil {
t.Errorf("failed to cleanup client TLS cert files, %v", err)
}
}()

opts, cleanup, err := c.setup(certFilename, keyFilename)
if err != nil {
t.Fatalf("test case failed setup, %v", err)
}
if cleanup != nil {
defer cleanup()
}

server, err := awstesting.NewTLSClientCertServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}))
if err != nil {
t.Fatalf("failed to load session, %v", err)
}
server.StartTLS()
defer server.Close()

// Give server change to start
time.Sleep(time.Second)

// Load SDK session with options configured.
sess, err := NewSessionWithOptions(opts)
if len(c.ExpectErr) != 0 {
if err == nil {
t.Fatalf("expect error, got none")
}
if e, a := c.ExpectErr, err.Error(); !strings.Contains(a, e) {
t.Fatalf("expect error to contain %v, got %v", e, a)
}
return
}
if err != nil {
t.Fatalf("expect no error, got %v", err)
}

// Clients need to add ca bundle for test service.
p := x509.NewCertPool()
p.AddCert(server.Certificate())
client := sess.Config.HTTPClient
client.Transport.(*http.Transport).TLSClientConfig.RootCAs = p

// Send request
req, _ := http.NewRequest("GET", server.URL, nil)
resp, err := client.Do(req)
if err != nil {
t.Fatalf("failed to send request, %v", err)
}

if e, a := 200, resp.StatusCode; e != a {
t.Errorf("expect %v status code, got %v", e, a)
}
})
}
}
27 changes: 27 additions & 0 deletions aws/session/custom_transport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// +build go1.13

package session

import (
"net"
"net/http"
"time"
)

// Transport that should be used when a custom CA bundle is specified with the
// SDK.
func getCustomTransport() *http.Transport {
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// +build go1.7
// +build !go1.13,go1.7

package session

Expand All @@ -10,7 +10,7 @@ import (

// Transport that should be used when a custom CA bundle is specified with the
// SDK.
func getCABundleTransport() *http.Transport {
func getCustomTransport() *http.Transport {
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

// Transport that should be used when a custom CA bundle is specified with the
// SDK.
func getCABundleTransport() *http.Transport {
func getCustomTransport() *http.Transport {
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

// Transport that should be used when a custom CA bundle is specified with the
// SDK.
func getCABundleTransport() *http.Transport {
func getCustomTransport() *http.Transport {
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Expand Down
27 changes: 27 additions & 0 deletions aws/session/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ env values as well.
AWS_SDK_LOAD_CONFIG=1
Custom Shared Config and Credential Files
Shared credentials file path can be set to instruct the SDK to use an alternative
file for the shared credentials. If not set the file will be loaded from
$HOME/.aws/credentials on Linux/Unix based systems, and
Expand All @@ -222,6 +224,8 @@ $HOME/.aws/config on Linux/Unix based systems, and
AWS_CONFIG_FILE=$HOME/my_shared_config
Custom CA Bundle
Path to a custom Credentials Authority (CA) bundle PEM file that the SDK
will use instead of the default system's root CA bundle. Use this only
if you want to replace the CA bundle the SDK uses for TLS requests.
Expand All @@ -242,6 +246,29 @@ Setting a custom HTTPClient in the aws.Config options will override this setting
To use this option and custom HTTP client, the HTTP client needs to be provided
when creating the session. Not the service client.
Custom Client TLS Certificate
The SDK supports the environment and session option being configured with
Client TLS certificates that are sent as a part of the client's TLS handshake
for client authentication. If used, both Cert and Key values are required. If
one is missing, or either fail to load the contents of the file an error will
be returned.
HTTP Client's Transport concrete implementation must be a http.Transport
or creating the session will fail.
AWS_SDK_GO_CLIENT_TLS_KEY=$HOME/my_client_key
AWS_SDK_GO_CLIENT_TLS_CERT=$HOME/my_client_cert
This can also be configured via the session.Options ClientTLSCert and ClientTLSKey.
sess, err := session.NewSessionWithOptions(session.Options{
ClientTLSCert: myCertFile,
ClientTLSKey: myKeyFile,
})
Custom EC2 IMDS Endpoint
The endpoint of the EC2 IMDS client can be configured via the environment
variable, AWS_EC2_METADATA_SERVICE_ENDPOINT when creating the client with a
Session. See Options.EC2IMDSEndpoint for more details.
Expand Down
25 changes: 24 additions & 1 deletion aws/session/env_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,18 @@ type envConfig struct {
// AWS_CA_BUNDLE=$HOME/my_custom_ca_bundle
CustomCABundle string

// Sets the TLC client certificate that should be used by the SDK's HTTP transport
// when making requests. The certificate must be paired with a TLS client key file.
//
// AWS_SDK_GO_CLIENT_TLS_CERT=$HOME/my_client_cert
ClientTLSCert string

// Sets the TLC client key that should be used by the SDK's HTTP transport
// when making requests. The key must be paired with a TLS client certificate file.
//
// AWS_SDK_GO_CLIENT_TLS_KEY=$HOME/my_client_key
ClientTLSKey string

csmEnabled string
CSMEnabled *bool
CSMPort string
Expand Down Expand Up @@ -219,6 +231,15 @@ var (
ec2IMDSEndpointEnvKey = []string{
"AWS_EC2_METADATA_SERVICE_ENDPOINT",
}
useCABundleKey = []string{
"AWS_CA_BUNDLE",
}
useClientTLSCert = []string{
"AWS_SDK_GO_CLIENT_TLS_CERT",
}
useClientTLSKey = []string{
"AWS_SDK_GO_CLIENT_TLS_KEY",
}
)

// loadEnvConfig retrieves the SDK's environment configuration.
Expand Down Expand Up @@ -302,7 +323,9 @@ func envConfigLoad(enableSharedConfig bool) (envConfig, error) {
cfg.SharedConfigFile = defaults.SharedConfigFilename()
}

cfg.CustomCABundle = os.Getenv("AWS_CA_BUNDLE")
setFromEnvVal(&cfg.CustomCABundle, useCABundleKey)
setFromEnvVal(&cfg.ClientTLSCert, useClientTLSCert)
setFromEnvVal(&cfg.ClientTLSKey, useClientTLSKey)

var err error
// STS Regional Endpoint variable
Expand Down
Loading

0 comments on commit 039c0ff

Please sign in to comment.