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)

GET request fetch profile and credentials
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)

PUT request fetch token with ttl
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 Hash: (**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