Skip to content

Commit 2d8560d

Browse files
authored
Refresh IMDSv2 token and retry upon HTTP 401 (#736)
* Invalidate session token for IMDSv1 * Refresh IMDSv2 token and retry upon HTTP 401 * Set project version to 1.96.0 * Formatting * Set project version to 1.97.0
1 parent 84e0dd5 commit 2d8560d

File tree

3 files changed

+221
-38
lines changed

3 files changed

+221
-38
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "AWS"
22
uuid = "fbe9abb3-538b-5e4e-ba9e-bc94f4f92ebc"
33
license = "MIT"
4-
version = "1.96.0"
4+
version = "1.97.0"
55

66
[deps]
77
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"

src/IMDS.jl

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ function refresh_token!(session::Session, duration::Integer=session.duration)
5858
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html#imds-considerations
5959
uri = URI(; scheme="http", host=IPv4_ADDRESS, path="/latest/api/token")
6060
r = try
61-
_http_request("PUT", uri, headers; status_exception=false)
61+
_http_request("PUT", uri, headers; status_exception=false, retry=false)
6262
catch e
6363
# The IMDSv2 uses a default Time To Live (TTL) of 1 (also known as the hop limit) at
6464
# the IP layer to ensure token requests occur on the instance. When this occurs we
@@ -71,6 +71,7 @@ function refresh_token!(session::Session, duration::Integer=session.duration)
7171
"https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/" *
7272
"instancedata-data-retrieval.html#imds-considerations"
7373

74+
session.token = ""
7475
session.duration = 0
7576
session.expiration = typemax(Int64) # Use IMDSv1 indefinitely
7677
return session
@@ -86,6 +87,7 @@ function refresh_token!(session::Session, duration::Integer=session.duration)
8687
session.duration = duration
8788
session.expiration = t + duration
8889
elseif r.status == 404
90+
session.token = ""
8991
session.duration = 0
9092
session.expiration = typemax(Int64) # Use IMDSv1 indefinitely
9193
else
@@ -97,27 +99,58 @@ function refresh_token!(session::Session, duration::Integer=session.duration)
9799
return session
98100
end
99101

100-
function request(session::Session, method::AbstractString, path::AbstractString; kwargs...)
102+
function request(
103+
session::Session, method::AbstractString, path::AbstractString; status_exception=true
104+
)
105+
# Only allow the token to be refreshed once per call to `IMDS.request`.
106+
allow_refresh = true
107+
101108
# Attempt to generate token for use with IMDSv2. If we're unable to generate a token
102109
# we'll fall back on using IMDSv1. We prefer using IMDSv2 as instances can be configured
103110
# to disable IMDSv1 access: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-IMDS-new-instances.html#configure-IMDS-new-instances
104-
token_expired(session) && refresh_token!(session)
105-
headers = Pair{String,String}[]
106-
!isempty(session.token) && push!(headers, "X-aws-ec2-metadata-token" => session.token)
111+
if token_expired(session)
112+
refresh_token!(session)
113+
allow_refresh = false
114+
end
115+
headers = HTTP.Header[]
116+
if !isempty(session.token)
117+
HTTP.setheader(headers, "X-aws-ec2-metadata-token" => session.token)
118+
end
107119

108120
# Only using the IPv4 endpoint as the IPv6 endpoint has to be explicitly enabled and
109121
# does not disable IPv4 support.
110122
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-IMDS-new-instances.html#configure-IMDS-new-instances-ipv4-ipv6-endpoints
111123
uri = URI(; scheme="http", host=IPv4_ADDRESS, path)
112-
return _http_request(method, uri, headers; kwargs...)
124+
125+
# Refresh the token and immediately retry if we encounter "HTTP 401 Unauthorized".
126+
#
127+
# > When token usage is set to `required` (IMDSv2), requests without a valid token or
128+
# > with an expired token receive a `401 - Unauthorized` HTTP error code.
129+
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html#instance-metadata-v2-how-it-works
130+
retry_delays = [0]
131+
function retry_check(s, e, request, response, response_body)
132+
if allow_refresh && e isa StatusError && e.status == 401 && !isempty(session.token)
133+
refresh_token!(session)
134+
allow_refresh = false
135+
HTTP.setheader(request.headers, "X-aws-ec2-metadata-token" => session.token)
136+
return true
137+
else
138+
return false
139+
end
140+
end
141+
142+
return _http_request(method, uri, headers; retry_delays, retry_check, status_exception)
113143
end
114144

115145
function _http_request(args...; status_exception=true, kwargs...)
116146
response = try
117-
# Always throw status exceptions so we can determine if the IMDS service is available
118-
@mock HTTP.request(
119-
args...; connect_timeout=1, retry=false, kwargs..., status_exception=true
120-
)
147+
# Override the user's `status_exception` so we can consistently handle status
148+
# exceptions. Additionally, by forcing `status_exception=true` this allows retries
149+
# to work.
150+
#
151+
# Additionally, we set a low connect timeout to have faster responses when
152+
# attempting to connect to IMDS outside of EC2.
153+
@mock HTTP.request(args...; connect_timeout=1, kwargs..., status_exception=true)
121154
catch e
122155
# When running outside of an EC2 instance the link-local address will be unavailable
123156
# and connections will fail. On EC2 instances where IMDS is disabled a HTTP 403 is

0 commit comments

Comments
 (0)