diff --git a/gems/aws-sdk-core/CHANGELOG.md b/gems/aws-sdk-core/CHANGELOG.md index a94da1d2da6..570acbe00e3 100644 --- a/gems/aws-sdk-core/CHANGELOG.md +++ b/gems/aws-sdk-core/CHANGELOG.md @@ -1,6 +1,10 @@ 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/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 be47a8f87e6..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 @@ -4,11 +4,23 @@ 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) + # + # ## 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. + # + # @see https://docs.aws.amazon.com/sdkref/latest/guide/feature-imds-credentials.html IMDS Credential Provider class InstanceProfileCredentials include CredentialProvider include RefreshingCredentials @@ -22,10 +34,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,100 +56,113 @@ 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_mode = resolve_endpoint_mode(options) - @endpoint = resolve_endpoint(options, endpoint_mode) - @port = options[:port] || 80 + @backoff = resolve_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 + @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 [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) - 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) + def resolve_backoff(backoff) case backoff when Proc then backoff when Numeric then ->(_) { sleep(backoff) } @@ -153,98 +176,74 @@ 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 @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) + warn_expired_credentials + else + # credentials are empty or successfully retrieved, update them + update_credentials(new_creds) end end - def get_credentials + def retrieve_credentials # 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 @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 + + 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 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 @@ -257,10 +256,6 @@ def token_set? @token && !@token.expired? end - def _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) @@ -272,9 +267,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 @@ -288,12 +283,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 [ @@ -322,18 +317,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.") - end - - def empty_credentials?(creds) - !creds || !creds.access_key_id || creds.access_key_id.empty? + 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 - # Compute an offset for refresh with jitter - def refresh_offset - 300 + rand(0..60) + def empty_credentials?(creds_hash) + !creds_hash['AccessKeyId'] || creds_hash['AccessKeyId'].empty? end # @api private 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] 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 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..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 @@ -5,11 +5,11 @@ 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]' } + 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) @@ -22,35 +22,31 @@ 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') + 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') + 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') + 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 - expect { InstanceProfileCredentials.new(endpoint_mode: 'IPv69') } - .to raise_error(ArgumentError) + expect { InstanceProfileCredentials.new(endpoint_mode: 'IPv69') }.to raise_error(ArgumentError) end end @@ -62,55 +58,44 @@ 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) - expect(subject.instance_variable_get(:@endpoint)).to eq endpoint + allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint).and_return(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('http://124.124.124.124') - expect(subject.instance_variable_get(:@endpoint)).to eq endpoint + allow_any_instance_of(Aws::SharedConfig).to receive(:ec2_metadata_service_endpoint_mode).and_return(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') + 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 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 + expect(subject.endpoint).to eq endpoint end 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 + 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 + 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.endpoint).to eq endpoint end end @@ -118,16 +103,12 @@ module Aws let(:ipv4_endpoint) { 'http://123.123.123.123:9001' } 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' } - ) - stub_request(:get, "#{ipv4_endpoint}#{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_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 @@ -138,28 +119,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 +140,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.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.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.disable_imds_v1).to eq disable_imds_v1 end end @@ -207,9 +166,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 +177,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 +204,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_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') @@ -268,12 +221,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_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') @@ -282,12 +232,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_creds_path}profile-name").to_return(status: 200, body: resp) c = InstanceProfileCredentials.new(backoff: 0, retries: 0) c.refresh! @@ -298,90 +245,19 @@ 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, "http://169.254.169.254#{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}") - .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") - .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, "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) - 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.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, "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 +274,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_creds_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 +322,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_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) @@ -479,11 +355,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_creds_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 +370,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_creds_path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: ' ') .to_return(status: 200, body: '') @@ -512,11 +388,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_creds_path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: '') .to_return(status: 200, body: ' ') @@ -531,11 +407,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_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" }') @@ -565,7 +441,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_creds_path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: resp) c = InstanceProfileCredentials.new @@ -580,16 +456,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_creds_path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_raise(Errno::ECONNREFUSED) end @@ -599,15 +475,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 +513,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 +527,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_creds_path}profile-name") .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) .to_return(status: 200, body: expired_resp) @@ -674,10 +547,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_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) provider = InstanceProfileCredentials.new(backoff: 0, retries: 0) 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