class Aws::InstanceProfileCredentials
@see docs.aws.amazon.com/sdkref/latest/guide/feature-imds-credentials.html IMDS Credential Provider
responses. These retries are separate from configurable retries.
* **JSON parsing retries**: Fixed at 3 attempts to handle cases when IMDS returns malformed JSON
* Token fetching
* Entire token fetch and credential retrieval process
with the IMDS endpoint. There are two separate retry mechanisms within the provider:
* **Configurable retries** (defaults to ‘1`): these retries handle errors when communicating
Breakdown of retries is as follows:
When initialized from the default credential chain, this provider defaults to `0` retries.
## Retries
ec2 = Aws::EC2::Client.new(credentials: instance_credentials)
instance_credentials = Aws::InstanceProfileCredentials.new
An auto-refreshing credential provider that loads credentials from EC2 instances.
def empty_credentials?(creds_hash)
def empty_credentials?(creds_hash) !creds_hash['AccessKeyId'] || creds_hash['AccessKeyId'].empty? end
def fetch_credentials(conn)
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) rescue TokenExpiredError # Token has expired, reset it # The next retry should fetch it @token = nil @imds_v1_fallback = false raise Non200Response end
def fetch_token(conn)
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 @imds_v1_fallback = true end
def http_get(connection, path)
def http_get(connection, path) headers = { 'User-Agent' => "aws-sdk-ruby3/#{CORE_GEM_VERSION}" } headers['x-aws-ec2-metadata-token'] = @token.value if @token response = connection.request(Net::HTTP::Get.new(path, headers)) case response.code.to_i when 200 response.body when 401 raise TokenExpiredError else raise Non200Response end end
def http_put(connection)
def http_put(connection) headers = { 'User-Agent' => "aws-sdk-ruby3/#{CORE_GEM_VERSION}", 'x-aws-ec2-metadata-token-ttl-seconds' => @token_ttl.to_s } response = connection.request(Net::HTTP::Put.new(METADATA_TOKEN_PATH, headers)) case response.code.to_i when 200 [ response.body, response.header['x-aws-ec2-metadata-token-ttl-seconds'].to_i ] when 400 raise TokenRetrivalError else raise Non200Response end end
def initialize(options = {})
(**options)
-
:before_refresh
(Callable
) -- Proc called before credentials are refreshed. `before_refresh` -
:token_ttl
(Integer
) -- Time-to-Live in seconds for EC2 Metadata Token used for fetching -
:http_debug_output
(IO
) -- HTTP wire traces are sent to this object. -
:delay
(Numeric, Proc
) -- By default, failures are retried with exponential back-off, i.e. -
:http_read_timeout
(Float
) -- -
:http_open_timeout
(Float
) -- -
:port
(Integer
) -- -
:ip_address
(String
) -- Deprecated. Use `:endpoint` instead. -
:disable_imds_v1
(Boolean
) -- Disable the use of the legacy EC2 Metadata Service v1. -
:endpoint_mode
(String
) -- The endpoint mode for the instance metadata service. This is -
:endpoint
(String
) -- The IMDS endpoint. This option has precedence -
:retries
(Integer
) -- Number of times to retry when retrieving credentials.
Parameters:
-
options
(Hash
) --
def initialize(options = {}) @backoff = resolve_backoff(options[:backoff]) @disable_imds_v1 = resolve_disable_v1(options) @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] @port = options[:port] || 80 @retries = options[:retries] || 1 @token_ttl = options[:token_ttl] || 21_600 @async_refresh = false @imds_v1_fallback = false @no_refresh_until = nil @token = nil @metrics = ['CREDENTIALS_IMDS'] super end
def open_connection
def open_connection uri = URI.parse(@endpoint) http = Net::HTTP.new(uri.hostname || @endpoint, uri.port || @port) http.open_timeout = @http_open_timeout http.read_timeout = @http_read_timeout http.set_debug_output(@http_debug_output) if @http_debug_output http.start yield(http).tap { http.finish } end
def refresh
def refresh if @no_refresh_until && @no_refresh_until > Time.now warn_expired_credentials 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) end rescue Aws::Json::ParseError raise Aws::Errors::MetadataParserError end 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 resolve_backoff(backoff)
def resolve_backoff(backoff) case backoff when Proc then backoff when Numeric then ->(_) { sleep(backoff) } else ->(num_failures) { Kernel.sleep(1.2**num_failures) } end end
def resolve_disable_v1(options)
def resolve_disable_v1(options) 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 resolve_endpoint(options)
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}" end end
def resolve_endpoint_mode(options)
def resolve_endpoint_mode(options) options[:endpoint_mode] || ENV['AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE'] || Aws.shared_config.ec2_metadata_service_endpoint_mode(profile: options[:profile]) || 'IPv4' end
def retrieve_credentials
def retrieve_credentials # 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 @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 end rescue StandardError => e warn("Error retrieving instance profile credentials: #{e}") '{}' end end
def retry_errors(error_classes, options = {}, &_block)
def retry_errors(error_classes, options = {}, &_block) max_retries = options[:max_retries] retries = 0 begin yield rescue *error_classes raise unless retries < max_retries @backoff.call(retries) retries += 1 retry end end
def token_set?
def token_set? @token && !@token.expired? end
def update_credentials(creds)
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 warn_expired_credentials
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