6
6
7
7
module Aws
8
8
# An auto-refreshing credential provider that loads credentials from
9
- # instances running in ECS .
9
+ # instances running in containers .
10
10
#
11
11
# ecs_credentials = Aws::ECSCredentials.new(retries: 3)
12
12
# ec2 = Aws::EC2::Client.new(credentials: ecs_credentials)
@@ -17,6 +17,12 @@ class ECSCredentials
17
17
# @api private
18
18
class Non200Response < RuntimeError ; end
19
19
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
+
20
26
# These are the errors we trap when attempting to talk to the
21
27
# instance metadata service. Any of these imply the service
22
28
# is not present, no responding or some other non-recoverable
@@ -41,7 +47,7 @@ class Non200Response < RuntimeError; end
41
47
# is set and `credential_path` is not set.
42
48
# @option options [String] :credential_path By default, the value of the
43
49
# 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.
45
51
# By default, this is the value of the AWS_CONTAINER_CREDENTIALS_FULL_URI
46
52
# environment variable. This value is ignored if `credential_path` or
47
53
# ENV['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] is set.
@@ -64,7 +70,6 @@ def initialize(options = {})
64
70
endpoint = options [ :endpoint ] ||
65
71
ENV [ 'AWS_CONTAINER_CREDENTIALS_FULL_URI' ]
66
72
initialize_uri ( options , credential_path , endpoint )
67
- @authorization_token = ENV [ 'AWS_CONTAINER_AUTHORIZATION_TOKEN' ]
68
73
69
74
@retries = options [ :retries ] || 5
70
75
@http_open_timeout = options [ :http_open_timeout ] || 5
@@ -103,31 +108,43 @@ def initialize_relative_uri(options, path)
103
108
104
109
def initialize_full_uri ( endpoint )
105
110
uri = URI . parse ( endpoint )
111
+ validate_full_uri_scheme! ( uri )
106
112
validate_full_uri! ( uri )
107
- @host = uri . host
113
+ @host = uri . hostname
108
114
@port = uri . port
109
115
@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"
111
123
end
112
124
113
125
# Validate that the full URI is using a loopback address if scheme is http.
114
126
def validate_full_uri! ( full_uri )
115
127
return unless full_uri . scheme == 'http'
116
128
117
129
begin
118
- return if ip_loopback ?( IPAddr . new ( full_uri . host ) )
130
+ return if valid_ip_address ?( IPAddr . new ( full_uri . host ) )
119
131
rescue IPAddr ::InvalidAddressError
120
132
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 ) ) }
122
134
end
123
135
124
136
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 )
127
143
end
128
144
129
145
# loopback? method is available in Ruby 2.5+
130
146
# Replicate the logic here.
147
+ # loopback (IPv4 127.0.0.0/8, IPv6 ::1/128)
131
148
def ip_loopback? ( ip_address )
132
149
case ip_address . family
133
150
when Socket ::AF_INET
@@ -139,6 +156,20 @@ def ip_loopback?(ip_address)
139
156
end
140
157
end
141
158
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
+
142
173
def backoff ( backoff )
143
174
case backoff
144
175
when Proc then backoff
@@ -174,10 +205,36 @@ def get_credentials
174
205
http_get ( conn , @credential_path )
175
206
end
176
207
end
208
+ rescue TokenFileReadError , InvalidTokenError
209
+ raise
177
210
rescue StandardError
178
211
'{}'
179
212
end
180
213
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
+
181
238
def open_connection
182
239
http = Net ::HTTP . new ( @host , @port , nil )
183
240
http . open_timeout = @http_open_timeout
@@ -190,18 +247,27 @@ def open_connection
190
247
191
248
def http_get ( connection , path )
192
249
request = Net ::HTTP ::Get . new ( path )
193
- request [ 'Authorization' ] = @authorization_token if @authorization_token
250
+ set_authorization_token ( request )
194
251
response = connection . request ( request )
195
252
raise Non200Response unless response . code . to_i == 200
196
253
197
254
response . body
198
255
end
199
256
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
+
200
264
def retry_errors ( error_classes , options = { } )
201
265
max_retries = options [ :max_retries ]
202
266
retries = 0
203
267
begin
204
268
yield
269
+ rescue TokenFileReadError , InvalidTokenError
270
+ raise
205
271
rescue *error_classes => _e
206
272
raise unless retries < max_retries
207
273
0 commit comments