Skip to content

Support IMDS V2.1 #3255

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

Closed
wants to merge 52 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
a211c53
Remove IMDSv1 mechanism in InstanceProfileCreds
jterapin Jun 2, 2025
7f0126d
Use ec2 metadata client to fetch credentials for instance profile
jterapin Jun 5, 2025
22c1bec
Fix handling of parsing creds
jterapin Jun 5, 2025
9a5410d
Clean up
jterapin Jun 5, 2025
bca31d5
Tidy up existing code
jterapin Jun 6, 2025
bf2fabb
Fix doc warning
jterapin Jun 6, 2025
2c5cca4
Add new config opts
jterapin Jun 6, 2025
350c6f3
Merge branch 'version-3' into imds_update
jterapin Jun 6, 2025
f018568
Minor doc fixes
jterapin Jun 6, 2025
5323f6c
IMDS 2.1 refactor
jterapin Jun 11, 2025
7527feb
Fix failing credential resolution chain specs
jterapin Jun 11, 2025
7349b61
Update shared config and its specs
jterapin Jun 11, 2025
b1de27f
Update chain specs
jterapin Jun 11, 2025
8f11f99
Update ec2 metadata specs
jterapin Jun 11, 2025
de3c2ca
Clean up ec2 metadata
jterapin Jun 11, 2025
82d5dd1
Minor update to EC2 metadata specs
jterapin Jun 11, 2025
f8a25e3
Clean up IMDS provider
jterapin Jun 11, 2025
89d42ba
Small update
jterapin Jun 11, 2025
2951add
Merge branch 'version-3' into imds_update
jterapin Jun 11, 2025
5cb5a41
Changelog draft
jterapin Jun 11, 2025
3856e44
Add back attribute
jterapin Jun 11, 2025
5f59afa
More tidyness
jterapin Jun 11, 2025
42ca582
Remove json checks
jterapin Jun 12, 2025
ff0500d
Remove redunant boolean expressions from disable ec2 metadata check
jterapin Jun 12, 2025
ba66dab
Update docs rendering for ec2 metadata
jterapin Jun 12, 2025
da697a7
Update EC2 specs
jterapin Jun 12, 2025
efce766
Remove trailing comma
jterapin Jun 12, 2025
0f9217d
Some feedback updates
jterapin Jun 12, 2025
ff4b2bd
Update outdated doc links
jterapin Jun 13, 2025
37d5cc5
Update imds provider
jterapin Jun 13, 2025
84762d1
update specs
jterapin Jun 13, 2025
d3ed775
Address feedbacks
jterapin Jun 13, 2025
e39fd9d
Minor clean up
jterapin Jun 13, 2025
f107214
Update changelog
jterapin Jun 13, 2025
4f55b42
Small clean up on chain
mullermp Jun 13, 2025
f759f7e
Add back some ec2metadata tests
mullermp Jun 13, 2025
168e077
Clean up of instance profile credentials
mullermp Jun 13, 2025
ac54816
Minor updates
jterapin Jun 13, 2025
ba05271
Add test runner
jterapin Jun 14, 2025
a4aabe7
Minor clean ups
jterapin Jun 14, 2025
3e38139
Fix ruby 2.7
mullermp Jun 14, 2025
aba9a45
WIP - test issues
mullermp Jun 14, 2025
dffaeb1
Add tests verbatum
mullermp Jun 14, 2025
e0d2224
Fix resolution tests
mullermp Jun 14, 2025
15f1b0d
socket error for extended path across all specs
mullermp Jun 14, 2025
5d37ab7
Update IMDS provider
jterapin Jun 14, 2025
8ab5bcd
Rubocop
jterapin Jun 16, 2025
004944d
Add json parser specs
jterapin Jun 16, 2025
93e191a
Fix docs
jterapin Jun 16, 2025
8cff23d
Merge branch 'version-3' into imds_update
mullermp Jun 18, 2025
ab6d4b0
Update based on feedbacks
jterapin Jun 18, 2025
2bfb830
Merge branch 'version-3' into imds_update
jterapin Jun 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Clean up IMDS provider
  • Loading branch information
jterapin committed Jun 11, 2025
commit f8a25e33b74128d3fe9d7912b7332557adc8f1a1
165 changes: 83 additions & 82 deletions gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class InstanceProfileCredentials
include CredentialProvider
include RefreshingCredentials

# Raised when the metadata path with profile name was not found.
# @api private
class InvalidProfile < RuntimeError; end

# Path base for GET request for profile and credentials
Expand All @@ -27,7 +29,8 @@ class InvalidProfile < RuntimeError; end

# Extended path base for GET request for profile and credentials
# @api private
METADATA_EXTENDED_PATH = '/latest/meta-data/iam/security-credentials-extended/'
METADATA_EXTENDED_PATH =
'/latest/meta-data/iam/security-credentials-extended/'

# @param [Hash] options
# @option options [Aws::EC2Metadata] :client A custom EC2 metadata client to
Expand All @@ -42,7 +45,8 @@ class InvalidProfile < RuntimeError; end
# * `~/.aws/config`
#
# @option options [Integer] :retries (3) Number of times to retry
# when retrieving credentials.
# when retrieving credentials. Defaults to `1` when resolving from
# the default credential chain
# @option options [String] :endpoint ('http://169.254.169.254') The IMDS
# endpoint. This option has precedence over the `:endpoint_mode`.
# @option options [String] :endpoint_mode ('IPv4') The endpoint mode for the
Expand Down Expand Up @@ -71,29 +75,95 @@ class InvalidProfile < RuntimeError; end
def initialize(options = {})
@client = options[:client] ||
EC2Metadata.new(resolve_client_opts(options))
@disable_ec2_metadata = resolve_disable_ec2_metadata(options)
@ec2_instance_profile_name = resolve_ec2_instance_profile_name(options)
@no_refresh_until = nil
@metrics = ['CREDENTIALS_IMDS']

# these are internal tracking
@api_version = :unknown
@resolved_profile = nil
super
end

# @return [Integer] Number of times to retry when retrieving credentials
# from the instance metadata service. Defaults to `1` when resolving from
# the default credential chain ({Aws::CredentialProviderChain}).
# the default credential chain.
def retries
@client.retries
end

private

# tracks which api version is used to call IMDS service
# starts as :unknown and updated to either
# :extended or :legacy after the first successful call
attr_accessor :api_version

# tracks profile name returned from an IMDS call
attr_accessor :resolved_profile

def empty_credentials?(creds)
return true if creds.nil?

!creds.set?
end

def fetch_credentials
profile_name = resolve_profile_name

begin
creds = @client.get(metadata_path + profile_name)
@api_version = :extended if @api_version == :unknown
creds
rescue EC2Metadata::MetadataNotFoundError
if @api_version == :unknown
@api_version = :legacy
fetch_credentials
elsif @ec2_instance_profile_name.nil?
# cache profile may have been replaced
@resolved_profile = nil
fetch_credentials
else
raise InvalidProfile
end
end
rescue StandardError => e
raise if e.is_a?(InvalidProfile)

warn("Error retrieving instance profile credentials: #{e}")
'{}'
end

def metadata_path
case @api_version
when :legacy then METADATA_LEGACY_PATH
else METADATA_EXTENDED_PATH
end
end

def refresh
if @no_refresh_until && @no_refresh_until > Time.now
warn_expired_credentials
return
end

new_creds = Aws::Json.load(fetch_credentials)
if !empty_credentials?(@credentials) &&
(!new_creds['AccessKeyId'] || new_creds['AccessKeyId'].empty?)
# credentials are already set
# error getting new credentials
# so don't update the credentials
@no_refresh_until = Time.now + refresh_offset
warn_expired_credentials
else
update_credentials(new_creds)
end
end

# Compute an offset for refresh with jitter
def refresh_offset
rand(300..360)
end

def resolve_client_opts(options)
opts = options.dup
opts[:backoff] = opts[:delay] if opts[:delay]
Expand All @@ -105,14 +175,6 @@ def resolve_client_opts(options)
)
end

def resolve_disable_ec2_metadata(options)
value =
ENV['AWS_EC2_METADATA_DISABLED'] ||
Aws.shared_config.disable_ec2_metadata(profile: options[:profile]) ||
'false'
Aws::Util.str_2_bool(value)
end

def resolve_ec2_instance_profile_name(options)
value =
options[:ec2_instance_profile_name] ||
Expand All @@ -139,79 +201,27 @@ def resolve_endpoint_mode(options)
) || 'IPv4'
end

def refresh
if @no_refresh_until && @no_refresh_until > Time.now
warn_expired_credentials
return
end

new_creds = Aws::Json.load(fetch_credentials)
if !empty_credentials?(@credentials) && # not empty
(!new_creds['AccessKeyId'] || new_creds['AccessKeyId'].empty?)
# credentials are already set
# error getting new credentials
# don't update the credentials
@no_refresh_until = Time.now + refresh_offset
warn_expired_credentials
else
update_credentials(new_creds)
end
end

def resolve_profile_name
if @ec2_instance_profile_name
@ec2_instance_profile_name
elsif @resolved_profile
@resolved_profile
else
begin
metadata = @client.get(path)
@resolved_profile = JSON.parse(metadata.lines.first.strip)
metadata = @client.get(metadata_path)
@resolved_profile = Aws::Json.load(metadata.lines.first.strip)
@api_version = :extended if @api_version == :unknown
@resolved_profile
rescue EC2Metadata::MetadataNotFoundError
raise unless @api_version == :unknown

# fall back to legacy api
@api_version = :legacy
resolve_profile_name
end
end
end

def path
case @api_version
when :legacy then METADATA_LEGACY_PATH
else METADATA_EXTENDED_PATH
end
end

def fetch_credentials
return '{}' if @disable_ec2_metadata

profile_name = resolve_profile_name
# use profile name
begin
creds = @client.get(path + profile_name)
@api_version = :extended if @api_version == :unknown
creds
rescue EC2Metadata::MetadataNotFoundError
if @api_version == :unknown
@api_version = :legacy
fetch_credentials
elsif @ec2_instance_profile_name.nil?
@resolved_profile = nil
fetch_credentials
else
raise InvalidProfile
end
end
rescue StandardError => e
raise if e.is_a?(InvalidProfile)

warn("Error retrieving instance profile credentials: #{e}")
'{}'
end

def update_credentials(creds)
@credentials = Credentials.new(
creds['AccessKeyId'],
Expand All @@ -228,20 +238,11 @@ def update_credentials(creds)
end

def warn_expired_credentials
warn('Attempting credential expiration extension due to a credential '\
'service availability issue. A refresh of these credentials '\
'will be attempted again in 5 minutes.')
end

def empty_credentials?(creds)
return true if creds.nil?

!creds.set?
end

# Compute an offset for refresh with jitter
def refresh_offset
rand(300..360)
warn(
'Attempting credential expiration extension due to a credential '\
'service availability issue. A refresh of these credentials '\
'will be attempted again in 5 minutes.'
)
end
end
end
Loading