66
77module Aws
88 # An auto-refreshing credential provider that loads credentials from
9- # instances running in ECS .
9+ # instances running in containers .
1010 #
1111 # ecs_credentials = Aws::ECSCredentials.new(retries: 3)
1212 # ec2 = Aws::EC2::Client.new(credentials: ecs_credentials)
@@ -17,6 +17,12 @@ class ECSCredentials
1717 # @api private
1818 class Non200Response < RuntimeError ; end
1919
20+ # Raised when the token file cannot be read.
21+ class TokenFileReadError < RuntimeError ; end
22+
23+ # Raised when the token file is invalid.
24+ class InvalidTokenError < RuntimeError ; end
25+
2026 # These are the errors we trap when attempting to talk to the
2127 # instance metadata service. Any of these imply the service
2228 # is not present, no responding or some other non-recoverable
@@ -41,7 +47,7 @@ class Non200Response < RuntimeError; end
4147 # is set and `credential_path` is not set.
4248 # @option options [String] :credential_path By default, the value of the
4349 # AWS_CONTAINER_CREDENTIALS_RELATIVE_URI environment variable.
44- # @option options [String] :endpoint The ECS credential endpoint.
50+ # @option options [String] :endpoint The container credential endpoint.
4551 # By default, this is the value of the AWS_CONTAINER_CREDENTIALS_FULL_URI
4652 # environment variable. This value is ignored if `credential_path` or
4753 # ENV['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] is set.
@@ -64,7 +70,6 @@ def initialize(options = {})
6470 endpoint = options [ :endpoint ] ||
6571 ENV [ 'AWS_CONTAINER_CREDENTIALS_FULL_URI' ]
6672 initialize_uri ( options , credential_path , endpoint )
67- @authorization_token = ENV [ 'AWS_CONTAINER_AUTHORIZATION_TOKEN' ]
6873
6974 @retries = options [ :retries ] || 5
7075 @http_open_timeout = options [ :http_open_timeout ] || 5
@@ -103,31 +108,43 @@ def initialize_relative_uri(options, path)
103108
104109 def initialize_full_uri ( endpoint )
105110 uri = URI . parse ( endpoint )
111+ validate_full_uri_scheme! ( uri )
106112 validate_full_uri! ( uri )
107- @host = uri . host
113+ @host = uri . hostname
108114 @port = uri . port
109115 @scheme = uri . scheme
110- @credential_path = uri . path
116+ @credential_path = uri . request_uri
117+ end
118+
119+ def validate_full_uri_scheme! ( full_uri )
120+ return if full_uri . is_a? ( URI ::HTTP ) || full_uri . is_a? ( URI ::HTTPS )
121+
122+ raise ArgumentError , "'#{ full_uri } ' must be a valid HTTP or HTTPS URI"
111123 end
112124
113125 # Validate that the full URI is using a loopback address if scheme is http.
114126 def validate_full_uri! ( full_uri )
115127 return unless full_uri . scheme == 'http'
116128
117129 begin
118- return if ip_loopback ?( IPAddr . new ( full_uri . host ) )
130+ return if valid_ip_address ?( IPAddr . new ( full_uri . host ) )
119131 rescue IPAddr ::InvalidAddressError
120132 addresses = Resolv . getaddresses ( full_uri . host )
121- return if addresses . all? { |addr | ip_loopback ?( IPAddr . new ( addr ) ) }
133+ return if addresses . all? { |addr | valid_ip_address ?( IPAddr . new ( addr ) ) }
122134 end
123135
124136 raise ArgumentError ,
125- 'AWS_CONTAINER_CREDENTIALS_FULL_URI must use a loopback ' \
126- 'address when using the http scheme.'
137+ 'AWS_CONTAINER_CREDENTIALS_FULL_URI must use a local loopback ' \
138+ 'or an ECS or EKS link-local address when using the http scheme.'
139+ end
140+
141+ def valid_ip_address? ( ip_address )
142+ ip_loopback? ( ip_address ) || ecs_or_eks_ip? ( ip_address )
127143 end
128144
129145 # loopback? method is available in Ruby 2.5+
130146 # Replicate the logic here.
147+ # loopback (IPv4 127.0.0.0/8, IPv6 ::1/128)
131148 def ip_loopback? ( ip_address )
132149 case ip_address . family
133150 when Socket ::AF_INET
@@ -139,6 +156,20 @@ def ip_loopback?(ip_address)
139156 end
140157 end
141158
159+ # Verify that the IP address is a link-local address from ECS or EKS.
160+ # ECS container host (IPv4 `169.254.170.2`)
161+ # EKS container host (IPv4 `169.254.170.23`, IPv6 `fd00:ec2::23`)
162+ def ecs_or_eks_ip? ( ip_address )
163+ case ip_address . family
164+ when Socket ::AF_INET
165+ [ 0xa9feaa02 , 0xa9feaa17 ] . include? ( ip_address )
166+ when Socket ::AF_INET6
167+ ip_address == 0xfd00_0ec2_0000_0000_0000_0000_0000_0023
168+ else
169+ false
170+ end
171+ end
172+
142173 def backoff ( backoff )
143174 case backoff
144175 when Proc then backoff
@@ -174,10 +205,36 @@ def get_credentials
174205 http_get ( conn , @credential_path )
175206 end
176207 end
208+ rescue TokenFileReadError , InvalidTokenError
209+ raise
177210 rescue StandardError
178211 '{}'
179212 end
180213
214+ def fetch_authorization_token
215+ if ( path = ENV [ 'AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE' ] )
216+ fetch_authorization_token_file ( path )
217+ elsif ( token = ENV [ 'AWS_CONTAINER_AUTHORIZATION_TOKEN' ] )
218+ token
219+ end
220+ end
221+
222+ def fetch_authorization_token_file ( path )
223+ File . read ( path ) . strip
224+ rescue Errno ::ENOENT
225+ raise TokenFileReadError ,
226+ 'AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE is set ' \
227+ "but the file doesn't exist: #{ path } "
228+ end
229+
230+ def validate_authorization_token! ( token )
231+ return unless token . include? ( "\r \n " )
232+
233+ raise InvalidTokenError ,
234+ 'Invalid Authorization token: token contains ' \
235+ 'a newline and carriage return character.'
236+ end
237+
181238 def open_connection
182239 http = Net ::HTTP . new ( @host , @port , nil )
183240 http . open_timeout = @http_open_timeout
@@ -190,18 +247,27 @@ def open_connection
190247
191248 def http_get ( connection , path )
192249 request = Net ::HTTP ::Get . new ( path )
193- request [ 'Authorization' ] = @authorization_token if @authorization_token
250+ set_authorization_token ( request )
194251 response = connection . request ( request )
195252 raise Non200Response unless response . code . to_i == 200
196253
197254 response . body
198255 end
199256
257+ def set_authorization_token ( request )
258+ if ( authorization_token = fetch_authorization_token )
259+ validate_authorization_token! ( authorization_token )
260+ request [ 'Authorization' ] = authorization_token
261+ end
262+ end
263+
200264 def retry_errors ( error_classes , options = { } )
201265 max_retries = options [ :max_retries ]
202266 retries = 0
203267 begin
204268 yield
269+ rescue TokenFileReadError , InvalidTokenError
270+ raise
205271 rescue *error_classes => _e
206272 raise unless retries < max_retries
207273
0 commit comments