Skip to content

Commit 76abc01

Browse files
authored
Merge pull request #7709 from folbricht-stripe/imdsv2
Use IMDSv2 for S3 instance credentials
2 parents 97e9da6 + bdd062c commit 76abc01

File tree

2 files changed

+284
-53
lines changed

2 files changed

+284
-53
lines changed

lib/rubygems/s3_uri_signer.rb

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
# frozen_string_literal: true
22

33
require_relative "openssl"
4+
require_relative "user_interaction"
45

56
##
67
# S3URISigner implements AWS SigV4 for S3 Source to avoid a dependency on the aws-sdk-* gems
78
# More on AWS SigV4: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
89
class Gem::S3URISigner
10+
include Gem::UserInteraction
11+
912
class ConfigurationError < Gem::Exception
1013
def initialize(message)
1114
super message
@@ -147,17 +150,40 @@ def ec2_metadata_credentials_json
147150
require_relative "request/connection_pools"
148151
require "json"
149152

150-
iam_info = ec2_metadata_request(EC2_IAM_INFO)
153+
# First try V2 fallback to V1
154+
res = nil
155+
begin
156+
res = ec2_metadata_credentials_imds_v2
157+
rescue InstanceProfileError
158+
alert_warning "Unable to access ec2 credentials via IMDSv2, falling back to IMDSv1"
159+
res = ec2_metadata_credentials_imds_v1
160+
end
161+
res
162+
end
163+
164+
def ec2_metadata_credentials_imds_v2
165+
token = ec2_metadata_token
166+
iam_info = ec2_metadata_request(EC2_IAM_INFO, token:)
151167
# Expected format: arn:aws:iam::<id>:instance-profile/<role_name>
152168
role_name = iam_info["InstanceProfileArn"].split("/").last
153-
ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name)
169+
ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name, token:)
154170
end
155171

156-
def ec2_metadata_request(url)
157-
uri = Gem::URI(url)
158-
@request_pool ||= create_request_pool(uri)
159-
request = Gem::Request.new(uri, Gem::Net::HTTP::Get, nil, @request_pool)
160-
response = request.fetch
172+
def ec2_metadata_credentials_imds_v1
173+
iam_info = ec2_metadata_request(EC2_IAM_INFO, token: nil)
174+
# Expected format: arn:aws:iam::<id>:instance-profile/<role_name>
175+
role_name = iam_info["InstanceProfileArn"].split("/").last
176+
ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name, token: nil)
177+
end
178+
179+
def ec2_metadata_request(url, token:)
180+
request = ec2_iam_request(Gem::URI(url), Gem::Net::HTTP::Get)
181+
182+
response = request.fetch do |req|
183+
if token
184+
req.add_field "X-aws-ec2-metadata-token", token
185+
end
186+
end
161187

162188
case response
163189
when Gem::Net::HTTPOK then
@@ -167,13 +193,34 @@ def ec2_metadata_request(url)
167193
end
168194
end
169195

196+
def ec2_metadata_token
197+
request = ec2_iam_request(Gem::URI(EC2_IAM_TOKEN), Gem::Net::HTTP::Put)
198+
199+
response = request.fetch do |req|
200+
req.add_field "X-aws-ec2-metadata-token-ttl-seconds", 60
201+
end
202+
203+
case response
204+
when Gem::Net::HTTPOK then
205+
response.body
206+
else
207+
raise InstanceProfileError.new("Unable to fetch AWS metadata from #{uri}: #{response.message} #{response.code}")
208+
end
209+
end
210+
211+
def ec2_iam_request(uri, verb)
212+
@request_pool ||= create_request_pool(uri)
213+
Gem::Request.new(uri, verb, nil, @request_pool)
214+
end
215+
170216
def create_request_pool(uri)
171217
proxy_uri = Gem::Request.proxy_uri(Gem::Request.get_proxy_from_env(uri.scheme))
172218
certs = Gem::Request.get_cert_files
173219
Gem::Request::ConnectionPools.new(proxy_uri, certs).pool_for(uri)
174220
end
175221

176222
BASE64_URI_TRANSLATE = { "+" => "%2B", "/" => "%2F", "=" => "%3D", "\n" => "" }.freeze
223+
EC2_IAM_TOKEN = "http://169.254.169.254/latest/api/token"
177224
EC2_IAM_INFO = "http://169.254.169.254/latest/meta-data/iam/info"
178225
EC2_IAM_SECURITY_CREDENTIALS = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
179226
end

0 commit comments

Comments
 (0)