Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

aws/session: Add support for client TLS certs on HTTP client #3654

Merged
merged 2 commits into from
Dec 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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