Skip to content

Commit

Permalink
Merge 530641a into 831d686
Browse files Browse the repository at this point in the history
  • Loading branch information
mullermp committed Nov 19, 2019
2 parents 831d686 + 530641a commit bf4750b
Show file tree
Hide file tree
Showing 5 changed files with 364 additions and 86 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
@@ -1,6 +1,8 @@
Unreleased Changes
------------------

* Feature = Aws::Core - Support EC2 IMDS updates.

2.11.400 (2019-11-18)
------------------

Expand Down
110 changes: 102 additions & 8 deletions aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb
Expand Up @@ -11,6 +11,15 @@ class InstanceProfileCredentials
# @api private
class Non200Response < RuntimeError; end

# @api private
class TokenRetrivalError < RuntimeError; end

# @api private
class TokenExpiredError < RuntimeError; end

# @api private
class TokenRetrivalUnavailableError < 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
Expand All @@ -26,6 +35,14 @@ class Non200Response < RuntimeError; end
Non200Response,
]

# Path base for GET request for profile and credentials
# @api private
METADATA_PATH_BASE = '/latest/meta-data/iam/security-credentials/'

# Path for PUT request for token
# @api private
METADATA_TOKEN_PATH = '/latest/api/token'

# @param [Hash] options
# @option options [Integer] :retries (5) Number of times to retry
# when retrieving credentials.
Expand All @@ -40,6 +57,8 @@ class Non200Response < RuntimeError; end
# @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 (21600) Time-to-Live in seconds for
# EC2 Metadata Token used for fetching Metadata Profile Credentials.
def initialize options = {}
@retries = options[:retries] || 5
@ip_address = options[:ip_address] || '169.254.169.254'
Expand All @@ -48,11 +67,13 @@ def initialize options = {}
@http_read_timeout = options[:http_read_timeout] || 5
@http_debug_output = options[:http_debug_output]
@backoff = backoff(options[:backoff])
@token_ttl = options[:token_ttl] || 21600
super
end

# @return [Integer] The number of times to retry failed attempts to
# fetch credentials from the instance metadata service. Defaults to 0.
# @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
Expand Down Expand Up @@ -93,9 +114,11 @@ def get_credentials
begin
retry_errors(NETWORK_ERRORS, max_retries: @retries) do
open_connection do |conn|
path = '/latest/meta-data/iam/security-credentials/'
profile_name = http_get(conn, path).lines.first.strip
http_get(conn, path + profile_name)
_token_attempt(conn)
token_value = @token.value if token_set?
profile_name = http_get(conn, METADATA_PATH_BASE, token_value)
.lines.first.strip
http_get(conn, METADATA_PATH_BASE + profile_name, token_value)
end
end
rescue
Expand All @@ -104,6 +127,28 @@ def get_credentials
end
end

def token_set?
@token && !@token.expired?
end

# attempt to fetch token with retries baked in
# would be skipped if token already set
def _token_attempt(conn)
begin
retry_errors(NETWORK_ERRORS, max_retries: @retries) do
unless token_set?
token_value, ttl = http_put(conn, METADATA_TOKEN_PATH, @token_ttl)
@token = Token.new(token_value, ttl) if token_value && ttl
end
end
rescue *NETWORK_ERRORS, TokenRetrivalUnavailableError
# token attempt failed with allowable errors (those indicating
# token retrieval not available on the instance), reset token to
# allow safe failover to non-token mode
@token = nil
end
end

def _metadata_disabled?
flag = ENV["AWS_EC2_METADATA_DISABLED"]
!flag.nil? && flag.downcase == "true"
Expand All @@ -118,10 +163,40 @@ def open_connection
yield(http).tap { http.finish }
end

def http_get(connection, path)
response = connection.request(Net::HTTP::Get.new(path))
if response.code.to_i == 200
# GET request fetch profile and credentials
def http_get(connection, path, token=nil)
headers = {"User-Agent" => "aws-sdk-ruby2/#{VERSION}"}
headers["x-aws-ec2-metadata-token"] = token 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

# PUT request fetch token with ttl
def http_put(connection, path, ttl)
headers = {
"User-Agent" => "aws-sdk-ruby2/#{VERSION}",
"x-aws-ec2-metadata-token-ttl-seconds" => ttl.to_s
}
response = connection.request(Net::HTTP::Put.new(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
when 403
when 404
when 405
raise TokenRetrivalUnavailableError
else
raise Non200Response
end
Expand All @@ -143,5 +218,24 @@ def retry_errors(error_classes, options = {}, &block)
end
end

# @api private
# Token used to fetch IMDS profile and credentials
class Token

def initialize(value, ttl)
@ttl = ttl
@value = value
@created_time = Time.now
end

# [String] token value
attr_reader :value

def expired?
Time.now - @created_time > @ttl
end

end

end
end
71 changes: 44 additions & 27 deletions aws-sdk-core/spec/aws/credential_resolution_chain_spec.rb
Expand Up @@ -13,6 +13,14 @@ module Aws
'..', 'fixtures', 'credentials', 'mock_shared_config'))
}

let(:imds_url) {
'http://169.254.169.254/latest/meta-data/iam/security-credentials/'
}

let(:imds_token_url) {
'http://169.254.169.254/latest/api/token'
}

describe "default behavior" do
before(:each) do
stub_const('ENV', {})
Expand Down Expand Up @@ -56,28 +64,32 @@ module Aws
"AR_TOKEN"
)
client = Aws::S3::Client.new(profile: "ar_plus_creds", region: "us-east-1")
expect(client.config.credentials.access_key_id).to eq("AR_AKID")
expect(client.config.credentials.credentials.access_key_id).to eq("AR_AKID")
end

it 'prefers shared credential file static credentials over shared config' do
client = Aws::S3::Client.new(profile: "credentials_first", region: "us-east-1")
expect(client.config.credentials.access_key_id).to eq("ACCESS_KEY_CRD")
expect(client.config.credentials.credentials.access_key_id).to eq("ACCESS_KEY_CRD")
end

it 'will source static credentials from shared config after shared credentials' do
client = Aws::S3::Client.new(profile: "incomplete_cred", region: "us-east-1")
expect(client.config.credentials.access_key_id).to eq("ACCESS_KEY_SC1")
expect(client.config.credentials.credentials.access_key_id).to eq("ACCESS_KEY_SC1")
end

it 'attempts to fetch metadata credentials last' do
stub_request(
:get,
"http://169.254.169.254/latest/meta-data/iam/security-credentials/"
).to_return(:status => 200, :body => "profile-name\n")
stub_request(
:get,
"http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name"
).to_return(:status => 200, :body => <<-JSON.strip)
stub_request(:put, imds_token_url)
.to_return(
:status => 200,
:body => "my-token\n",
:headers => {"x-aws-ec2-metadata-token-ttl-seconds" => "21600"}
)
stub_request(:get, imds_url)
.with(:headers => {"x-aws-ec2-metadata-token" => "my-token"})
.to_return(:status => 200, :body => "profile-name\n")
stub_request(:get, "#{imds_url}profile-name")
.with(:headers => {"x-aws-ec2-metadata-token" => "my-token"})
.to_return(:status => 200, :body => <<-JSON.strip)
{
"Code" : "Success",
"LastUpdated" : "2013-11-22T20:03:48Z",
Expand All @@ -89,11 +101,11 @@ module Aws
}
JSON
client = Aws::S3::Client.new(profile: "nonexistant", region: "us-east-1")
expect(client.config.credentials.access_key_id).to eq("akid-md")
expect(client.config.credentials.credentials.access_key_id).to eq("akid-md")
end

describe 'Assume Role Resolution' do
it 'will not assume a role without source_profile present' do
it 'will not assume a role without a source present' do
expect {
Aws::S3::Client.new(profile: "ar_no_src", region: "us-east-1")
}.to raise_error(Errors::NoSourceProfileError)
Expand All @@ -114,7 +126,7 @@ module Aws
"AR_TOKEN"
)
client = Aws::S3::Client.new(profile: "assumerole_sc", region: "us-east-1")
expect(client.config.credentials.access_key_id).to eq("AR_AKID")
expect(client.config.credentials.credentials.access_key_id).to eq("AR_AKID")
end

it 'will then try to assume a role from shared config' do
Expand All @@ -126,7 +138,7 @@ module Aws
"AR_TOKEN"
)
client = Aws::S3::Client.new(profile: "ar_from_self", region: "us-east-1")
expect(client.config.credentials.access_key_id).to eq("AR_AKID")
expect(client.config.credentials.credentials.access_key_id).to eq("AR_AKID")
end

it 'will assume a role from config using source credentials in shared credentials' do
Expand All @@ -138,9 +150,10 @@ module Aws
"AR_TOKEN"
)
client = Aws::S3::Client.new(profile: "creds_from_sc", region: "us-east-1")
expect(client.config.credentials.access_key_id).to eq("AR_AKID")
expect(client.config.credentials.credentials.access_key_id).to eq("AR_AKID")
end
end

end

describe "AWS_SDK_CONFIG_OPT_OUT set" do
Expand All @@ -165,7 +178,7 @@ module Aws
profile: "fooprofile",
region: "us-east-1"
)
expect(client.config.credentials.access_key_id).to eq("ACCESS_DIRECT")
expect(client.config.credentials.credentials.access_key_id).to eq("ACCESS_DIRECT")
end

it 'prefers ENV credentials over shared config' do
Expand All @@ -174,7 +187,7 @@ module Aws
"AWS_SECRET_ACCESS_KEY" => "SECRET_ENV_STUB"
})
client = Aws::S3::Client.new(profile: "fooprofile", region: "us-east-1")
expect(client.config.credentials.access_key_id).to eq("AKID_ENV_STUB")
expect(client.config.credentials.credentials.access_key_id).to eq("AKID_ENV_STUB")
end

it 'will not load credentials from shared config' do
Expand All @@ -188,14 +201,18 @@ module Aws
end

it 'attempts to fetch metadata credentials last' do
stub_request(
:get,
"http://169.254.169.254/latest/meta-data/iam/security-credentials/"
).to_return(:status => 200, :body => "profile-name\n")
stub_request(
:get,
"http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name"
).to_return(:status => 200, :body => <<-JSON.strip)
stub_request(:put, imds_token_url)
.to_return(
:status => 200,
:body => "my-token\n",
:headers => {"x-aws-ec2-metadata-token-ttl-seconds" => "21600"}
)
stub_request(:get, imds_url)
.with(:headers => {"x-aws-ec2-metadata-token" => "my-token"})
.to_return(:status => 200, :body => "profile-name\n")
stub_request(:get, "#{imds_url}profile-name")
.with(:headers => {"x-aws-ec2-metadata-token" => "my-token"})
.to_return(:status => 200, :body => <<-JSON.strip)
{
"Code" : "Success",
"LastUpdated" : "2013-11-22T20:03:48Z",
Expand All @@ -207,7 +224,7 @@ module Aws
}
JSON
client = Aws::S3::Client.new(profile: "nonexistant", region: "us-east-1")
expect(client.config.credentials.access_key_id).to eq("akid-md")
expect(client.config.credentials.credentials.access_key_id).to eq("akid-md")
end
end

Expand Down

0 comments on commit bf4750b

Please sign in to comment.