From 43975ab0e4b3c7d0be5a0432c92fb58af6be5c56 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Wed, 2 Jul 2025 10:18:23 -0700 Subject: [PATCH 01/14] Clean up configs --- .../instance_profile_credentials.rb | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb b/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb index be47a8f87e6..15b58a1d61b 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb @@ -76,8 +76,7 @@ class TokenExpiredError < RuntimeError; end # AWS credentials are required and need to be refreshed. def initialize(options = {}) @retries = options[:retries] || 1 - endpoint_mode = resolve_endpoint_mode(options) - @endpoint = resolve_endpoint(options, endpoint_mode) + @endpoint = resolve_endpoint(options) @port = options[:port] || 80 @disable_imds_v1 = resolve_disable_v1(options) # Flag for if v2 flow fails, skip future attempts @@ -102,41 +101,41 @@ def initialize(options = {}) private def resolve_endpoint_mode(options) - value = options[:endpoint_mode] - value ||= ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE'] - value ||= Aws.shared_config.ec2_metadata_service_endpoint_mode( - profile: options[:profile] - ) - value || 'IPv4' + options[:endpoint_mode] || + ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE'] || + Aws.shared_config.ec2_metadata_service_endpoint_mode(profile: options[:profile]) || + 'IPv4' end - def resolve_endpoint(options, endpoint_mode) - value = options[:endpoint] || options[:ip_address] - value ||= ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT'] - value ||= Aws.shared_config.ec2_metadata_service_endpoint( - profile: options[:profile] - ) + def resolve_endpoint(options) + if (value = options[:ip_address]) + warn('The `:ip_address` option is deprecated. Use `:endpoint` instead.') + return value + end + value = + options[:endpoint] || + ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT'] || + Aws.shared_config.ec2_metadata_service_endpoint(profile: options[:profile]) || + nil return value if value + endpoint_mode = resolve_endpoint_mode(options) case endpoint_mode.downcase when 'ipv4' then 'http://169.254.169.254' when 'ipv6' then 'http://[fd00:ec2::254]' else - raise ArgumentError, - ':endpoint_mode is not valid, expected IPv4 or IPv6, '\ - "got: #{endpoint_mode}" + raise ArgumentError, ":endpoint_mode is not valid, expected IPv4 or IPv6, got: #{endpoint_mode}" end end def resolve_disable_v1(options) - value = options[:disable_imds_v1] - value ||= ENV['AWS_EC2_METADATA_V1_DISABLED'] - value ||= Aws.shared_config.ec2_metadata_v1_disabled( - profile: options[:profile] - ) - value = value.to_s.downcase if value - Aws::Util.str_2_bool(value) || false + value = + options[:disable_imds_v1] || + ENV['AWS_EC2_METADATA_V1_DISABLED'] || + Aws.shared_config.ec2_metadata_v1_disabled(profile: options[:profile]) || + 'false' + Aws::Util.str_2_bool(value.to_s.downcase) end def backoff(backoff) From d74ba05c4ee85f4c14e6bc24eb3407da5f360242 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 3 Jul 2025 07:25:06 -0700 Subject: [PATCH 02/14] Clean up imds provider --- .../instance_profile_credentials.rb | 224 ++++++++---------- 1 file changed, 94 insertions(+), 130 deletions(-) diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb b/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb index 15b58a1d61b..06ed1cf3c18 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb @@ -4,9 +4,7 @@ require 'net/http' module Aws - # An auto-refreshing credential provider that loads credentials from - # EC2 instances. - # + # An auto-refreshing credential provider that loads credentials from EC2 instances. # instance_credentials = Aws::InstanceProfileCredentials.new # ec2 = Aws::EC2::Client.new(credentials: instance_credentials) class InstanceProfileCredentials @@ -22,10 +20,8 @@ class TokenRetrivalError < RuntimeError; end # @api private class TokenExpiredError < RuntimeError; end - # These are the errors we trap when attempting to talk to the - # instance metadata service. Any of these imply the service - # is not present, no responding or some other non-recoverable - # error. + # These are the errors we trap when attempting to talk to the instance metadata service. + # Any of these imply the service is not present, no responding or some other non-recoverable error. # @api private NETWORK_ERRORS = [ Errno::EHOSTUNREACH, @@ -46,56 +42,48 @@ class TokenExpiredError < RuntimeError; end METADATA_TOKEN_PATH = '/latest/api/token'.freeze # @param [Hash] options - # @option options [Integer] :retries (1) Number of times to retry - # when retrieving credentials. - # @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 instance metadata service. This is either 'IPv4' ('169.254.169.254') - # or 'IPv6' ('[fd00:ec2::254]'). - # @option options [Boolean] :disable_imds_v1 (false) Disable the use of the - # legacy EC2 Metadata Service v1. - # @option options [String] :ip_address ('169.254.169.254') Deprecated. Use - # :endpoint instead. The IP address for the endpoint. + # @option options [Integer] :retries (1) Number of times to retry when retrieving credentials. + # @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 instance metadata service. This is + # either 'IPv4' ('169.254.169.254') or 'IPv6' ('[fd00:ec2::254]'). + # @option options [Boolean] :disable_imds_v1 (false) Disable the use of the legacy EC2 Metadata Service v1. + # @option options [String] :ip_address ('169.254.169.254') Deprecated. Use `:endpoint` instead. + # The IP address for the endpoint. # @option options [Integer] :port (80) # @option options [Float] :http_open_timeout (1) # @option options [Float] :http_read_timeout (1) - # @option options [Numeric, Proc] :delay By default, failures are retried - # with exponential back-off, i.e. `sleep(1.2 ** num_failures)`. You can - # pass a number of seconds to sleep between failed attempts, or - # a Proc that accepts the number of failures. - # @option options [IO] :http_debug_output (nil) HTTP wire - # traces are sent to this object. You can specify something - # like $stdout. - # @option options [Integer] :token_ttl Time-to-Live in seconds for EC2 - # Metadata Token used for fetching Metadata Profile Credentials, defaults - # to 21600 seconds - # @option options [Callable] before_refresh Proc called before - # credentials are refreshed. `before_refresh` is called - # with an instance of this object when - # AWS credentials are required and need to be refreshed. + # @option options [Numeric, Proc] :delay By default, failures are retried with exponential back-off, i.e. + # `sleep(1.2 ** num_failures)`. You can pass a number of seconds to sleep between failed attempts, or a Proc + # that accepts the number of failures. + # @option options [IO] :http_debug_output (nil) HTTP wire traces are sent to this object. + # You can specify something like `$stdout`. + # @option options [Integer] :token_ttl Time-to-Live in seconds for EC2 Metadata Token used for fetching + # Metadata Profile Credentials, defaults to 21600 seconds. + # @option options [Callable] :before_refresh Proc called before credentials are refreshed. `before_refresh` + # is called with an instance of this object when AWS credentials are required and need to be refreshed. def initialize(options = {}) - @retries = options[:retries] || 1 - @endpoint = resolve_endpoint(options) - @port = options[:port] || 80 + @backoff = backoff(options[:backoff]) @disable_imds_v1 = resolve_disable_v1(options) - # Flag for if v2 flow fails, skip future attempts - @imds_v1_fallback = false + @endpoint = resolve_endpoint(options) @http_open_timeout = options[:http_open_timeout] || 1 @http_read_timeout = options[:http_read_timeout] || 1 @http_debug_output = options[:http_debug_output] - @backoff = backoff(options[:backoff]) + @port = options[:port] || 80 + @retries = options[:retries] || 1 @token_ttl = options[:token_ttl] || 21_600 - @token = nil - @no_refresh_until = nil + @async_refresh = false + # Flag for if v2 flow fails, skip future attempts + @imds_v1_fallback = false + @no_refresh_until = nil + @token = nil @metrics = ['CREDENTIALS_IMDS'] super end - # @return [Integer] Number of times to retry when retrieving credentials - # from the instance metadata service. Defaults to 0 when resolving from - # the default credential chain ({Aws::CredentialProviderChain}). + # @return [Integer] Number of times to retry when retrieving credentials from the instance metadata service. + # Defaults to 0 when resolving from the default credential chain ({Aws::CredentialProviderChain}). attr_reader :retries private @@ -152,98 +140,80 @@ def refresh return end - # Retry loading credentials up to 3 times is the instance metadata - # service is responding but is returning invalid JSON documents - # in response to the GET profile credentials call. - begin - retry_errors([Aws::Json::ParseError], max_retries: 3) do - c = Aws::Json.load(get_credentials.to_s) - if empty_credentials?(@credentials) - @credentials = Credentials.new( - c['AccessKeyId'], - c['SecretAccessKey'], - c['Token'] - ) - @expiration = c['Expiration'] ? Time.iso8601(c['Expiration']) : nil - if @expiration && @expiration < Time.now - @no_refresh_until = Time.now + refresh_offset - warn_expired_credentials - end - else - # credentials are already set, update them only if the new ones are not empty - if !c['AccessKeyId'] || c['AccessKeyId'].empty? - # error getting new credentials - @no_refresh_until = Time.now + refresh_offset - warn_expired_credentials - else - @credentials = Credentials.new( - c['AccessKeyId'], - c['SecretAccessKey'], - c['Token'] - ) - @expiration = c['Expiration'] ? Time.iso8601(c['Expiration']) : nil - if @expiration && @expiration < Time.now - @no_refresh_until = Time.now + refresh_offset - warn_expired_credentials - end - end + new_creds = + begin + # Retry loading credentials up to 3 times is the instance metadata + # service is responding but is returning invalid JSON documents + # in response to the GET profile credentials call. + retry_errors([Aws::Json::ParseError], max_retries: 3) do + Aws::Json.load(retrieve_credentials.to_s) end + rescue Aws::Json::ParseError + raise Aws::Errors::MetadataParserError end - rescue Aws::Json::ParseError - raise Aws::Errors::MetadataParserError + + if !empty_credentials?(@credentials) && (!new_creds['AccessKeyId'] || new_creds['AccessKeyId'].empty?) + # credentials are already set, but there was an error getting new credentials + # so don't update the credentials and use stale ones (static stability) + @no_refresh_until = Time.now + rand(300..360) + warn_expired_credentials + else + # credentials are empty or successfully retrieved, update them + update_credentials(new_creds) end end - def get_credentials + def retrieve_credentials + return '{}' if ec2_metadata_disabled? + # Retry loading credentials a configurable number of times if # the instance metadata service is not responding. - if _metadata_disabled? - '{}' - else - begin - retry_errors(NETWORK_ERRORS, max_retries: @retries) do - open_connection do |conn| - # attempt to fetch token to start secure flow first - # and rescue to failover - fetch_token(conn) unless @imds_v1_fallback - token = @token.value if token_set? - - # disable insecure flow if we couldn't get token - # and imds v1 is disabled - raise TokenRetrivalError if token.nil? && @disable_imds_v1 - - _get_credentials(conn, token) - end + begin + retry_errors(NETWORK_ERRORS, max_retries: @retries) do + open_connection do |conn| + # attempt to fetch token to start secure flow first + # and rescue to failover + fetch_token(conn) unless skip_token? + + # disable insecure flow if we couldn't get token and imds v1 is disabled + raise TokenRetrivalError if @token.nil? && @disable_imds_v1 + + fetch_credentials(conn) end - rescue => e - warn("Error retrieving instance profile credentials: #{e}") - '{}' end + rescue StandardError => e + warn("Error retrieving instance profile credentials: #{e}") + '{}' end end + def skip_token? + @imds_v1_fallback || (@token && !@token.expired?) + end + + def update_credentials(creds) + @credentials = Credentials.new(creds['AccessKeyId'], creds['SecretAccessKey'], creds['Token']) + @expiration = creds['Expiration'] ? Time.iso8601(creds['Expiration']) : nil + return unless @expiration && @expiration < Time.now + + @no_refresh_until = Time.now + rand(300..360) + warn_expired_credentials + end + def fetch_token(conn) - retry_errors(NETWORK_ERRORS, max_retries: @retries) do - unless token_set? - created_time = Time.now - token_value, ttl = http_put( - conn, METADATA_TOKEN_PATH, @token_ttl - ) - @token = Token.new(token_value, ttl, created_time) if token_value && ttl - end - end + created_time = Time.now + token_value, ttl = http_put(conn) + @token = Token.new(token_value, ttl, created_time) if token_value && ttl rescue *NETWORK_ERRORS # token attempt failed, reset token # fallback to non-token mode - @token = nil @imds_v1_fallback = true end - # token is optional - if nil, uses v1 (insecure) flow - def _get_credentials(conn, token) - metadata = http_get(conn, METADATA_PATH_BASE, token) + def fetch_credentials(conn) + metadata = http_get(conn, METADATA_PATH_BASE) profile_name = metadata.lines.first.strip - http_get(conn, METADATA_PATH_BASE + profile_name, token) + http_get(conn, METADATA_PATH_BASE + profile_name) rescue TokenExpiredError # Token has expired, reset it # The next retry should fetch it @@ -256,7 +226,7 @@ def token_set? @token && !@token.expired? end - def _metadata_disabled? + def ec2_metadata_disabled? ENV.fetch('AWS_EC2_METADATA_DISABLED', 'false').downcase == 'true' end @@ -271,9 +241,9 @@ def open_connection end # GET request fetch profile and credentials - def http_get(connection, path, token = nil) + def http_get(connection, path) headers = { 'User-Agent' => "aws-sdk-ruby3/#{CORE_GEM_VERSION}" } - headers['x-aws-ec2-metadata-token'] = token if token + headers['x-aws-ec2-metadata-token'] = @token.value if @token response = connection.request(Net::HTTP::Get.new(path, headers)) case response.code.to_i @@ -287,12 +257,12 @@ def http_get(connection, path, token = nil) end # PUT request fetch token with ttl - def http_put(connection, path, ttl) + def http_put(connection) headers = { 'User-Agent' => "aws-sdk-ruby3/#{CORE_GEM_VERSION}", - 'x-aws-ec2-metadata-token-ttl-seconds' => ttl.to_s + 'x-aws-ec2-metadata-token-ttl-seconds' => @token_ttl.to_s } - response = connection.request(Net::HTTP::Put.new(path, headers)) + response = connection.request(Net::HTTP::Put.new(METADATA_TOKEN_PATH, headers)) case response.code.to_i when 200 [ @@ -321,18 +291,12 @@ def retry_errors(error_classes, options = {}, &_block) 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.") + 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) - !creds || !creds.access_key_id || creds.access_key_id.empty? - end - - # Compute an offset for refresh with jitter - def refresh_offset - 300 + rand(0..60) + creds.nil? || !creds.set? end # @api private From ea1828d3daee737c60276104e192b1db72911a10 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 3 Jul 2025 07:39:09 -0700 Subject: [PATCH 03/14] Clean up imds provider specs --- .../aws/instance_profile_credentials_spec.rb | 205 ++++++------------ 1 file changed, 72 insertions(+), 133 deletions(-) diff --git a/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb b/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb index 925b5808e9a..fa7a2c9aa24 100644 --- a/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb +++ b/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb @@ -5,9 +5,7 @@ module Aws describe InstanceProfileCredentials do let(:path) { '/latest/meta-data/iam/security-credentials/' } - let(:token_path) { '/latest/api/token' } - let(:ipv4_endpoint) { 'http://169.254.169.254' } let(:ipv6_endpoint) { 'http://[fd00:ec2::254]' } @@ -26,31 +24,27 @@ module Aws end it 'can be configured with shared config' do - allow_any_instance_of(Aws::SharedConfig) - .to receive(:ec2_metadata_service_endpoint_mode).and_return('IPv6') + allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint_mode).and_return('IPv6') subject = InstanceProfileCredentials.new expect(subject.instance_variable_get(:@endpoint)).to eq ipv6_endpoint end it 'can be configured using env variable with precedence' do ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE'] = 'IPv4' - allow_any_instance_of(Aws::SharedConfig) - .to receive(:ec2_metadata_service_endpoint_mode).and_return('IPv6') + allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint_mode).and_return('IPv6') subject = InstanceProfileCredentials.new expect(subject.instance_variable_get(:@endpoint)).to eq ipv4_endpoint end it 'can be configure through code with precedence' do ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE'] = 'IPv4' - allow_any_instance_of(Aws::SharedConfig) - .to receive(:ec2_metadata_service_endpoint_mode).and_return('IPv4') + allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint_mode).and_return('IPv4') subject = InstanceProfileCredentials.new(endpoint_mode: 'IPv6') expect(subject.instance_variable_get(:@endpoint)).to eq ipv6_endpoint end it 'raises ArgumentError when endpoint mode is unexpected' do - expect { InstanceProfileCredentials.new(endpoint_mode: 'IPv69') } - .to raise_error(ArgumentError) + expect { InstanceProfileCredentials.new(endpoint_mode: 'IPv69') }.to raise_error(ArgumentError) end end @@ -62,23 +56,18 @@ module Aws end it 'can be configured with shared config' do - allow_any_instance_of(Aws::SharedConfig) - .to receive(:ec2_metadata_service_endpoint).and_return(endpoint) + allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint).and_return(endpoint) expect(subject.instance_variable_get(:@endpoint)).to eq endpoint end it 'can be configured using env variable with precedence' do ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT'] = endpoint - allow_any_instance_of(Aws::SharedConfig) - .to receive(:ec2_metadata_service_endpoint_mode) - .and_return('http://124.124.124.124') + allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint_mode).and_return(endpoint) expect(subject.instance_variable_get(:@endpoint)).to eq endpoint end it 'can be configured through code with precedence' do - allow_any_instance_of(Aws::SharedConfig) - .to receive(:ec2_metadata_service_endpoint) - .and_return('bar-example.com') + allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint).and_return('bar-example.com') ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT'] = 'foo-example.com' subject = InstanceProfileCredentials.new(ip_address: endpoint) expect(subject.instance_variable_get(:@endpoint)).to eq endpoint @@ -86,8 +75,7 @@ module Aws it 'overrides endpoint mode configuration with ENV' do ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE'] = 'IPv4' - allow_any_instance_of(Aws::SharedConfig) - .to receive(:ec2_metadata_service_endpoint_mode).and_return('IPv4') + allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint_mode).and_return('IPv4') ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT'] = endpoint subject = InstanceProfileCredentials.new(endpoint_mode: 'IPv4') expect(subject.instance_variable_get(:@endpoint)).to eq endpoint @@ -95,21 +83,16 @@ module Aws it 'overrides endpoint mode configuration with shared config' do ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE'] = 'IPv4' - allow_any_instance_of(Aws::SharedConfig) - .to receive(:ec2_metadata_service_endpoint_mode).and_return('IPv4') - allow_any_instance_of(Aws::SharedConfig) - .to receive(:ec2_metadata_service_endpoint).and_return(endpoint) + allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint_mode).and_return('IPv4') + allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint).and_return(endpoint) subject = InstanceProfileCredentials.new(endpoint_mode: 'IPv4') expect(subject.instance_variable_get(:@endpoint)).to eq endpoint end it 'overrides endpoint mode configuration with code' do ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE'] = 'IPv4' - allow_any_instance_of(Aws::SharedConfig) - .to receive(:ec2_metadata_service_endpoint_mode).and_return('IPv4') - subject = InstanceProfileCredentials.new( - endpoint_mode: 'IPv4', endpoint: endpoint - ) + allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint_mode).and_return('IPv4') + subject = InstanceProfileCredentials.new(endpoint_mode: 'IPv4', endpoint: endpoint) expect(subject.instance_variable_get(:@endpoint)).to eq endpoint end end @@ -119,11 +102,7 @@ module Aws before do stub_request(:put, "#{ipv4_endpoint}#{token_path}") - .to_return( - status: 200, - body: "my-token\n", - headers: { 'x-aws-ec2-metadata-token-ttl-seconds' => '21600' } - ) + .to_return(status: 200, body: "my-token\n", headers: { 'x-aws-ec2-metadata-token-ttl-seconds' => '21600' }) stub_request(:get, "#{ipv4_endpoint}#{path}") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: "profile-name\n") @@ -138,28 +117,16 @@ module Aws it 'uses endpoint without a scheme and a configured port' do uri = URI(ipv4_endpoint) - InstanceProfileCredentials.new( - endpoint: uri.hostname, - port: uri.port, - backoff: 0 - ) + InstanceProfileCredentials.new(endpoint: uri.hostname, port: uri.port, backoff: 0) end it 'still supports ip_address' do uri = URI(ipv4_endpoint) - InstanceProfileCredentials.new( - ip_address: uri.hostname, - port: uri.port, - backoff: 0 - ) + InstanceProfileCredentials.new(ip_address: uri.hostname, port: uri.port, backoff: 0) end it 'endpoint takes precedence over endpoint mode' do - InstanceProfileCredentials.new( - endpoint: ipv4_endpoint, - endpoint_mode: 'IPv6', - backoff: 0 - ) + InstanceProfileCredentials.new(endpoint: ipv4_endpoint, endpoint_mode: 'IPv6', backoff: 0) end end @@ -171,31 +138,21 @@ module Aws end it 'can be configured with shared config' do - allow_any_instance_of(Aws::SharedConfig) - .to receive(:ec2_metadata_v1_disabled) - .and_return(disable_imds_v1.to_s) - expect(subject.instance_variable_get(:@disable_imds_v1)) - .to eq disable_imds_v1 + allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_v1_disabled).and_return(disable_imds_v1.to_s) + expect(subject.instance_variable_get(:@disable_imds_v1)).to eq disable_imds_v1 end it 'can be configured using env variable with precedence' do ENV['AWS_EC2_METADATA_V1_DISABLED'] = disable_imds_v1.to_s - allow_any_instance_of(Aws::SharedConfig) - .to receive(:ec2_metadata_v1_disabled).and_return('false') - expect(subject.instance_variable_get(:@disable_imds_v1)) - .to eq disable_imds_v1 + allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_v1_disabled).and_return('false') + expect(subject.instance_variable_get(:@disable_imds_v1)).to eq disable_imds_v1 end it 'can be configured through code with precedence' do - allow_any_instance_of(Aws::SharedConfig) - .to receive(:ec2_metadata_v1_disabled) - .and_return('false') + allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_v1_disabled).and_return('false') ENV['AWS_EC2_METADATA_V1_DISABLED'] = 'false' - subject = InstanceProfileCredentials.new( - disable_imds_v1: disable_imds_v1 - ) - expect(subject.instance_variable_get(:@disable_imds_v1)) - .to eq disable_imds_v1 + subject = InstanceProfileCredentials.new(disable_imds_v1: disable_imds_v1) + expect(subject.instance_variable_get(:@disable_imds_v1)).to eq disable_imds_v1 end end @@ -207,9 +164,8 @@ module Aws Timeout::Error ].each do |error_class| it "returns no credentials for #{error_class}" do - stub_request(:put, "http://169.254.169.254#{token_path}") - .to_return(status: 200, body: 'mytoken') - stub_request(:get, "http://169.254.169.254#{path}").to_raise(error_class) + stub_request(:put, ipv4_endpoint + token_path).to_return(status: 200, body: 'mytoken') + stub_request(:get, ipv4_endpoint + path).to_raise(error_class) expect(InstanceProfileCredentials.new(backoff: 0).set?).to be(false) end end @@ -219,10 +175,8 @@ module Aws 401 ].each do |error_code| it "returns no credentials for #{error_code} when fetching token" do - stub_request(:put, "http://169.254.169.254#{token_path}") - .to_return(status: error_code) - stub_request(:get, "http://169.254.169.254#{path}") - .to_return(status: 200) + stub_request(:put, ipv4_endpoint + token_path).to_return(status: error_code) + stub_request(:get, ipv4_endpoint + path).to_return(status: 200) expect(InstanceProfileCredentials.new(backoff: 0).set?).to be(false) end end @@ -248,12 +202,9 @@ module Aws 404 ].each do |error_code| it "fails over to insecure flow for error code #{error_code}" do - stub_request(:put, "http://169.254.169.254#{token_path}") - .to_return(status: error_code) - stub_request(:get, "http://169.254.169.254#{path}") - .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "http://169.254.169.254#{path}profile-name") - .to_return(status: 200, body: resp) + stub_request(:put, ipv4_endpoint + token_path).to_return(status: error_code) + stub_request(:get, ipv4_endpoint + path).to_return(status: 200, body: "profile-name\n") + stub_request(:get, "#{ipv4_endpoint}#{path}profile-name").to_return(status: 200, body: resp) c = InstanceProfileCredentials.new(backoff: 0) expect(c.credentials.access_key_id).to eq('akid') expect(c.credentials.secret_access_key).to eq('secret') @@ -268,12 +219,9 @@ module Aws Timeout::Error ].each do |error_class| it "fails over to insecure flow for #{error_class}" do - stub_request(:put, "http://169.254.169.254#{token_path}") - .to_raise(error_class) - stub_request(:get, "http://169.254.169.254#{path}") - .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "http://169.254.169.254#{path}profile-name") - .to_return(status: 200, body: resp) + stub_request(:put, ipv4_endpoint + token_path).to_raise(error_class) + stub_request(:get, ipv4_endpoint + path).to_return(status: 200, body: "profile-name\n") + stub_request(:get, "#{ipv4_endpoint}#{path}profile-name").to_return(status: 200, body: resp) c = InstanceProfileCredentials.new(backoff: 0) expect(c.credentials.access_key_id).to eq('akid') expect(c.credentials.secret_access_key).to eq('secret') @@ -282,12 +230,9 @@ module Aws end it 'memoizes v1 fallback' do - token_stub = stub_request(:put, "http://169.254.169.254#{token_path}") - .to_return(status: 403) - profile_name_stub = stub_request(:get, "http://169.254.169.254#{path}") - .to_return(status: 200, body: "profile-name\n") - credentials_stub = stub_request(:get, "http://169.254.169.254#{path}profile-name") - .to_return(status: 200, body: resp) + token_stub = stub_request(:put, ipv4_endpoint + token_path).to_return(status: 403) + profile_name_stub = stub_request(:get, ipv4_endpoint + path).to_return(status: 200, body: "profile-name\n") + credentials_stub = stub_request(:get, "#{ipv4_endpoint}#{path}profile-name").to_return(status: 200, body: resp) c = InstanceProfileCredentials.new(backoff: 0, retries: 0) c.refresh! @@ -323,16 +268,16 @@ module Aws "Expiration" : "#{expiration.strftime('%Y-%m-%dT%H:%M:%SZ')}" } JSON - stub_request(:put, "http://169.254.169.254#{token_path}") + stub_request(:put, ipv4_endpoint + token_path) .to_return( status: 200, body: "my-token\n", headers: { 'x-aws-ec2-metadata-token-ttl-seconds' => '21600' } ) - stub_request(:get, "http://169.254.169.254#{path}") + stub_request(:get, ipv4_endpoint + path) .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "http://169.254.169.254#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: resp) c = InstanceProfileCredentials.new(backoff: 0) @@ -355,12 +300,9 @@ module Aws "Expiration" : "#{expiration.strftime('%Y-%m-%dT%H:%M:%SZ')}" } JSON - stub_request(:put, "http://169.254.169.254#{token_path}") - .to_return(status: 404) - stub_request(:get, "http://169.254.169.254#{path}") - .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "http://169.254.169.254#{path}profile-name") - .to_return(status: 200, body: resp) + stub_request(:put, ipv4_endpoint + token_path).to_return(status: 404) + stub_request(:get, ipv4_endpoint + path).to_return(status: 200, body: "profile-name\n") + stub_request(:get, "#{ipv4_endpoint}#{path}profile-name").to_return(status: 200, body: resp) c = InstanceProfileCredentials.new(backoff: 0) expect(c.credentials.access_key_id).to eq('akid') expect(c.credentials.secret_access_key).to eq('secret') @@ -380,8 +322,7 @@ module Aws end it 'does not attempt to get credentials (insecure)' do - stub_request(:put, "http://169.254.169.254#{token_path}") - .to_return(status: 404) + stub_request(:put, ipv4_endpoint + token_path).to_return(status: 404) expect(InstanceProfileCredentials.new(backoff: 0).set?).to be(false) end @@ -398,16 +339,16 @@ module Aws "Expiration" : "#{expiration.strftime('%Y-%m-%dT%H:%M:%SZ')}" } JSON - stub_request(:put, "http://169.254.169.254#{token_path}") + stub_request(:put, ipv4_endpoint + token_path) .to_return( status: 200, body: "my-token\n", headers: { 'x-aws-ec2-metadata-token-ttl-seconds' => '21600' } ) - stub_request(:get, "http://169.254.169.254#{path}") + stub_request(:get, ipv4_endpoint + path) .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "http://169.254.169.254#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: resp) c = InstanceProfileCredentials.new(backoff: 0) @@ -446,16 +387,16 @@ module Aws JSON before(:each) do - stub_request(:put, "http://169.254.169.254#{token_path}") + stub_request(:put, ipv4_endpoint + token_path) .to_return( status: 200, body: "my-token\n", headers: { 'x-aws-ec2-metadata-token-ttl-seconds' => '21600' } ) - stub_request(:get, "http://169.254.169.254#{path}") + stub_request(:get, ipv4_endpoint + path) .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "http://169.254.169.254#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: resp) .to_return(status: 200, body: resp2) @@ -479,11 +420,11 @@ module Aws end it 'retries if the first load fails' do - stub_request(:get, "http://169.254.169.254#{path}") + stub_request(:get, ipv4_endpoint + path) .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 500) .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "http://169.254.169.254#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: resp2) c = InstanceProfileCredentials.new(backoff: 0) @@ -494,11 +435,11 @@ module Aws end it 'retries if get profile response is invalid JSON' do - stub_request(:get, "http://169.254.169.254#{path}") + stub_request(:get, ipv4_endpoint + path) .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 500) .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "http://169.254.169.254#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: ' ') .to_return(status: 200, body: '') @@ -512,11 +453,11 @@ module Aws end it 'retries invalid JSON exactly 3 times' do - stub_request(:get, "http://169.254.169.254#{path}") + stub_request(:get, ipv4_endpoint + path) .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 500) .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "http://169.254.169.254#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: '') .to_return(status: 200, body: ' ') @@ -531,11 +472,11 @@ module Aws end it 'retries errors parsing expiration time 3 times' do - stub_request(:get, "http://169.254.169.254#{path}") + stub_request(:get, ipv4_endpoint + path) .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 500) .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "http://169.254.169.254#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: '{ "Expiration": "Expiration" }') .to_return(status: 200, body: '{ "Expiration": "Expiration" }') @@ -565,7 +506,7 @@ module Aws it 'given an empty response, entry credentials are returned' do # This handles the case when the service response but returns # a JSON document without credentials (error cases) - stub_request(:get, "http://169.254.169.254#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: resp) c = InstanceProfileCredentials.new @@ -580,16 +521,16 @@ module Aws describe '#retries' do before(:each) do - stub_request(:put, "http://169.254.169.254#{token_path}") + stub_request(:put, ipv4_endpoint + token_path) .to_return( status: 200, body: "my-token\n", headers: { 'x-aws-ec2-metadata-token-ttl-seconds' => '21600' } ) - stub_request(:get, "http://169.254.169.254#{path}") + stub_request(:get, ipv4_endpoint + path) .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_raise(Errno::ECONNREFUSED) - stub_request(:get, "http://169.254.169.254#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_raise(Errno::ECONNREFUSED) end @@ -599,15 +540,11 @@ module Aws end it 'keeps trying "retries" times, with exponential backoff' do - expected_request = stub_request(:get, "http://169.254.169.254#{path}") - .to_raise(Errno::ECONNREFUSED) + expected_request = stub_request(:get, ipv4_endpoint + path).to_raise(Errno::ECONNREFUSED) expect(Kernel).to receive(:sleep).with(1) expect(Kernel).to receive(:sleep).with(2) expect(Kernel).to receive(:sleep).with(4) - InstanceProfileCredentials.new( - backoff: ->(n) { Kernel.sleep(2**n) }, - retries: 3 - ) + InstanceProfileCredentials.new(backoff: ->(n) { Kernel.sleep(2**n) }, retries: 3) assert_requested(expected_request, times: 4) end end @@ -641,13 +578,13 @@ module Aws JSON before(:each) do - stub_request(:put, "http://169.254.169.254#{token_path}") + stub_request(:put, ipv4_endpoint + token_path) .to_return( status: 200, body: "my-token\n", headers: { 'x-aws-ec2-metadata-token-ttl-seconds' => '21600' } ) - stub_request(:get, "http://169.254.169.254#{path}") + stub_request(:get, ipv4_endpoint + path) .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: "profile-name\n") end @@ -655,7 +592,8 @@ module Aws it 'provides credentials when the first call returns expired credentials' do expect_any_instance_of(InstanceProfileCredentials).to receive(:warn).at_least(:once) - expected_request = stub_request(:get, "http://169.254.169.254#{path}profile-name") + expected_request = + stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: expired_resp) @@ -674,10 +612,11 @@ module Aws it 'provides credentials after a read timeout during a refresh' do expect_any_instance_of(InstanceProfileCredentials).to receive(:warn).at_least(:once) - expected_request = stub_request(:get, "http://169.254.169.254#{path}profile-name") - .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) - .to_return(status: 200, body: near_expiration_resp) - .to_raise(Timeout::Error) + expected_request = + stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") + .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) + .to_return(status: 200, body: near_expiration_resp) + .to_raise(Timeout::Error) provider = InstanceProfileCredentials.new(backoff: 0, retries: 0) From bdf135b877d0c4b9003f10e0e0f1c08e34781863 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 3 Jul 2025 07:42:04 -0700 Subject: [PATCH 04/14] Fix doc --- gems/aws-sdk-core/lib/seahorse/client/request_context.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gems/aws-sdk-core/lib/seahorse/client/request_context.rb b/gems/aws-sdk-core/lib/seahorse/client/request_context.rb index de7650b2ae9..843b9b89495 100644 --- a/gems/aws-sdk-core/lib/seahorse/client/request_context.rb +++ b/gems/aws-sdk-core/lib/seahorse/client/request_context.rb @@ -5,7 +5,7 @@ module Seahorse module Client class RequestContext - + # @param [Hash] options # @option options [required,Symbol] :operation_name (nil) # @option options [required,Model::Operation] :operation (nil) # @option options [Model::Authorizer] :authorizer (nil) @@ -16,7 +16,7 @@ class RequestContext # @option options [Http::Response] :http_response (Http::Response.new) # @option options [Integer] :retries (0) # @option options [Aws::Telemetry::TracerBase] :tracer (Aws::Telemetry::NoOpTracer.new) - # @options options [Hash] :metadata ({}) + # @option options [Hash] :metadata ({}) def initialize(options = {}) @operation_name = options[:operation_name] @operation = options[:operation] From 6e85f6a82207e2048a29e12dcd8e7b551f86572d Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 3 Jul 2025 07:43:13 -0700 Subject: [PATCH 05/14] Minor update to shared spec helper --- gems/aws-sdk-core/spec/shared_spec_helper.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/gems/aws-sdk-core/spec/shared_spec_helper.rb b/gems/aws-sdk-core/spec/shared_spec_helper.rb index 70dd39ab60c..3271650be0d 100644 --- a/gems/aws-sdk-core/spec/shared_spec_helper.rb +++ b/gems/aws-sdk-core/spec/shared_spec_helper.rb @@ -29,10 +29,8 @@ allow(Dir).to receive(:home).and_raise(ArgumentError) # disable instance profile credentials - token_path = '/latest/api/token' - path = '/latest/meta-data/iam/security-credentials/' - stub_request(:get, "http://169.254.169.254#{path}").to_raise(SocketError) - stub_request(:put, "http://169.254.169.254#{token_path}").to_raise(SocketError) + stub_request(:put, 'http://169.254.169.254/latest/api/token').to_raise(SocketError) + stub_request(:get, 'http://169.254.169.254/latest/meta-data/iam/security-credentials/').to_raise(SocketError) allow_any_instance_of(Aws::InstanceProfileCredentials).to receive(:warn) Aws.shared_config.fresh From 99ad43cb15c8bfe98c2aed2784554854b8b464ba Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 3 Jul 2025 07:57:58 -0700 Subject: [PATCH 06/14] Add clarification docs --- .../aws-sdk-core/instance_profile_credentials.rb | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb b/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb index 06ed1cf3c18..c1eaa86fca7 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb @@ -5,8 +5,20 @@ module Aws # An auto-refreshing credential provider that loads credentials from EC2 instances. + # # instance_credentials = Aws::InstanceProfileCredentials.new # ec2 = Aws::EC2::Client.new(credentials: instance_credentials) + # + # ## Retries + # When initialized from the default credential chain, this provider defaults to `0` retries. + # Breakdown of retries is as follows: + # + # * **Configurable retries** (defaults to `1`): these retries handle errors when communicating + # with the IMDS endpoint. + # * **JSON parsing retries**: Fixed at 3 attempts to handle cases when IMDS returns malformed JSON + # responses. These retries are separate from configurable retries. + # + # @see https://docs.aws.amazon.com/sdkref/latest/guide/feature-imds-credentials.html IMDS Credential Provider class InstanceProfileCredentials include CredentialProvider include RefreshingCredentials @@ -46,7 +58,7 @@ class TokenExpiredError < RuntimeError; end # @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 instance metadata service. This is - # either 'IPv4' ('169.254.169.254') or 'IPv6' ('[fd00:ec2::254]'). + # either 'IPv4' (`169.254.169.254`) or IPv6' (`[fd00:ec2::254]`). # @option options [Boolean] :disable_imds_v1 (false) Disable the use of the legacy EC2 Metadata Service v1. # @option options [String] :ip_address ('169.254.169.254') Deprecated. Use `:endpoint` instead. # The IP address for the endpoint. @@ -83,7 +95,7 @@ def initialize(options = {}) end # @return [Integer] Number of times to retry when retrieving credentials from the instance metadata service. - # Defaults to 0 when resolving from the default credential chain ({Aws::CredentialProviderChain}). + # Defaults to 0 when resolving from the default credential chain. attr_reader :retries private From 0cf295de07d8bdb7ecb142fdb4e7c05988ce5a84 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 3 Jul 2025 08:05:11 -0700 Subject: [PATCH 07/14] Add more doc updates --- .../lib/aws-sdk-core/instance_profile_credentials.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb b/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb index c1eaa86fca7..a2d9fba5cde 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb @@ -14,7 +14,9 @@ module Aws # Breakdown of retries is as follows: # # * **Configurable retries** (defaults to `1`): these retries handle errors when communicating - # with the IMDS endpoint. + # with the IMDS endpoint. There are two separate retry mechanisms within the provider: + # * Entire token fetch and credential retrieval process + # * Token fetching # * **JSON parsing retries**: Fixed at 3 attempts to handle cases when IMDS returns malformed JSON # responses. These retries are separate from configurable retries. # From 5455e66d7f57b9f8103bcb959bd972da14482592 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 3 Jul 2025 08:05:20 -0700 Subject: [PATCH 08/14] Add changelog entry --- gems/aws-sdk-core/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gems/aws-sdk-core/CHANGELOG.md b/gems/aws-sdk-core/CHANGELOG.md index a94da1d2da6..e89bbc64769 100644 --- a/gems/aws-sdk-core/CHANGELOG.md +++ b/gems/aws-sdk-core/CHANGELOG.md @@ -1,6 +1,8 @@ Unreleased Changes ------------------ +* Issue - Document retry behaviors in `InstanceProfileCredentials`. + 3.226.2 (2025-07-01) ------------------ From 5e42b7f39da6dad18dc2a02308c32f652b23c217 Mon Sep 17 00:00:00 2001 From: Matt Muller Date: Thu, 3 Jul 2025 13:59:24 -0400 Subject: [PATCH 09/14] Global retries of 3 including parsing errors --- gems/aws-sdk-core/CHANGELOG.md | 2 +- .../aws-sdk-core/credential_provider_chain.rb | 2 +- .../instance_profile_credentials.rb | 159 +++++++----------- .../aws/credential_provider_chain_spec.rb | 53 +++--- .../aws/instance_profile_credentials_spec.rb | 83 +-------- 5 files changed, 99 insertions(+), 200 deletions(-) diff --git a/gems/aws-sdk-core/CHANGELOG.md b/gems/aws-sdk-core/CHANGELOG.md index e89bbc64769..d786029e58d 100644 --- a/gems/aws-sdk-core/CHANGELOG.md +++ b/gems/aws-sdk-core/CHANGELOG.md @@ -1,7 +1,7 @@ Unreleased Changes ------------------ -* Issue - Document retry behaviors in `InstanceProfileCredentials`. +* Issue - Refactor `InstanceProfileCredentials` to be simpler and increase network retries to be more resilient. 3.226.2 (2025-07-01) ------------------ diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb b/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb index 2efebeaaef1..25163f33e1c 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb @@ -191,7 +191,7 @@ def instance_profile_credentials(options) if ENV['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] || ENV['AWS_CONTAINER_CREDENTIALS_FULL_URI'] ECSCredentials.new(options) - else + elsif !(ENV.fetch('AWS_EC2_METADATA_DISABLED', 'false').downcase == 'true') InstanceProfileCredentials.new(options.merge(profile: profile_name)) end end diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb b/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb index a2d9fba5cde..922dbe7331f 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb @@ -10,15 +10,9 @@ module Aws # ec2 = Aws::EC2::Client.new(credentials: instance_credentials) # # ## Retries - # When initialized from the default credential chain, this provider defaults to `0` retries. - # Breakdown of retries is as follows: # - # * **Configurable retries** (defaults to `1`): these retries handle errors when communicating - # with the IMDS endpoint. There are two separate retry mechanisms within the provider: - # * Entire token fetch and credential retrieval process - # * Token fetching - # * **JSON parsing retries**: Fixed at 3 attempts to handle cases when IMDS returns malformed JSON - # responses. These retries are separate from configurable retries. + # When initialized from the default credential chain, this provider defaults to `0` retries. + # Otherwise, it defaults to 3 retries and will retry network and parsing errors. # # @see https://docs.aws.amazon.com/sdkref/latest/guide/feature-imds-credentials.html IMDS Credential Provider class InstanceProfileCredentials @@ -28,24 +22,15 @@ class InstanceProfileCredentials # @api private class Non200Response < RuntimeError; end + # @deprecated Unfortunate spelling name. # @api private class TokenRetrivalError < RuntimeError; end # @api private - class TokenExpiredError < RuntimeError; end + class TokenRetrievalError < TokenRetrivalError; end - # These are the errors we trap when attempting to talk to the instance metadata service. - # Any of these imply the service is not present, no responding or some other non-recoverable error. # @api private - NETWORK_ERRORS = [ - Errno::EHOSTUNREACH, - Errno::ECONNREFUSED, - Errno::EHOSTDOWN, - Errno::ENETUNREACH, - SocketError, - Timeout::Error, - Non200Response - ].freeze + class TokenExpiredError < RuntimeError; end # Path base for GET request for profile and credentials # @api private @@ -56,9 +41,10 @@ class TokenExpiredError < RuntimeError; end METADATA_TOKEN_PATH = '/latest/api/token'.freeze # @param [Hash] options - # @option options [Integer] :retries (1) Number of times to retry when retrieving credentials. + # @option options [Integer] :retries (3) Number of times to retry when retrieving credentials. Defaults to 0 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`. + # over the `:endpoint_mode`. # @option options [String] :endpoint_mode ('IPv4') The endpoint mode for the instance metadata service. This is # either 'IPv4' (`169.254.169.254`) or IPv6' (`[fd00:ec2::254]`). # @option options [Boolean] :disable_imds_v1 (false) Disable the use of the legacy EC2 Metadata Service v1. @@ -84,20 +70,19 @@ def initialize(options = {}) @http_read_timeout = options[:http_read_timeout] || 1 @http_debug_output = options[:http_debug_output] @port = options[:port] || 80 - @retries = options[:retries] || 1 + @retries = options[:retries] || 3 @token_ttl = options[:token_ttl] || 21_600 - @async_refresh = false - # Flag for if v2 flow fails, skip future attempts @imds_v1_fallback = false - @no_refresh_until = nil @token = nil + @no_refresh_until = nil + + @async_refresh = false @metrics = ['CREDENTIALS_IMDS'] super end - # @return [Integer] Number of times to retry when retrieving credentials from the instance metadata service. - # Defaults to 0 when resolving from the default credential chain. + # @return [Integer] attr_reader :retries private @@ -154,73 +139,42 @@ def refresh return end - new_creds = - begin - # Retry loading credentials up to 3 times is the instance metadata - # service is responding but is returning invalid JSON documents - # in response to the GET profile credentials call. - retry_errors([Aws::Json::ParseError], max_retries: 3) do - Aws::Json.load(retrieve_credentials.to_s) + # Retry loading and parsing credentials up to a configurable number of times. + # StandardError is not ideal but it covers Net::HTTP errors. + # https://gist.github.com/tenderlove/245188 + # ArgumentError can be raised when parsing a bad expiration. + retry_errors([Aws::Json::ParseError, ArgumentError, StandardError, Non200Response]) do + open_connection do |conn| + # attempt to fetch token to start secure flow first, and rescue to failover + fetch_token(conn) unless @imds_v1_fallback || token_set? + # disable insecure flow if we couldn't get token and imds v1 is disabled + raise TokenRetrievalError if @token.nil? && @disable_imds_v1 + + creds = Aws::Json.load(fetch_credentials(conn)) + if @credentials&.set? && empty_credentials?(creds) + # credentials are already set, but there was an error getting new credentials + # so don't update the credentials and use stale ones (static stability) + @no_refresh_until = Time.now + rand(300..360) + warn_expired_credentials + else + # credentials are empty or successfully retrieved, update them + update_credentials(creds) end - rescue Aws::Json::ParseError - raise Aws::Errors::MetadataParserError end - - if !empty_credentials?(@credentials) && (!new_creds['AccessKeyId'] || new_creds['AccessKeyId'].empty?) - # credentials are already set, but there was an error getting new credentials - # so don't update the credentials and use stale ones (static stability) - @no_refresh_until = Time.now + rand(300..360) - warn_expired_credentials - else - # credentials are empty or successfully retrieved, update them - update_credentials(new_creds) end - end - - def retrieve_credentials - return '{}' if ec2_metadata_disabled? - - # Retry loading credentials a configurable number of times if - # the instance metadata service is not responding. - begin - retry_errors(NETWORK_ERRORS, max_retries: @retries) do - open_connection do |conn| - # attempt to fetch token to start secure flow first - # and rescue to failover - fetch_token(conn) unless skip_token? - - # disable insecure flow if we couldn't get token and imds v1 is disabled - raise TokenRetrivalError if @token.nil? && @disable_imds_v1 - - fetch_credentials(conn) - end - end - rescue StandardError => e - warn("Error retrieving instance profile credentials: #{e}") - '{}' - end - end - - def skip_token? - @imds_v1_fallback || (@token && !@token.expired?) - end - - def update_credentials(creds) - @credentials = Credentials.new(creds['AccessKeyId'], creds['SecretAccessKey'], creds['Token']) - @expiration = creds['Expiration'] ? Time.iso8601(creds['Expiration']) : nil - return unless @expiration && @expiration < Time.now - - @no_refresh_until = Time.now + rand(300..360) - warn_expired_credentials + rescue ArgumentError, Aws::Json::ParseError + raise Aws::Errors::MetadataParserError + rescue StandardError => e + warn("Error retrieving instance profile credentials: #{e}") + '{}' end def fetch_token(conn) created_time = Time.now token_value, ttl = http_put(conn) @token = Token.new(token_value, ttl, created_time) if token_value && ttl - rescue *NETWORK_ERRORS - # token attempt failed, reset token - # fallback to non-token mode + rescue StandardError, Non200Response + # Token attempt failed, reset token fallback to insecure mode. @imds_v1_fallback = true end @@ -229,8 +183,7 @@ def fetch_credentials(conn) profile_name = metadata.lines.first.strip http_get(conn, METADATA_PATH_BASE + profile_name) rescue TokenExpiredError - # Token has expired, reset it - # The next retry should fetch it + # Token has expired, reset it. The next retry should fetch it @token = nil @imds_v1_fallback = false raise Non200Response @@ -240,8 +193,13 @@ def token_set? @token && !@token.expired? end - def ec2_metadata_disabled? - ENV.fetch('AWS_EC2_METADATA_DISABLED', 'false').downcase == 'true' + def update_credentials(creds) + @credentials = Credentials.new(creds['AccessKeyId'], creds['SecretAccessKey'], creds['Token']) + @expiration = creds['Expiration'] ? Time.iso8601(creds['Expiration']) : nil + return unless @expiration && @expiration < Time.now + + @no_refresh_until = Time.now + rand(300..360) + warn_expired_credentials end def open_connection @@ -284,33 +242,32 @@ def http_put(connection) response.header['x-aws-ec2-metadata-token-ttl-seconds'].to_i ] when 400 - raise TokenRetrivalError + raise TokenRetrievalError else raise Non200Response end end - def retry_errors(error_classes, options = {}, &_block) - max_retries = options[:max_retries] - retries = 0 + def retry_errors(error_classes, &_block) + attempts = 0 begin yield - rescue *error_classes - raise unless retries < max_retries + rescue *error_classes => e + raise unless attempts < @retries - @backoff.call(retries) - retries += 1 + @backoff.call(attempts) + attempts += 1 retry end 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.') + 'A refresh of these credentials will be attempted again in 5 minutes.') end - def empty_credentials?(creds) - creds.nil? || !creds.set? + def empty_credentials?(creds_hash) + !creds_hash['AccessKeyId'] || creds_hash['AccessKeyId'].empty? end # @api private diff --git a/gems/aws-sdk-core/spec/aws/credential_provider_chain_spec.rb b/gems/aws-sdk-core/spec/aws/credential_provider_chain_spec.rb index 737494beb71..df9ee1939b6 100644 --- a/gems/aws-sdk-core/spec/aws/credential_provider_chain_spec.rb +++ b/gems/aws-sdk-core/spec/aws/credential_provider_chain_spec.rb @@ -15,16 +15,15 @@ def random_creds end def with_shared_credentials(profile_name = SecureRandom.hex, credentials_file = nil) - path = File.expand_path( - File.join('HOME', '.aws', 'credentials')) + path = File.expand_path(File.join('HOME', '.aws', 'credentials')) creds = random_creds - credentials_file ||= <<-CREDS -[#{profile_name}] -aws_access_key_id = #{creds[:access_key_id]} -aws_secret_access_key = #{creds[:secret_access_key]} -aws_session_token = #{creds[:session_token]} -aws_account_id = #{creds[:account_id]} -CREDS + credentials_file ||= <<~CREDS + [#{profile_name}] + aws_access_key_id = #{creds[:access_key_id]} + aws_secret_access_key = #{creds[:secret_access_key]} + aws_session_token = #{creds[:session_token]} + aws_account_id = #{creds[:account_id]} + CREDS allow(Dir).to receive(:home).and_return('HOME') allow(File).to receive(:exist?).with(path).and_return(true) allow(File).to receive(:readable?).with(path).and_return(true) @@ -64,15 +63,17 @@ def validate_metrics(*expected_metrics) end let(:config) do - double('config', - access_key_id: nil, - secret_access_key: nil, - session_token: nil, - account_id: nil, - profile: nil, - region: nil, - instance_profile_credentials_timeout: 1, - instance_profile_credentials_retries: 0) + double( + 'config', + access_key_id: nil, + secret_access_key: nil, + session_token: nil, + account_id: nil, + profile: nil, + region: nil, + instance_profile_credentials_timeout: 1, + instance_profile_credentials_retries: 0 + ) end let(:mock_instance_creds) { double('InstanceProfileCredentials', set?: false) } @@ -133,6 +134,18 @@ def validate_metrics(*expected_metrics) validate_metrics('CREDENTIALS_IMDS') end + it 'skips instance profile service when AWS_EC2_METADATA_DISABLED is true' do + ENV['AWS_EC2_METADATA_DISABLED'] = 'true' + expect(InstanceProfileCredentials).not_to receive(:new) + expect(credentials).to be(nil) + end + + it 'AWS_EC2_METADATA_DISABLED is not case sensitive' do + ENV['AWS_EC2_METADATA_DISABLED'] = 'TrUe' + expect(InstanceProfileCredentials).not_to receive(:new) + expect(credentials).to be(nil) + end + it 'hydrates credentials from ECS when AWS_CONTAINER_CREDENTIALS_RELATIVE_URI is set' do ENV['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] = 'test_uri' mock_ecs_creds = double('ECSCredentials', metrics: ['CREDENTIALS_HTTP'], set?: true) @@ -194,7 +207,7 @@ def validate_metrics(*expected_metrics) end it 'hydrates credentials from config over ENV' do - env_creds = with_env_credentials + with_env_credentials expected_creds = with_config_credentials validate_credentials(expected_creds) validate_metrics('CREDENTIALS_PROFILE') @@ -203,7 +216,7 @@ def validate_metrics(*expected_metrics) it 'hydrates credentials from profile when config set over ENV' do expected_creds = with_shared_credentials allow(config).to receive(:profile).and_return(expected_creds[:profile_name]) - env_creds = with_env_credentials + with_env_credentials validate_credentials(expected_creds) validate_metrics('CREDENTIALS_PROFILE') end diff --git a/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb b/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb index fa7a2c9aa24..5e7b21d0dd5 100644 --- a/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb +++ b/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb @@ -243,73 +243,6 @@ module Aws end end - describe 'disable IMDS flag' do - it 'does not attempt to get credentials when disable flag set' do - ENV['AWS_EC2_METADATA_DISABLED'] = 'true' - expect(InstanceProfileCredentials.new.set?).to be(false) - end - - it 'has a disable flag which is not case sensitive' do - ENV['AWS_EC2_METADATA_DISABLED'] = 'TrUe' - expect(InstanceProfileCredentials.new.set?).to be(false) - end - - it 'ignores values other than true for the disable flag (secure)' do - ENV['AWS_EC2_METADATA_DISABLED'] = '1' - expiration = Time.now.utc + 3600 - resp = <<-JSON.strip - { - "Code" : "Success", - "LastUpdated" : "2013-11-22T20:03:48Z", - "Type" : "AWS-HMAC", - "AccessKeyId" : "akid", - "SecretAccessKey" : "secret", - "Token" : "session-token", - "Expiration" : "#{expiration.strftime('%Y-%m-%dT%H:%M:%SZ')}" - } - JSON - stub_request(:put, ipv4_endpoint + token_path) - .to_return( - status: 200, - body: "my-token\n", - headers: { 'x-aws-ec2-metadata-token-ttl-seconds' => '21600' } - ) - stub_request(:get, ipv4_endpoint + path) - .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) - .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") - .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) - .to_return(status: 200, body: resp) - c = InstanceProfileCredentials.new(backoff: 0) - expect(c.credentials.access_key_id).to eq('akid') - expect(c.credentials.secret_access_key).to eq('secret') - expect(c.credentials.session_token).to eq('session-token') - end - - it 'ignores values other than true for the disable flag (insecure)' do - ENV['AWS_EC2_METADATA_DISABLED'] = '1' - expiration = Time.now.utc + 3600 - resp = <<-JSON.strip - { - "Code" : "Success", - "LastUpdated" : "2013-11-22T20:03:48Z", - "Type" : "AWS-HMAC", - "AccessKeyId" : "akid", - "SecretAccessKey" : "secret", - "Token" : "session-token", - "Expiration" : "#{expiration.strftime('%Y-%m-%dT%H:%M:%SZ')}" - } - JSON - stub_request(:put, ipv4_endpoint + token_path).to_return(status: 404) - stub_request(:get, ipv4_endpoint + path).to_return(status: 200, body: "profile-name\n") - stub_request(:get, "#{ipv4_endpoint}#{path}profile-name").to_return(status: 200, body: resp) - c = InstanceProfileCredentials.new(backoff: 0) - expect(c.credentials.access_key_id).to eq('akid') - expect(c.credentials.secret_access_key).to eq('secret') - expect(c.credentials.session_token).to eq('session-token') - end - end - describe 'disable IMDS v1 flag' do before do ENV['AWS_EC2_METADATA_V1_DISABLED'] = 'true' @@ -442,7 +375,6 @@ module Aws stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: ' ') - .to_return(status: 200, body: '') .to_return(status: 200, body: '{') .to_return(status: 200, body: resp2) c = InstanceProfileCredentials.new(backoff: 0) @@ -455,36 +387,33 @@ module Aws it 'retries invalid JSON exactly 3 times' do stub_request(:get, ipv4_endpoint + path) .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) - .to_return(status: 500) .to_return(status: 200, body: "profile-name\n") stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: '') .to_return(status: 200, body: ' ') .to_return(status: 200, body: '{') - .to_return(status: 200, body: ' ') expect do InstanceProfileCredentials.new(backoff: 0) end.to raise_error( - Aws::Errors::MetadataParserError, - 'Failed to parse metadata service response.' + Aws::Errors::MetadataParserError, 'Failed to parse metadata service response.' ) end it 'retries errors parsing expiration time 3 times' do stub_request(:get, ipv4_endpoint + path) .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) - .to_return(status: 500) .to_return(status: 200, body: "profile-name\n") stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: '{ "Expiration": "Expiration" }') .to_return(status: 200, body: '{ "Expiration": "Expiration" }') .to_return(status: 200, body: '{ "Expiration": "Expiration" }') - .to_return(status: 200, body: '{ "Expiration": "Expiration" }') expect do InstanceProfileCredentials.new(backoff: 0) - end.to raise_error(ArgumentError) + end.to raise_error( + Aws::Errors::MetadataParserError, 'Failed to parse metadata service response.' + ) end describe 'auto refreshing' do @@ -535,8 +464,8 @@ module Aws .to_raise(Errno::ECONNREFUSED) end - it 'defaults to 1' do - expect(InstanceProfileCredentials.new(backoff: 0).retries).to be(1) + it 'defaults to 3' do + expect(InstanceProfileCredentials.new(backoff: 0).retries).to be(3) end it 'keeps trying "retries" times, with exponential backoff' do From eff5edb4df54e7fa04f56ffc1ffaac19a826ec6c Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 3 Jul 2025 12:38:27 -0700 Subject: [PATCH 10/14] Revert "Global retries of 3 including parsing errors" This reverts commit 5e42b7f39da6dad18dc2a02308c32f652b23c217. --- gems/aws-sdk-core/CHANGELOG.md | 2 +- .../aws-sdk-core/credential_provider_chain.rb | 2 +- .../instance_profile_credentials.rb | 159 +++++++++++------- .../aws/credential_provider_chain_spec.rb | 53 +++--- .../aws/instance_profile_credentials_spec.rb | 83 ++++++++- 5 files changed, 200 insertions(+), 99 deletions(-) diff --git a/gems/aws-sdk-core/CHANGELOG.md b/gems/aws-sdk-core/CHANGELOG.md index d786029e58d..e89bbc64769 100644 --- a/gems/aws-sdk-core/CHANGELOG.md +++ b/gems/aws-sdk-core/CHANGELOG.md @@ -1,7 +1,7 @@ Unreleased Changes ------------------ -* Issue - Refactor `InstanceProfileCredentials` to be simpler and increase network retries to be more resilient. +* Issue - Document retry behaviors in `InstanceProfileCredentials`. 3.226.2 (2025-07-01) ------------------ diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb b/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb index 25163f33e1c..2efebeaaef1 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb @@ -191,7 +191,7 @@ def instance_profile_credentials(options) if ENV['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] || ENV['AWS_CONTAINER_CREDENTIALS_FULL_URI'] ECSCredentials.new(options) - elsif !(ENV.fetch('AWS_EC2_METADATA_DISABLED', 'false').downcase == 'true') + else InstanceProfileCredentials.new(options.merge(profile: profile_name)) end end diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb b/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb index 922dbe7331f..a2d9fba5cde 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb @@ -10,9 +10,15 @@ module Aws # ec2 = Aws::EC2::Client.new(credentials: instance_credentials) # # ## Retries - # # When initialized from the default credential chain, this provider defaults to `0` retries. - # Otherwise, it defaults to 3 retries and will retry network and parsing errors. + # Breakdown of retries is as follows: + # + # * **Configurable retries** (defaults to `1`): these retries handle errors when communicating + # with the IMDS endpoint. There are two separate retry mechanisms within the provider: + # * Entire token fetch and credential retrieval process + # * Token fetching + # * **JSON parsing retries**: Fixed at 3 attempts to handle cases when IMDS returns malformed JSON + # responses. These retries are separate from configurable retries. # # @see https://docs.aws.amazon.com/sdkref/latest/guide/feature-imds-credentials.html IMDS Credential Provider class InstanceProfileCredentials @@ -22,15 +28,24 @@ class InstanceProfileCredentials # @api private class Non200Response < RuntimeError; end - # @deprecated Unfortunate spelling name. # @api private class TokenRetrivalError < RuntimeError; end # @api private - class TokenRetrievalError < TokenRetrivalError; end + class TokenExpiredError < RuntimeError; end + # These are the errors we trap when attempting to talk to the instance metadata service. + # Any of these imply the service is not present, no responding or some other non-recoverable error. # @api private - class TokenExpiredError < RuntimeError; end + NETWORK_ERRORS = [ + Errno::EHOSTUNREACH, + Errno::ECONNREFUSED, + Errno::EHOSTDOWN, + Errno::ENETUNREACH, + SocketError, + Timeout::Error, + Non200Response + ].freeze # Path base for GET request for profile and credentials # @api private @@ -41,10 +56,9 @@ class TokenExpiredError < RuntimeError; end METADATA_TOKEN_PATH = '/latest/api/token'.freeze # @param [Hash] options - # @option options [Integer] :retries (3) Number of times to retry when retrieving credentials. Defaults to 0 when - # resolving from the default credential chain. + # @option options [Integer] :retries (1) Number of times to retry when retrieving credentials. # @option options [String] :endpoint ('http://169.254.169.254') The IMDS endpoint. This option has precedence - # over the `:endpoint_mode`. + # over the `:endpoint_mode`. # @option options [String] :endpoint_mode ('IPv4') The endpoint mode for the instance metadata service. This is # either 'IPv4' (`169.254.169.254`) or IPv6' (`[fd00:ec2::254]`). # @option options [Boolean] :disable_imds_v1 (false) Disable the use of the legacy EC2 Metadata Service v1. @@ -70,19 +84,20 @@ def initialize(options = {}) @http_read_timeout = options[:http_read_timeout] || 1 @http_debug_output = options[:http_debug_output] @port = options[:port] || 80 - @retries = options[:retries] || 3 + @retries = options[:retries] || 1 @token_ttl = options[:token_ttl] || 21_600 + @async_refresh = false + # Flag for if v2 flow fails, skip future attempts @imds_v1_fallback = false - @token = nil @no_refresh_until = nil - - @async_refresh = false + @token = nil @metrics = ['CREDENTIALS_IMDS'] super end - # @return [Integer] + # @return [Integer] Number of times to retry when retrieving credentials from the instance metadata service. + # Defaults to 0 when resolving from the default credential chain. attr_reader :retries private @@ -139,42 +154,73 @@ def refresh return end - # Retry loading and parsing credentials up to a configurable number of times. - # StandardError is not ideal but it covers Net::HTTP errors. - # https://gist.github.com/tenderlove/245188 - # ArgumentError can be raised when parsing a bad expiration. - retry_errors([Aws::Json::ParseError, ArgumentError, StandardError, Non200Response]) do - open_connection do |conn| - # attempt to fetch token to start secure flow first, and rescue to failover - fetch_token(conn) unless @imds_v1_fallback || token_set? - # disable insecure flow if we couldn't get token and imds v1 is disabled - raise TokenRetrievalError if @token.nil? && @disable_imds_v1 - - creds = Aws::Json.load(fetch_credentials(conn)) - if @credentials&.set? && empty_credentials?(creds) - # credentials are already set, but there was an error getting new credentials - # so don't update the credentials and use stale ones (static stability) - @no_refresh_until = Time.now + rand(300..360) - warn_expired_credentials - else - # credentials are empty or successfully retrieved, update them - update_credentials(creds) + new_creds = + begin + # Retry loading credentials up to 3 times is the instance metadata + # service is responding but is returning invalid JSON documents + # in response to the GET profile credentials call. + retry_errors([Aws::Json::ParseError], max_retries: 3) do + Aws::Json.load(retrieve_credentials.to_s) end + rescue Aws::Json::ParseError + raise Aws::Errors::MetadataParserError end + + if !empty_credentials?(@credentials) && (!new_creds['AccessKeyId'] || new_creds['AccessKeyId'].empty?) + # credentials are already set, but there was an error getting new credentials + # so don't update the credentials and use stale ones (static stability) + @no_refresh_until = Time.now + rand(300..360) + warn_expired_credentials + else + # credentials are empty or successfully retrieved, update them + update_credentials(new_creds) end - rescue ArgumentError, Aws::Json::ParseError - raise Aws::Errors::MetadataParserError - rescue StandardError => e - warn("Error retrieving instance profile credentials: #{e}") - '{}' + end + + def retrieve_credentials + return '{}' if ec2_metadata_disabled? + + # Retry loading credentials a configurable number of times if + # the instance metadata service is not responding. + begin + retry_errors(NETWORK_ERRORS, max_retries: @retries) do + open_connection do |conn| + # attempt to fetch token to start secure flow first + # and rescue to failover + fetch_token(conn) unless skip_token? + + # disable insecure flow if we couldn't get token and imds v1 is disabled + raise TokenRetrivalError if @token.nil? && @disable_imds_v1 + + fetch_credentials(conn) + end + end + rescue StandardError => e + warn("Error retrieving instance profile credentials: #{e}") + '{}' + end + end + + def skip_token? + @imds_v1_fallback || (@token && !@token.expired?) + end + + def update_credentials(creds) + @credentials = Credentials.new(creds['AccessKeyId'], creds['SecretAccessKey'], creds['Token']) + @expiration = creds['Expiration'] ? Time.iso8601(creds['Expiration']) : nil + return unless @expiration && @expiration < Time.now + + @no_refresh_until = Time.now + rand(300..360) + warn_expired_credentials end def fetch_token(conn) created_time = Time.now token_value, ttl = http_put(conn) @token = Token.new(token_value, ttl, created_time) if token_value && ttl - rescue StandardError, Non200Response - # Token attempt failed, reset token fallback to insecure mode. + rescue *NETWORK_ERRORS + # token attempt failed, reset token + # fallback to non-token mode @imds_v1_fallback = true end @@ -183,7 +229,8 @@ def fetch_credentials(conn) profile_name = metadata.lines.first.strip http_get(conn, METADATA_PATH_BASE + profile_name) rescue TokenExpiredError - # Token has expired, reset it. The next retry should fetch it + # Token has expired, reset it + # The next retry should fetch it @token = nil @imds_v1_fallback = false raise Non200Response @@ -193,13 +240,8 @@ def token_set? @token && !@token.expired? end - def update_credentials(creds) - @credentials = Credentials.new(creds['AccessKeyId'], creds['SecretAccessKey'], creds['Token']) - @expiration = creds['Expiration'] ? Time.iso8601(creds['Expiration']) : nil - return unless @expiration && @expiration < Time.now - - @no_refresh_until = Time.now + rand(300..360) - warn_expired_credentials + def ec2_metadata_disabled? + ENV.fetch('AWS_EC2_METADATA_DISABLED', 'false').downcase == 'true' end def open_connection @@ -242,32 +284,33 @@ def http_put(connection) response.header['x-aws-ec2-metadata-token-ttl-seconds'].to_i ] when 400 - raise TokenRetrievalError + raise TokenRetrivalError else raise Non200Response end end - def retry_errors(error_classes, &_block) - attempts = 0 + def retry_errors(error_classes, options = {}, &_block) + max_retries = options[:max_retries] + retries = 0 begin yield - rescue *error_classes => e - raise unless attempts < @retries + rescue *error_classes + raise unless retries < max_retries - @backoff.call(attempts) - attempts += 1 + @backoff.call(retries) + retries += 1 retry end 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.') + 'A refresh of these credentials will be attempted again in 5 minutes.') end - def empty_credentials?(creds_hash) - !creds_hash['AccessKeyId'] || creds_hash['AccessKeyId'].empty? + def empty_credentials?(creds) + creds.nil? || !creds.set? end # @api private diff --git a/gems/aws-sdk-core/spec/aws/credential_provider_chain_spec.rb b/gems/aws-sdk-core/spec/aws/credential_provider_chain_spec.rb index df9ee1939b6..737494beb71 100644 --- a/gems/aws-sdk-core/spec/aws/credential_provider_chain_spec.rb +++ b/gems/aws-sdk-core/spec/aws/credential_provider_chain_spec.rb @@ -15,15 +15,16 @@ def random_creds end def with_shared_credentials(profile_name = SecureRandom.hex, credentials_file = nil) - path = File.expand_path(File.join('HOME', '.aws', 'credentials')) + path = File.expand_path( + File.join('HOME', '.aws', 'credentials')) creds = random_creds - credentials_file ||= <<~CREDS - [#{profile_name}] - aws_access_key_id = #{creds[:access_key_id]} - aws_secret_access_key = #{creds[:secret_access_key]} - aws_session_token = #{creds[:session_token]} - aws_account_id = #{creds[:account_id]} - CREDS + credentials_file ||= <<-CREDS +[#{profile_name}] +aws_access_key_id = #{creds[:access_key_id]} +aws_secret_access_key = #{creds[:secret_access_key]} +aws_session_token = #{creds[:session_token]} +aws_account_id = #{creds[:account_id]} +CREDS allow(Dir).to receive(:home).and_return('HOME') allow(File).to receive(:exist?).with(path).and_return(true) allow(File).to receive(:readable?).with(path).and_return(true) @@ -63,17 +64,15 @@ def validate_metrics(*expected_metrics) end let(:config) do - double( - 'config', - access_key_id: nil, - secret_access_key: nil, - session_token: nil, - account_id: nil, - profile: nil, - region: nil, - instance_profile_credentials_timeout: 1, - instance_profile_credentials_retries: 0 - ) + double('config', + access_key_id: nil, + secret_access_key: nil, + session_token: nil, + account_id: nil, + profile: nil, + region: nil, + instance_profile_credentials_timeout: 1, + instance_profile_credentials_retries: 0) end let(:mock_instance_creds) { double('InstanceProfileCredentials', set?: false) } @@ -134,18 +133,6 @@ def validate_metrics(*expected_metrics) validate_metrics('CREDENTIALS_IMDS') end - it 'skips instance profile service when AWS_EC2_METADATA_DISABLED is true' do - ENV['AWS_EC2_METADATA_DISABLED'] = 'true' - expect(InstanceProfileCredentials).not_to receive(:new) - expect(credentials).to be(nil) - end - - it 'AWS_EC2_METADATA_DISABLED is not case sensitive' do - ENV['AWS_EC2_METADATA_DISABLED'] = 'TrUe' - expect(InstanceProfileCredentials).not_to receive(:new) - expect(credentials).to be(nil) - end - it 'hydrates credentials from ECS when AWS_CONTAINER_CREDENTIALS_RELATIVE_URI is set' do ENV['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] = 'test_uri' mock_ecs_creds = double('ECSCredentials', metrics: ['CREDENTIALS_HTTP'], set?: true) @@ -207,7 +194,7 @@ def validate_metrics(*expected_metrics) end it 'hydrates credentials from config over ENV' do - with_env_credentials + env_creds = with_env_credentials expected_creds = with_config_credentials validate_credentials(expected_creds) validate_metrics('CREDENTIALS_PROFILE') @@ -216,7 +203,7 @@ def validate_metrics(*expected_metrics) it 'hydrates credentials from profile when config set over ENV' do expected_creds = with_shared_credentials allow(config).to receive(:profile).and_return(expected_creds[:profile_name]) - with_env_credentials + env_creds = with_env_credentials validate_credentials(expected_creds) validate_metrics('CREDENTIALS_PROFILE') end diff --git a/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb b/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb index 5e7b21d0dd5..fa7a2c9aa24 100644 --- a/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb +++ b/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb @@ -243,6 +243,73 @@ module Aws end end + describe 'disable IMDS flag' do + it 'does not attempt to get credentials when disable flag set' do + ENV['AWS_EC2_METADATA_DISABLED'] = 'true' + expect(InstanceProfileCredentials.new.set?).to be(false) + end + + it 'has a disable flag which is not case sensitive' do + ENV['AWS_EC2_METADATA_DISABLED'] = 'TrUe' + expect(InstanceProfileCredentials.new.set?).to be(false) + end + + it 'ignores values other than true for the disable flag (secure)' do + ENV['AWS_EC2_METADATA_DISABLED'] = '1' + expiration = Time.now.utc + 3600 + resp = <<-JSON.strip + { + "Code" : "Success", + "LastUpdated" : "2013-11-22T20:03:48Z", + "Type" : "AWS-HMAC", + "AccessKeyId" : "akid", + "SecretAccessKey" : "secret", + "Token" : "session-token", + "Expiration" : "#{expiration.strftime('%Y-%m-%dT%H:%M:%SZ')}" + } + JSON + stub_request(:put, ipv4_endpoint + token_path) + .to_return( + status: 200, + body: "my-token\n", + headers: { 'x-aws-ec2-metadata-token-ttl-seconds' => '21600' } + ) + stub_request(:get, ipv4_endpoint + path) + .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) + .to_return(status: 200, body: "profile-name\n") + stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") + .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) + .to_return(status: 200, body: resp) + c = InstanceProfileCredentials.new(backoff: 0) + expect(c.credentials.access_key_id).to eq('akid') + expect(c.credentials.secret_access_key).to eq('secret') + expect(c.credentials.session_token).to eq('session-token') + end + + it 'ignores values other than true for the disable flag (insecure)' do + ENV['AWS_EC2_METADATA_DISABLED'] = '1' + expiration = Time.now.utc + 3600 + resp = <<-JSON.strip + { + "Code" : "Success", + "LastUpdated" : "2013-11-22T20:03:48Z", + "Type" : "AWS-HMAC", + "AccessKeyId" : "akid", + "SecretAccessKey" : "secret", + "Token" : "session-token", + "Expiration" : "#{expiration.strftime('%Y-%m-%dT%H:%M:%SZ')}" + } + JSON + stub_request(:put, ipv4_endpoint + token_path).to_return(status: 404) + stub_request(:get, ipv4_endpoint + path).to_return(status: 200, body: "profile-name\n") + stub_request(:get, "#{ipv4_endpoint}#{path}profile-name").to_return(status: 200, body: resp) + c = InstanceProfileCredentials.new(backoff: 0) + expect(c.credentials.access_key_id).to eq('akid') + expect(c.credentials.secret_access_key).to eq('secret') + expect(c.credentials.session_token).to eq('session-token') + end + end + describe 'disable IMDS v1 flag' do before do ENV['AWS_EC2_METADATA_V1_DISABLED'] = 'true' @@ -375,6 +442,7 @@ module Aws stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: ' ') + .to_return(status: 200, body: '') .to_return(status: 200, body: '{') .to_return(status: 200, body: resp2) c = InstanceProfileCredentials.new(backoff: 0) @@ -387,33 +455,36 @@ module Aws it 'retries invalid JSON exactly 3 times' do stub_request(:get, ipv4_endpoint + path) .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) + .to_return(status: 500) .to_return(status: 200, body: "profile-name\n") stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: '') .to_return(status: 200, body: ' ') .to_return(status: 200, body: '{') + .to_return(status: 200, body: ' ') expect do InstanceProfileCredentials.new(backoff: 0) end.to raise_error( - Aws::Errors::MetadataParserError, 'Failed to parse metadata service response.' + Aws::Errors::MetadataParserError, + 'Failed to parse metadata service response.' ) end it 'retries errors parsing expiration time 3 times' do stub_request(:get, ipv4_endpoint + path) .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) + .to_return(status: 500) .to_return(status: 200, body: "profile-name\n") stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: '{ "Expiration": "Expiration" }') .to_return(status: 200, body: '{ "Expiration": "Expiration" }') .to_return(status: 200, body: '{ "Expiration": "Expiration" }') + .to_return(status: 200, body: '{ "Expiration": "Expiration" }') expect do InstanceProfileCredentials.new(backoff: 0) - end.to raise_error( - Aws::Errors::MetadataParserError, 'Failed to parse metadata service response.' - ) + end.to raise_error(ArgumentError) end describe 'auto refreshing' do @@ -464,8 +535,8 @@ module Aws .to_raise(Errno::ECONNREFUSED) end - it 'defaults to 3' do - expect(InstanceProfileCredentials.new(backoff: 0).retries).to be(3) + it 'defaults to 1' do + expect(InstanceProfileCredentials.new(backoff: 0).retries).to be(1) end it 'keeps trying "retries" times, with exponential backoff' do From 42a49bee8ad130a9b33c2ee0accbe509675345c3 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 3 Jul 2025 12:52:16 -0700 Subject: [PATCH 11/14] Feedbacks --- .../aws-sdk-core/credential_provider_chain.rb | 2 +- .../instance_profile_credentials.rb | 44 +++++++++++----- .../aws/credential_provider_chain_spec.rb | 50 ++++++++++++------- 3 files changed, 64 insertions(+), 32 deletions(-) diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb b/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb index 2efebeaaef1..25163f33e1c 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb @@ -191,7 +191,7 @@ def instance_profile_credentials(options) if ENV['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] || ENV['AWS_CONTAINER_CREDENTIALS_FULL_URI'] ECSCredentials.new(options) - else + elsif !(ENV.fetch('AWS_EC2_METADATA_DISABLED', 'false').downcase == 'true') InstanceProfileCredentials.new(options.merge(profile: profile_name)) end end diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb b/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb index a2d9fba5cde..56398d228cd 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb @@ -77,7 +77,7 @@ class TokenExpiredError < RuntimeError; end # @option options [Callable] :before_refresh Proc called before credentials are refreshed. `before_refresh` # is called with an instance of this object when AWS credentials are required and need to be refreshed. def initialize(options = {}) - @backoff = backoff(options[:backoff]) + @backoff = resolve_backoff(options[:backoff]) @disable_imds_v1 = resolve_disable_v1(options) @endpoint = resolve_endpoint(options) @http_open_timeout = options[:http_open_timeout] || 1 @@ -88,7 +88,6 @@ def initialize(options = {}) @token_ttl = options[:token_ttl] || 21_600 @async_refresh = false - # Flag for if v2 flow fails, skip future attempts @imds_v1_fallback = false @no_refresh_until = nil @token = nil @@ -96,10 +95,33 @@ def initialize(options = {}) super end - # @return [Integer] Number of times to retry when retrieving credentials from the instance metadata service. - # Defaults to 0 when resolving from the default credential chain. + # @return [Boolean0 + attr_reader :disable_imds_v1 + + # @return [Integer] + attr_reader :token_ttl + + # @return [Integer] attr_reader :retries + # @return [Proc] + attr_reader :backoff + + # @return [String] + attr_reader :endpoint + + # @return [Integer] + attr_reader :port + + # @return [Integer] + attr_reader :http_open_timeout + + # @return [Integer] + attr_reader :http_read_timeout + + # @return [IO, nil] + attr_reader :http_debug_output + private def resolve_endpoint_mode(options) @@ -140,7 +162,7 @@ def resolve_disable_v1(options) Aws::Util.str_2_bool(value.to_s.downcase) end - def backoff(backoff) + def resolve_backoff(backoff) case backoff when Proc then backoff when Numeric then ->(_) { sleep(backoff) } @@ -166,7 +188,7 @@ def refresh raise Aws::Errors::MetadataParserError end - if !empty_credentials?(@credentials) && (!new_creds['AccessKeyId'] || new_creds['AccessKeyId'].empty?) + if @credentials&.set? && empty_credentials?(new_creds) # credentials are already set, but there was an error getting new credentials # so don't update the credentials and use stale ones (static stability) @no_refresh_until = Time.now + rand(300..360) @@ -187,7 +209,7 @@ def retrieve_credentials open_connection do |conn| # attempt to fetch token to start secure flow first # and rescue to failover - fetch_token(conn) unless skip_token? + fetch_token(conn) unless @imds_v1_fallback || (@token && !@token.expired?) # disable insecure flow if we couldn't get token and imds v1 is disabled raise TokenRetrivalError if @token.nil? && @disable_imds_v1 @@ -201,10 +223,6 @@ def retrieve_credentials end end - def skip_token? - @imds_v1_fallback || (@token && !@token.expired?) - end - def update_credentials(creds) @credentials = Credentials.new(creds['AccessKeyId'], creds['SecretAccessKey'], creds['Token']) @expiration = creds['Expiration'] ? Time.iso8601(creds['Expiration']) : nil @@ -309,8 +327,8 @@ def warn_expired_credentials 'A refresh of these credentials will be attempted again in 5 minutes.') end - def empty_credentials?(creds) - creds.nil? || !creds.set? + def empty_credentials?(creds_hash) + !creds_hash['AccessKeyId'] || creds_hash['AccessKeyId'].empty? end # @api private diff --git a/gems/aws-sdk-core/spec/aws/credential_provider_chain_spec.rb b/gems/aws-sdk-core/spec/aws/credential_provider_chain_spec.rb index 737494beb71..cad7ea4ba2a 100644 --- a/gems/aws-sdk-core/spec/aws/credential_provider_chain_spec.rb +++ b/gems/aws-sdk-core/spec/aws/credential_provider_chain_spec.rb @@ -18,13 +18,13 @@ def with_shared_credentials(profile_name = SecureRandom.hex, credentials_file = path = File.expand_path( File.join('HOME', '.aws', 'credentials')) creds = random_creds - credentials_file ||= <<-CREDS -[#{profile_name}] -aws_access_key_id = #{creds[:access_key_id]} -aws_secret_access_key = #{creds[:secret_access_key]} -aws_session_token = #{creds[:session_token]} -aws_account_id = #{creds[:account_id]} -CREDS + credentials_file ||= <<~CREDS + [#{profile_name}] + aws_access_key_id = #{creds[:access_key_id]} + aws_secret_access_key = #{creds[:secret_access_key]} + aws_session_token = #{creds[:session_token]} + aws_account_id = #{creds[:account_id]} + CREDS allow(Dir).to receive(:home).and_return('HOME') allow(File).to receive(:exist?).with(path).and_return(true) allow(File).to receive(:readable?).with(path).and_return(true) @@ -64,15 +64,17 @@ def validate_metrics(*expected_metrics) end let(:config) do - double('config', - access_key_id: nil, - secret_access_key: nil, - session_token: nil, - account_id: nil, - profile: nil, - region: nil, - instance_profile_credentials_timeout: 1, - instance_profile_credentials_retries: 0) + double( + 'config', + access_key_id: nil, + secret_access_key: nil, + session_token: nil, + account_id: nil, + profile: nil, + region: nil, + instance_profile_credentials_timeout: 1, + instance_profile_credentials_retries: 0 + ) end let(:mock_instance_creds) { double('InstanceProfileCredentials', set?: false) } @@ -133,6 +135,18 @@ def validate_metrics(*expected_metrics) validate_metrics('CREDENTIALS_IMDS') end + it 'skips instance profile service when AWS_EC2_METADATA_DISABLED is true' do + ENV['AWS_EC2_METADATA_DISABLED'] = 'true' + expect(InstanceProfileCredentials).not_to receive(:new) + expect(credentials).to be(nil) + end + + it 'AWS_EC2_METADATA_DISABLED is not case sensitive' do + ENV['AWS_EC2_METADATA_DISABLED'] = 'TrUe' + expect(InstanceProfileCredentials).not_to receive(:new) + expect(credentials).to be(nil) + end + it 'hydrates credentials from ECS when AWS_CONTAINER_CREDENTIALS_RELATIVE_URI is set' do ENV['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] = 'test_uri' mock_ecs_creds = double('ECSCredentials', metrics: ['CREDENTIALS_HTTP'], set?: true) @@ -194,7 +208,7 @@ def validate_metrics(*expected_metrics) end it 'hydrates credentials from config over ENV' do - env_creds = with_env_credentials + with_env_credentials expected_creds = with_config_credentials validate_credentials(expected_creds) validate_metrics('CREDENTIALS_PROFILE') @@ -203,7 +217,7 @@ def validate_metrics(*expected_metrics) it 'hydrates credentials from profile when config set over ENV' do expected_creds = with_shared_credentials allow(config).to receive(:profile).and_return(expected_creds[:profile_name]) - env_creds = with_env_credentials + with_env_credentials validate_credentials(expected_creds) validate_metrics('CREDENTIALS_PROFILE') end From 7d184a74382da72f7fa3fd74431b58dc94ae08b6 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 3 Jul 2025 12:57:23 -0700 Subject: [PATCH 12/14] Fix specs --- .../aws/instance_profile_credentials_spec.rb | 90 ++++++++++--------- 1 file changed, 46 insertions(+), 44 deletions(-) diff --git a/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb b/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb index fa7a2c9aa24..3b62fa60614 100644 --- a/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb +++ b/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb @@ -8,6 +8,8 @@ module Aws let(:token_path) { '/latest/api/token' } let(:ipv4_endpoint) { 'http://169.254.169.254' } let(:ipv6_endpoint) { 'http://[fd00:ec2::254]' } + let(:ipv4_endpoint_token_path) { ipv4_endpoint + token_path } + let(:ipv4_endpoint_creds_path) { ipv4_endpoint + path } before do allow_any_instance_of(InstanceProfileCredentials).to receive(:warn) @@ -20,27 +22,27 @@ module Aws it 'mode is ipv4 by default' do subject = InstanceProfileCredentials.new - expect(subject.instance_variable_get(:@endpoint)).to eq ipv4_endpoint + expect(subject.endpoint).to eq ipv4_endpoint end it 'can be configured with shared config' do allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint_mode).and_return('IPv6') subject = InstanceProfileCredentials.new - expect(subject.instance_variable_get(:@endpoint)).to eq ipv6_endpoint + expect(subject.endpoint).to eq ipv6_endpoint end it 'can be configured using env variable with precedence' do ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE'] = 'IPv4' allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint_mode).and_return('IPv6') subject = InstanceProfileCredentials.new - expect(subject.instance_variable_get(:@endpoint)).to eq ipv4_endpoint + expect(subject.endpoint).to eq ipv4_endpoint end it 'can be configure through code with precedence' do ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE'] = 'IPv4' allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint_mode).and_return('IPv4') subject = InstanceProfileCredentials.new(endpoint_mode: 'IPv6') - expect(subject.instance_variable_get(:@endpoint)).to eq ipv6_endpoint + expect(subject.endpoint).to eq ipv6_endpoint end it 'raises ArgumentError when endpoint mode is unexpected' do @@ -57,20 +59,20 @@ module Aws it 'can be configured with shared config' do allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint).and_return(endpoint) - expect(subject.instance_variable_get(:@endpoint)).to eq endpoint + expect(subject.endpoint).to eq endpoint end it 'can be configured using env variable with precedence' do ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT'] = endpoint allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint_mode).and_return(endpoint) - expect(subject.instance_variable_get(:@endpoint)).to eq endpoint + expect(subject.endpoint).to eq endpoint end it 'can be configured through code with precedence' do allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint).and_return('bar-example.com') ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT'] = 'foo-example.com' subject = InstanceProfileCredentials.new(ip_address: endpoint) - expect(subject.instance_variable_get(:@endpoint)).to eq endpoint + expect(subject.endpoint).to eq endpoint end it 'overrides endpoint mode configuration with ENV' do @@ -78,7 +80,7 @@ module Aws allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint_mode).and_return('IPv4') ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT'] = endpoint subject = InstanceProfileCredentials.new(endpoint_mode: 'IPv4') - expect(subject.instance_variable_get(:@endpoint)).to eq endpoint + expect(subject.endpoint).to eq endpoint end it 'overrides endpoint mode configuration with shared config' do @@ -86,14 +88,14 @@ module Aws allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint_mode).and_return('IPv4') allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint).and_return(endpoint) subject = InstanceProfileCredentials.new(endpoint_mode: 'IPv4') - expect(subject.instance_variable_get(:@endpoint)).to eq endpoint + expect(subject.endpoint).to eq endpoint end it 'overrides endpoint mode configuration with code' do ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE'] = 'IPv4' allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint_mode).and_return('IPv4') subject = InstanceProfileCredentials.new(endpoint_mode: 'IPv4', endpoint: endpoint) - expect(subject.instance_variable_get(:@endpoint)).to eq endpoint + expect(subject.endpoint).to eq endpoint end end @@ -101,12 +103,12 @@ module Aws let(:ipv4_endpoint) { 'http://123.123.123.123:9001' } before do - stub_request(:put, "#{ipv4_endpoint}#{token_path}") + stub_request(:put, ipv4_endpoint_token_path) .to_return(status: 200, body: "my-token\n", headers: { 'x-aws-ec2-metadata-token-ttl-seconds' => '21600' }) - stub_request(:get, "#{ipv4_endpoint}#{path}") + stub_request(:get, ipv4_endpoint_creds_path) .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint_creds_path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: '{}') end @@ -139,20 +141,20 @@ module Aws it 'can be configured with shared config' do allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_v1_disabled).and_return(disable_imds_v1.to_s) - expect(subject.instance_variable_get(:@disable_imds_v1)).to eq disable_imds_v1 + expect(subject.disable_imds_v1).to eq disable_imds_v1 end it 'can be configured using env variable with precedence' do ENV['AWS_EC2_METADATA_V1_DISABLED'] = disable_imds_v1.to_s allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_v1_disabled).and_return('false') - expect(subject.instance_variable_get(:@disable_imds_v1)).to eq disable_imds_v1 + expect(subject.disable_imds_v1).to eq disable_imds_v1 end it 'can be configured through code with precedence' do allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_v1_disabled).and_return('false') ENV['AWS_EC2_METADATA_V1_DISABLED'] = 'false' subject = InstanceProfileCredentials.new(disable_imds_v1: disable_imds_v1) - expect(subject.instance_variable_get(:@disable_imds_v1)).to eq disable_imds_v1 + expect(subject.disable_imds_v1).to eq disable_imds_v1 end end @@ -164,7 +166,7 @@ module Aws Timeout::Error ].each do |error_class| it "returns no credentials for #{error_class}" do - stub_request(:put, ipv4_endpoint + token_path).to_return(status: 200, body: 'mytoken') + stub_request(:put, ipv4_endpoint_token_path).to_return(status: 200, body: 'mytoken') stub_request(:get, ipv4_endpoint + path).to_raise(error_class) expect(InstanceProfileCredentials.new(backoff: 0).set?).to be(false) end @@ -175,7 +177,7 @@ module Aws 401 ].each do |error_code| it "returns no credentials for #{error_code} when fetching token" do - stub_request(:put, ipv4_endpoint + token_path).to_return(status: error_code) + stub_request(:put, ipv4_endpoint_token_path).to_return(status: error_code) stub_request(:get, ipv4_endpoint + path).to_return(status: 200) expect(InstanceProfileCredentials.new(backoff: 0).set?).to be(false) end @@ -202,9 +204,9 @@ module Aws 404 ].each do |error_code| it "fails over to insecure flow for error code #{error_code}" do - stub_request(:put, ipv4_endpoint + token_path).to_return(status: error_code) + stub_request(:put, ipv4_endpoint_token_path).to_return(status: error_code) stub_request(:get, ipv4_endpoint + path).to_return(status: 200, body: "profile-name\n") - stub_request(:get, "#{ipv4_endpoint}#{path}profile-name").to_return(status: 200, body: resp) + stub_request(:get, "#{ipv4_endpoint_creds_path}profile-name").to_return(status: 200, body: resp) c = InstanceProfileCredentials.new(backoff: 0) expect(c.credentials.access_key_id).to eq('akid') expect(c.credentials.secret_access_key).to eq('secret') @@ -219,9 +221,9 @@ module Aws Timeout::Error ].each do |error_class| it "fails over to insecure flow for #{error_class}" do - stub_request(:put, ipv4_endpoint + token_path).to_raise(error_class) + stub_request(:put, ipv4_endpoint_token_path).to_raise(error_class) stub_request(:get, ipv4_endpoint + path).to_return(status: 200, body: "profile-name\n") - stub_request(:get, "#{ipv4_endpoint}#{path}profile-name").to_return(status: 200, body: resp) + stub_request(:get, "#{ipv4_endpoint_creds_path}profile-name").to_return(status: 200, body: resp) c = InstanceProfileCredentials.new(backoff: 0) expect(c.credentials.access_key_id).to eq('akid') expect(c.credentials.secret_access_key).to eq('secret') @@ -230,9 +232,9 @@ module Aws end it 'memoizes v1 fallback' do - token_stub = stub_request(:put, ipv4_endpoint + token_path).to_return(status: 403) + token_stub = stub_request(:put, ipv4_endpoint_token_path).to_return(status: 403) profile_name_stub = stub_request(:get, ipv4_endpoint + path).to_return(status: 200, body: "profile-name\n") - credentials_stub = stub_request(:get, "#{ipv4_endpoint}#{path}profile-name").to_return(status: 200, body: resp) + credentials_stub = stub_request(:get, "#{ipv4_endpoint_creds_path}profile-name").to_return(status: 200, body: resp) c = InstanceProfileCredentials.new(backoff: 0, retries: 0) c.refresh! @@ -268,7 +270,7 @@ module Aws "Expiration" : "#{expiration.strftime('%Y-%m-%dT%H:%M:%SZ')}" } JSON - stub_request(:put, ipv4_endpoint + token_path) + stub_request(:put, ipv4_endpoint_token_path) .to_return( status: 200, body: "my-token\n", @@ -277,7 +279,7 @@ module Aws stub_request(:get, ipv4_endpoint + path) .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint_creds_path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: resp) c = InstanceProfileCredentials.new(backoff: 0) @@ -300,9 +302,9 @@ module Aws "Expiration" : "#{expiration.strftime('%Y-%m-%dT%H:%M:%SZ')}" } JSON - stub_request(:put, ipv4_endpoint + token_path).to_return(status: 404) + stub_request(:put, ipv4_endpoint_token_path).to_return(status: 404) stub_request(:get, ipv4_endpoint + path).to_return(status: 200, body: "profile-name\n") - stub_request(:get, "#{ipv4_endpoint}#{path}profile-name").to_return(status: 200, body: resp) + stub_request(:get, "#{ipv4_endpoint_creds_path}profile-name").to_return(status: 200, body: resp) c = InstanceProfileCredentials.new(backoff: 0) expect(c.credentials.access_key_id).to eq('akid') expect(c.credentials.secret_access_key).to eq('secret') @@ -318,11 +320,11 @@ module Aws it 'has a disable flag which is not case sensitive' do ENV['AWS_EC2_METADATA_DISABLED'] = 'TrUe' c = InstanceProfileCredentials.new(backoff: 0) - expect(c.instance_variable_get(:@disable_imds_v1)).to be(true) + expect(c.disable_imds_v1).to be(true) end it 'does not attempt to get credentials (insecure)' do - stub_request(:put, ipv4_endpoint + token_path).to_return(status: 404) + stub_request(:put, ipv4_endpoint_token_path).to_return(status: 404) expect(InstanceProfileCredentials.new(backoff: 0).set?).to be(false) end @@ -339,7 +341,7 @@ module Aws "Expiration" : "#{expiration.strftime('%Y-%m-%dT%H:%M:%SZ')}" } JSON - stub_request(:put, ipv4_endpoint + token_path) + stub_request(:put, ipv4_endpoint_token_path) .to_return( status: 200, body: "my-token\n", @@ -348,7 +350,7 @@ module Aws stub_request(:get, ipv4_endpoint + path) .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint_creds_path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: resp) c = InstanceProfileCredentials.new(backoff: 0) @@ -387,7 +389,7 @@ module Aws JSON before(:each) do - stub_request(:put, ipv4_endpoint + token_path) + stub_request(:put, ipv4_endpoint_token_path) .to_return( status: 200, body: "my-token\n", @@ -396,7 +398,7 @@ module Aws stub_request(:get, ipv4_endpoint + path) .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint_creds_path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: resp) .to_return(status: 200, body: resp2) @@ -424,7 +426,7 @@ module Aws .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 500) .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint_creds_path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: resp2) c = InstanceProfileCredentials.new(backoff: 0) @@ -439,7 +441,7 @@ module Aws .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 500) .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint_creds_path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: ' ') .to_return(status: 200, body: '') @@ -457,7 +459,7 @@ module Aws .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 500) .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint_creds_path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: '') .to_return(status: 200, body: ' ') @@ -476,7 +478,7 @@ module Aws .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 500) .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint_creds_path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: '{ "Expiration": "Expiration" }') .to_return(status: 200, body: '{ "Expiration": "Expiration" }') @@ -506,7 +508,7 @@ module Aws it 'given an empty response, entry credentials are returned' do # This handles the case when the service response but returns # a JSON document without credentials (error cases) - stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint_creds_path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: resp) c = InstanceProfileCredentials.new @@ -521,7 +523,7 @@ module Aws describe '#retries' do before(:each) do - stub_request(:put, ipv4_endpoint + token_path) + stub_request(:put, ipv4_endpoint_token_path) .to_return( status: 200, body: "my-token\n", @@ -530,7 +532,7 @@ module Aws stub_request(:get, ipv4_endpoint + path) .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_raise(Errno::ECONNREFUSED) - stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint_creds_path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_raise(Errno::ECONNREFUSED) end @@ -578,7 +580,7 @@ module Aws JSON before(:each) do - stub_request(:put, ipv4_endpoint + token_path) + stub_request(:put, ipv4_endpoint_token_path) .to_return( status: 200, body: "my-token\n", @@ -593,7 +595,7 @@ module Aws expect_any_instance_of(InstanceProfileCredentials).to receive(:warn).at_least(:once) expected_request = - stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint_creds_path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: expired_resp) @@ -613,7 +615,7 @@ module Aws it 'provides credentials after a read timeout during a refresh' do expect_any_instance_of(InstanceProfileCredentials).to receive(:warn).at_least(:once) expected_request = - stub_request(:get, "#{ipv4_endpoint}#{path}profile-name") + stub_request(:get, "#{ipv4_endpoint_creds_path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: near_expiration_resp) .to_raise(Timeout::Error) From 97beabe87e4d835868b53a153ab4f510680804a1 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Thu, 3 Jul 2025 13:14:15 -0700 Subject: [PATCH 13/14] Update changelog entry --- gems/aws-sdk-core/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gems/aws-sdk-core/CHANGELOG.md b/gems/aws-sdk-core/CHANGELOG.md index e89bbc64769..b273c2526ff 100644 --- a/gems/aws-sdk-core/CHANGELOG.md +++ b/gems/aws-sdk-core/CHANGELOG.md @@ -1,7 +1,7 @@ Unreleased Changes ------------------ -* Issue - Document retry behaviors in `InstanceProfileCredentials`. +* Issue - Refactor `InstanceProfileCredentials` to improve code clarity and documentation. 3.226.2 (2025-07-01) ------------------ From 016d1e3841be1a34e01004175c61f0ce2b37c095 Mon Sep 17 00:00:00 2001 From: Juli Tera Date: Mon, 7 Jul 2025 11:53:33 -0700 Subject: [PATCH 14/14] Update based on feedbacks --- gems/aws-sdk-core/CHANGELOG.md | 2 + .../instance_profile_credentials.rb | 6 -- .../aws/instance_profile_credentials_spec.rb | 69 +------------------ 3 files changed, 3 insertions(+), 74 deletions(-) diff --git a/gems/aws-sdk-core/CHANGELOG.md b/gems/aws-sdk-core/CHANGELOG.md index b273c2526ff..570acbe00e3 100644 --- a/gems/aws-sdk-core/CHANGELOG.md +++ b/gems/aws-sdk-core/CHANGELOG.md @@ -1,6 +1,8 @@ Unreleased Changes ------------------ +* Issue - Skip `Aws::InstanceProfileCredentials` instantiation when `ENV['AWS_EC2_METADATA_DISABLED']` is set to `true` in the credential resolution chain. + * Issue - Refactor `InstanceProfileCredentials` to improve code clarity and documentation. 3.226.2 (2025-07-01) diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb b/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb index 56398d228cd..6f53ebea41b 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb @@ -200,8 +200,6 @@ def refresh end def retrieve_credentials - return '{}' if ec2_metadata_disabled? - # Retry loading credentials a configurable number of times if # the instance metadata service is not responding. begin @@ -258,10 +256,6 @@ def token_set? @token && !@token.expired? end - def ec2_metadata_disabled? - ENV.fetch('AWS_EC2_METADATA_DISABLED', 'false').downcase == 'true' - end - def open_connection uri = URI.parse(@endpoint) http = Net::HTTP.new(uri.hostname || @endpoint, uri.port || @port) diff --git a/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb b/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb index 3b62fa60614..9d60d1b9896 100644 --- a/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb +++ b/gems/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb @@ -245,80 +245,13 @@ module Aws end end - describe 'disable IMDS flag' do - it 'does not attempt to get credentials when disable flag set' do - ENV['AWS_EC2_METADATA_DISABLED'] = 'true' - expect(InstanceProfileCredentials.new.set?).to be(false) - end - - it 'has a disable flag which is not case sensitive' do - ENV['AWS_EC2_METADATA_DISABLED'] = 'TrUe' - expect(InstanceProfileCredentials.new.set?).to be(false) - end - - it 'ignores values other than true for the disable flag (secure)' do - ENV['AWS_EC2_METADATA_DISABLED'] = '1' - expiration = Time.now.utc + 3600 - resp = <<-JSON.strip - { - "Code" : "Success", - "LastUpdated" : "2013-11-22T20:03:48Z", - "Type" : "AWS-HMAC", - "AccessKeyId" : "akid", - "SecretAccessKey" : "secret", - "Token" : "session-token", - "Expiration" : "#{expiration.strftime('%Y-%m-%dT%H:%M:%SZ')}" - } - JSON - stub_request(:put, ipv4_endpoint_token_path) - .to_return( - status: 200, - body: "my-token\n", - headers: { 'x-aws-ec2-metadata-token-ttl-seconds' => '21600' } - ) - stub_request(:get, ipv4_endpoint + path) - .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) - .to_return(status: 200, body: "profile-name\n") - stub_request(:get, "#{ipv4_endpoint_creds_path}profile-name") - .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) - .to_return(status: 200, body: resp) - c = InstanceProfileCredentials.new(backoff: 0) - expect(c.credentials.access_key_id).to eq('akid') - expect(c.credentials.secret_access_key).to eq('secret') - expect(c.credentials.session_token).to eq('session-token') - end - - it 'ignores values other than true for the disable flag (insecure)' do - ENV['AWS_EC2_METADATA_DISABLED'] = '1' - expiration = Time.now.utc + 3600 - resp = <<-JSON.strip - { - "Code" : "Success", - "LastUpdated" : "2013-11-22T20:03:48Z", - "Type" : "AWS-HMAC", - "AccessKeyId" : "akid", - "SecretAccessKey" : "secret", - "Token" : "session-token", - "Expiration" : "#{expiration.strftime('%Y-%m-%dT%H:%M:%SZ')}" - } - JSON - stub_request(:put, ipv4_endpoint_token_path).to_return(status: 404) - stub_request(:get, ipv4_endpoint + path).to_return(status: 200, body: "profile-name\n") - stub_request(:get, "#{ipv4_endpoint_creds_path}profile-name").to_return(status: 200, body: resp) - c = InstanceProfileCredentials.new(backoff: 0) - expect(c.credentials.access_key_id).to eq('akid') - expect(c.credentials.secret_access_key).to eq('secret') - expect(c.credentials.session_token).to eq('session-token') - end - end - describe 'disable IMDS v1 flag' do before do ENV['AWS_EC2_METADATA_V1_DISABLED'] = 'true' end it 'has a disable flag which is not case sensitive' do - ENV['AWS_EC2_METADATA_DISABLED'] = 'TrUe' + ENV['AWS_EC2_METADATA_V1_DISABLED'] = 'TrUe' c = InstanceProfileCredentials.new(backoff: 0) expect(c.disable_imds_v1).to be(true) end