Skip to content

Commit c2533c6

Browse files
authored
feat: add flag to disable IMDSv1 fallback (#2048)
1 parent fe59fdb commit c2533c6

4 files changed

Lines changed: 142 additions & 36 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "a1050420-a5f7-409f-bc4d-1d82c882d168",
3+
"type": "feature",
4+
"description": "Add flag to disable IMDSv1 fallback",
5+
"modules": [
6+
"feature/ec2/imds"
7+
]
8+
}

feature/ec2/imds/api_client.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,16 @@ type Options struct {
174174
// The logger writer interface to write logging messages to.
175175
Logger logging.Logger
176176

177+
// Configure IMDSv1 fallback behavior. By default, the client will attempt
178+
// to fall back to IMDSv1 as needed for backwards compatibility. When set to [aws.FalseTernary]
179+
// the client will return any errors encountered from attempting to fetch a token
180+
// instead of silently using the insecure data flow of IMDSv1.
181+
//
182+
// See [configuring IMDS] for more information.
183+
//
184+
// [configuring IMDS]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
185+
EnableFallback aws.Ternary
186+
177187
// provides the caching of API tokens used for operation calls. If unset,
178188
// the API token will not be retrieved for the operation.
179189
tokenProvider *tokenProvider

feature/ec2/imds/request_middleware_test.go

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"encoding/hex"
77
"fmt"
8+
"github.com/aws/aws-sdk-go-v2/aws"
89
"io"
910
"io/ioutil"
1011
"net/http"
@@ -330,11 +331,12 @@ func (h *successAPIResponseHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
330331

331332
func TestRequestGetToken(t *testing.T) {
332333
cases := map[string]struct {
333-
GetHandler func(*testing.T) http.Handler
334-
APICallCount int
335-
ExpectTrace []string
336-
ExpectContent []byte
337-
ExpectErr string
334+
GetHandler func(*testing.T) http.Handler
335+
APICallCount int
336+
ExpectTrace []string
337+
ExpectContent []byte
338+
ExpectErr string
339+
EnableFallback aws.Ternary
338340
}{
339341
"secure": {
340342
ExpectTrace: []string{
@@ -496,8 +498,69 @@ func TestRequestGetToken(t *testing.T) {
496498
}),
497499
))
498500
},
501+
ExpectErr: "failed to get API token",
502+
},
503+
504+
// retryable token error with fallback enabled (default)
505+
"token failure fallback enabled": {
506+
ExpectTrace: []string{
507+
getTokenPath,
508+
getTokenPath,
509+
getTokenPath,
510+
"/latest/foo",
511+
},
512+
APICallCount: 1,
513+
GetHandler: func(t *testing.T) http.Handler {
514+
return newTestServeMux(t,
515+
newInsecureAPIHandler(t,
516+
500,
517+
&successAPIResponseHandler{t: t,
518+
path: "/latest/foo",
519+
method: "GET",
520+
body: []byte("hello"),
521+
},
522+
))
523+
},
499524
ExpectContent: []byte("hello"),
500-
ExpectErr: "EC2 IMDS failed",
525+
},
526+
// retryable token error with fallback disabled
527+
"token failure fallback disabled": {
528+
ExpectTrace: []string{
529+
getTokenPath,
530+
getTokenPath,
531+
getTokenPath,
532+
},
533+
APICallCount: 1,
534+
GetHandler: func(t *testing.T) http.Handler {
535+
return newTestServeMux(t,
536+
newInsecureAPIHandler(t,
537+
500,
538+
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
539+
t.Errorf("expected no call to API handler")
540+
http.Error(w, "", 400)
541+
}),
542+
))
543+
},
544+
ExpectErr: "failed to get API token",
545+
EnableFallback: aws.BoolTernary(false),
546+
},
547+
"insecure 403 fallback disabled": {
548+
ExpectTrace: []string{
549+
getTokenPath,
550+
},
551+
APICallCount: 1,
552+
GetHandler: func(t *testing.T) http.Handler {
553+
return newTestServeMux(t,
554+
newInsecureAPIHandler(t,
555+
403,
556+
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
557+
t.Errorf("expected no call to API handler")
558+
http.Error(w, "", 400)
559+
}),
560+
))
561+
},
562+
ExpectErr: "failed to get API token",
563+
EnableFallback: aws.BoolTernary(false),
501564
},
502565
}
503566

@@ -515,7 +578,8 @@ func TestRequestGetToken(t *testing.T) {
515578
defer server.Close()
516579

517580
client := New(Options{
518-
Endpoint: server.URL,
581+
Endpoint: server.URL,
582+
EnableFallback: c.EnableFallback,
519583
})
520584

521585
ctx := context.Background()

feature/ec2/imds/token_provider.go

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"github.com/aws/aws-sdk-go-v2/aws"
8+
"github.com/aws/smithy-go"
9+
"github.com/aws/smithy-go/logging"
710
"net/http"
811
"sync"
912
"sync/atomic"
1013
"time"
1114

12-
smithy "github.com/aws/smithy-go"
1315
"github.com/aws/smithy-go/middleware"
1416
smithyhttp "github.com/aws/smithy-go/transport/http"
1517
)
@@ -68,7 +70,7 @@ func (t *tokenProvider) HandleFinalize(
6870
) (
6971
out middleware.FinalizeOutput, metadata middleware.Metadata, err error,
7072
) {
71-
if !t.enabled() {
73+
if t.fallbackEnabled() && !t.enabled() {
7274
// short-circuits to insecure data flow if token provider is disabled.
7375
return next.HandleFinalize(ctx, input)
7476
}
@@ -115,23 +117,15 @@ func (t *tokenProvider) HandleDeserialize(
115117
}
116118

117119
if resp.StatusCode == http.StatusUnauthorized { // unauthorized
118-
err = &retryableError{Err: err}
119120
t.enable()
121+
err = &retryableError{Err: err, isRetryable: true}
120122
}
121123

122124
return out, metadata, err
123125
}
124126

125-
type retryableError struct {
126-
Err error
127-
}
128-
129-
func (*retryableError) RetryableError() bool { return true }
130-
131-
func (e *retryableError) Error() string { return e.Err.Error() }
132-
133127
func (t *tokenProvider) getToken(ctx context.Context) (tok *apiToken, err error) {
134-
if !t.enabled() {
128+
if t.fallbackEnabled() && !t.enabled() {
135129
return nil, &bypassTokenRetrievalError{
136130
Err: fmt.Errorf("cannot get API token, provider disabled"),
137131
}
@@ -147,7 +141,7 @@ func (t *tokenProvider) getToken(ctx context.Context) (tok *apiToken, err error)
147141

148142
tok, err = t.updateToken(ctx)
149143
if err != nil {
150-
return nil, fmt.Errorf("cannot get API token, %w", err)
144+
return nil, err
151145
}
152146

153147
return tok, nil
@@ -167,17 +161,19 @@ func (t *tokenProvider) updateToken(ctx context.Context) (*apiToken, error) {
167161
TokenTTL: t.tokenTTL,
168162
})
169163
if err != nil {
170-
// change the disabled flag on token provider to true, when error is request timeout error.
171164
var statusErr interface{ HTTPStatusCode() int }
172165
if errors.As(err, &statusErr) {
173166
switch statusErr.HTTPStatusCode() {
174-
175-
// Disable get token if failed because of 403, 404, or 405
167+
// Disable future get token if failed because of 403, 404, or 405
176168
case http.StatusForbidden,
177169
http.StatusNotFound,
178170
http.StatusMethodNotAllowed:
179171

180-
t.disable()
172+
if t.fallbackEnabled() {
173+
logger := middleware.GetLogger(ctx)
174+
logger.Logf(logging.Warn, "falling back to IMDSv1: %v", err)
175+
t.disable()
176+
}
181177

182178
// 400 errors are terminal, and need to be upstreamed
183179
case http.StatusBadRequest:
@@ -192,8 +188,17 @@ func (t *tokenProvider) updateToken(ctx context.Context) (*apiToken, error) {
192188
atomic.StoreUint32(&t.disabled, 1)
193189
}
194190

195-
// Token couldn't be retrieved, but bypass this, and allow the
196-
// request to continue.
191+
if !t.fallbackEnabled() {
192+
// NOTE: getToken() is an implementation detail of some outer operation
193+
// (e.g. GetMetadata). It has its own retries that have already been exhausted.
194+
// Mark the underlying error as a terminal error.
195+
err = &retryableError{Err: err, isRetryable: false}
196+
return nil, err
197+
}
198+
199+
// Token couldn't be retrieved, fallback to IMDSv1 insecure flow for this request
200+
// and allow the request to proceed. Future requests _may_ re-attempt fetching a
201+
// token if not disabled.
197202
return nil, &bypassTokenRetrievalError{Err: err}
198203
}
199204

@@ -206,21 +211,21 @@ func (t *tokenProvider) updateToken(ctx context.Context) (*apiToken, error) {
206211
return tok, nil
207212
}
208213

209-
type bypassTokenRetrievalError struct {
210-
Err error
211-
}
212-
213-
func (e *bypassTokenRetrievalError) Error() string {
214-
return fmt.Sprintf("bypass token retrieval, %v", e.Err)
215-
}
216-
217-
func (e *bypassTokenRetrievalError) Unwrap() error { return e.Err }
218-
219214
// enabled returns if the token provider is current enabled or not.
220215
func (t *tokenProvider) enabled() bool {
221216
return atomic.LoadUint32(&t.disabled) == 0
222217
}
223218

219+
// fallbackEnabled returns false if EnableFallback is [aws.FalseTernary], true otherwise
220+
func (t *tokenProvider) fallbackEnabled() bool {
221+
switch t.client.options.EnableFallback {
222+
case aws.FalseTernary:
223+
return false
224+
default:
225+
return true
226+
}
227+
}
228+
224229
// disable disables the token provider and it will no longer attempt to inject
225230
// the token, nor request updates.
226231
func (t *tokenProvider) disable() {
@@ -235,3 +240,22 @@ func (t *tokenProvider) enable() {
235240
t.tokenMux.Unlock()
236241
atomic.StoreUint32(&t.disabled, 0)
237242
}
243+
244+
type bypassTokenRetrievalError struct {
245+
Err error
246+
}
247+
248+
func (e *bypassTokenRetrievalError) Error() string {
249+
return fmt.Sprintf("bypass token retrieval, %v", e.Err)
250+
}
251+
252+
func (e *bypassTokenRetrievalError) Unwrap() error { return e.Err }
253+
254+
type retryableError struct {
255+
Err error
256+
isRetryable bool
257+
}
258+
259+
func (e *retryableError) RetryableError() bool { return e.isRetryable }
260+
261+
func (e *retryableError) Error() string { return e.Err.Error() }

0 commit comments

Comments
 (0)