From be1aa64764bb1214f492f783f4025e1a62d1fbfd Mon Sep 17 00:00:00 2001 From: Rich Vanderwal Date: Tue, 24 Nov 2020 15:48:28 -0800 Subject: [PATCH 1/4] Initial AWS IMDSv2 work --- CHANGELOG.md | 5 ++++ v3/internal/utilization/aws.go | 42 ++++++++++++++++++++++++++--- v3/internal/utilization/provider.go | 4 +-- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c732e93f2..466e57a0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # ChangeLog +## Unreleased + +* To keep up with the latest security protocols implemented by Amazon Web + Services, the agent now uses AWS IMDSv2 to find utilization data. + ## 3.9.0 ### Changes diff --git a/v3/internal/utilization/aws.go b/v3/internal/utilization/aws.go index e18f66ade..ffe91fc58 100644 --- a/v3/internal/utilization/aws.go +++ b/v3/internal/utilization/aws.go @@ -12,9 +12,11 @@ import ( ) const ( - awsHostname = "169.254.169.254" - awsEndpointPath = "/2016-09-02/dynamic/instance-identity/document" - awsEndpoint = "http://" + awsHostname + awsEndpointPath + awsHostname = "169.254.169.254" + awsEndpointPath = "/2016-09-02/dynamic/instance-identity/document" + awsTokenEndpointPath = "/latest/api/token" + awsEndpoint = "http://" + awsHostname + awsEndpointPath + awsTokenEndpoint = "http://" + awsHostname + awsTokenEndpointPath ) type aws struct { @@ -44,6 +46,27 @@ func (e unexpectedAWSErr) Error() string { return fmt.Sprintf("unexpected AWS error: %v", e.e) } +// getAWSToken attempts to get the IMDSv2 token within the providerTimeout set +// provider.go. +func getAWSToken() (token string, err error) { + client := http.Client{ + Timeout: providerTimeout, + } + request, err := http.NewRequest("PUT", awsTokenEndpoint, nil) + request.Header.Add("X-aws-ec2-metadata-token-ttl-seconds", "60") + response, err := client.Do(request) + if err != nil { + return "", err + } + defer response.Body.Close() + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return "", err + } + + return string(body), nil +} + func getAWS(client *http.Client) (ret *aws, err error) { // In some cases, 3rd party providers might block requests to metadata // endpoints in such a way that causes a panic in the underlying @@ -56,7 +79,18 @@ func getAWS(client *http.Client) (ret *aws, err error) { } }() - response, err := client.Get(awsEndpoint) + // AWS' IMDSv2 requires us to get a token before requesting metadata. + awsToken, err := getAWSToken() + if err != nil { + ret = nil + err = unexpectedAWSErr{e: fmt.Errorf("error contacting AWS IMDSv2 token endpoint, %v", err)} + } + + //Add the header to the outbound request. + request, err := http.NewRequest("GET", awsEndpoint, nil) + request.Header.Add("X-aws-ec2-metadata-token", awsToken) + + response, err := client.Do(request) if err != nil { // No unexpectedAWSErr here: A timeout is usually going to // happen. diff --git a/v3/internal/utilization/provider.go b/v3/internal/utilization/provider.go index 03bcb64e0..a15bfef2b 100644 --- a/v3/internal/utilization/provider.go +++ b/v3/internal/utilization/provider.go @@ -14,8 +14,8 @@ import ( // Constants from the spec. const ( - maxFieldValueSize = 255 // The maximum value size, in bytes. - providerTimeout = 1 * time.Second // The maximum time a HTTP provider may block. + maxFieldValueSize = 255 // The maximum value size, in bytes. + providerTimeout = 500 * time.Millisecond // The maximum time a HTTP provider may block. lookupAddrTimeout = 500 * time.Millisecond ) From 9394ef5bdfb455c7658acce24efef1673968fbc9 Mon Sep 17 00:00:00 2001 From: Rich Vanderwal Date: Tue, 24 Nov 2020 16:17:45 -0800 Subject: [PATCH 2/4] re-use exising http.Client --- v3/internal/utilization/aws.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/v3/internal/utilization/aws.go b/v3/internal/utilization/aws.go index ffe91fc58..4ecd47733 100644 --- a/v3/internal/utilization/aws.go +++ b/v3/internal/utilization/aws.go @@ -17,6 +17,7 @@ const ( awsTokenEndpointPath = "/latest/api/token" awsEndpoint = "http://" + awsHostname + awsEndpointPath awsTokenEndpoint = "http://" + awsHostname + awsTokenEndpointPath + awsTokenTTL = "60" // seconds this AWS utiliation session will last ) type aws struct { @@ -48,12 +49,9 @@ func (e unexpectedAWSErr) Error() string { // getAWSToken attempts to get the IMDSv2 token within the providerTimeout set // provider.go. -func getAWSToken() (token string, err error) { - client := http.Client{ - Timeout: providerTimeout, - } +func getAWSToken(client *http.Client) (token string, err error) { request, err := http.NewRequest("PUT", awsTokenEndpoint, nil) - request.Header.Add("X-aws-ec2-metadata-token-ttl-seconds", "60") + request.Header.Add("X-aws-ec2-metadata-token-ttl-seconds", awsTokenTTL) response, err := client.Do(request) if err != nil { return "", err @@ -80,7 +78,7 @@ func getAWS(client *http.Client) (ret *aws, err error) { }() // AWS' IMDSv2 requires us to get a token before requesting metadata. - awsToken, err := getAWSToken() + awsToken, err := getAWSToken(client) if err != nil { ret = nil err = unexpectedAWSErr{e: fmt.Errorf("error contacting AWS IMDSv2 token endpoint, %v", err)} From f767f7b9d01a030c0317d7c36d3991d821dc55a7 Mon Sep 17 00:00:00 2001 From: Rich Vanderwal Date: Tue, 1 Dec 2020 17:24:17 -0800 Subject: [PATCH 3/4] Fix cross-agent test --- v3/internal/utilization/provider_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/v3/internal/utilization/provider_test.go b/v3/internal/utilization/provider_test.go index 1b5c1fbcc..aebad0776 100644 --- a/v3/internal/utilization/provider_test.go +++ b/v3/internal/utilization/provider_test.go @@ -54,7 +54,13 @@ func (m *mockTransport) RoundTrip(r *http.Request) (*http.Response, error) { } } - m.t.Errorf("Unknown request URI: %s", r.URL.String()) + // Since this cross-agent test hacks the transport to check responses to + // the IMDS endpoint, this UN-hacks it for IMDSv2, where half the requests + // are going to the token-granting endpoint, not the information-gathering + // endpoint that's meant to be tested. + if r.URL.String() != "http://169.254.169.254/latest/api/token" { + m.t.Errorf("Unknown request URI: %s", r.URL.String()) + } return nil, nil } From fd7116a68290af4b35b72b16997dc1fff5668485 Mon Sep 17 00:00:00 2001 From: Rich Vanderwal Date: Tue, 1 Dec 2020 18:16:38 -0800 Subject: [PATCH 4/4] quieter response to IMDSv2 error, typo. --- v3/internal/utilization/aws.go | 7 ++++--- v3/internal/utilization/provider_test.go | 14 ++++++-------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/v3/internal/utilization/aws.go b/v3/internal/utilization/aws.go index 4ecd47733..d7fd10493 100644 --- a/v3/internal/utilization/aws.go +++ b/v3/internal/utilization/aws.go @@ -17,7 +17,7 @@ const ( awsTokenEndpointPath = "/latest/api/token" awsEndpoint = "http://" + awsHostname + awsEndpointPath awsTokenEndpoint = "http://" + awsHostname + awsTokenEndpointPath - awsTokenTTL = "60" // seconds this AWS utiliation session will last + awsTokenTTL = "60" // seconds this AWS utilization session will last ) type aws struct { @@ -80,8 +80,9 @@ func getAWS(client *http.Client) (ret *aws, err error) { // AWS' IMDSv2 requires us to get a token before requesting metadata. awsToken, err := getAWSToken(client) if err != nil { - ret = nil - err = unexpectedAWSErr{e: fmt.Errorf("error contacting AWS IMDSv2 token endpoint, %v", err)} + // No unexpectedAWSErr here: A timeout is usually going to + // happen. + return nil, err } //Add the header to the outbound request. diff --git a/v3/internal/utilization/provider_test.go b/v3/internal/utilization/provider_test.go index aebad0776..7b57b9b67 100644 --- a/v3/internal/utilization/provider_test.go +++ b/v3/internal/utilization/provider_test.go @@ -48,19 +48,17 @@ type mockBody struct { } func (m *mockTransport) RoundTrip(r *http.Request) (*http.Response, error) { + + // Half the requests are going to the test's endpoint, while the other half + // are going to the AWS IMDSv2 token endpoint. Accept both. for match, response := range m.responses { - if r.URL.String() == match { + if (r.URL.String() == match) || + (r.URL.String() == awsTokenEndpoint) { return m.respond(response) } } - // Since this cross-agent test hacks the transport to check responses to - // the IMDS endpoint, this UN-hacks it for IMDSv2, where half the requests - // are going to the token-granting endpoint, not the information-gathering - // endpoint that's meant to be tested. - if r.URL.String() != "http://169.254.169.254/latest/api/token" { - m.t.Errorf("Unknown request URI: %s", r.URL.String()) - } + m.t.Errorf("Unknown request URI: %s", r.URL.String()) return nil, nil }