Skip to content

Commit 4ed5568

Browse files
authored
fix: signed IMDS workflow (#2964)
1 parent c40d0fd commit 4ed5568

File tree

4 files changed

+278
-45
lines changed

4 files changed

+278
-45
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "bugfix",
3+
"category": "IMDS",
4+
"description": "Signed IMDS workflow"
5+
}

lib/metadata_service.js

Lines changed: 122 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ AWS.MetadataService = inherit({
3737
*/
3838
httpOptions: { timeout: 0 },
3939

40+
/**
41+
* when enabled, metadata service will not fetch token
42+
*/
43+
disableFetchToken: false,
44+
4045
/**
4146
* Creates a new MetadataService object with a given set of options.
4247
*
@@ -60,21 +65,36 @@ AWS.MetadataService = inherit({
6065
* Sends a request to the instance metadata service for a given resource.
6166
*
6267
* @param path [String] the path of the resource to get
68+
*
69+
* @param options [map] an optional map used to make request
70+
*
71+
* * **method** (String) — HTTP request method
72+
*
73+
* * **headers** (map<String,String>) &mdash; a map of response header keys and their respective values
74+
*
6375
* @callback callback function(err, data)
6476
* Called when a response is available from the service.
6577
* @param err [Error, null] if an error occurred, this value will be set
6678
* @param data [String, null] if the request was successful, the body of
6779
* the response
6880
*/
69-
request: function request(path, callback) {
81+
request: function request(path, options, callback) {
82+
if (arguments.length === 2) {
83+
callback = options;
84+
options = {};
85+
}
86+
7087
if (process.env[AWS.util.imdsDisabledEnv]) {
7188
callback(new Error('EC2 Instance Metadata Service access disabled'));
7289
return;
7390
}
7491

7592
path = path || '/';
7693
var httpRequest = new AWS.HttpRequest('http://' + this.host + path);
77-
httpRequest.method = 'GET';
94+
httpRequest.method = options.method || 'GET';
95+
if (options.headers) {
96+
httpRequest.headers = options.headers;
97+
}
7898
AWS.util.handleRequestWithRetries(httpRequest, this, callback);
7999
},
80100

@@ -83,6 +103,72 @@ AWS.MetadataService = inherit({
83103
*/
84104
loadCredentialsCallbacks: [],
85105

106+
/**
107+
* Fetches metadata token used for getting credentials
108+
*
109+
* @api private
110+
* @callback callback function(err, token)
111+
* Called when token is loaded from the resource
112+
*/
113+
fetchMetadataToken: function fetchMetadataToken(callback) {
114+
var self = this;
115+
var tokenFetchPath = '/latest/api/token';
116+
self.request(
117+
tokenFetchPath,
118+
{
119+
'method': 'PUT',
120+
'headers': {
121+
'x-aws-ec2-metadata-token-ttl-seconds': '21600'
122+
}
123+
},
124+
callback
125+
);
126+
},
127+
128+
/**
129+
* Fetches credentials
130+
*
131+
* @api private
132+
* @callback cb function(err, creds)
133+
* Called when credentials are loaded from the resource
134+
*/
135+
fetchCredentials: function fetchCredentials(options, cb) {
136+
var self = this;
137+
var basePath = '/latest/meta-data/iam/security-credentials/';
138+
139+
self.request(basePath, options, function (err, roleName) {
140+
if (err) {
141+
self.disableFetchToken = !(err.statusCode === 401);
142+
cb(AWS.util.error(
143+
err,
144+
{
145+
message: 'EC2 Metadata roleName request returned error'
146+
}
147+
));
148+
return;
149+
}
150+
roleName = roleName.split('\n')[0]; // grab first (and only) role
151+
self.request(basePath + roleName, options, function (credErr, credData) {
152+
if (credErr) {
153+
self.disableFetchToken = !(credErr.statusCode === 401);
154+
cb(AWS.util.error(
155+
credErr,
156+
{
157+
message: 'EC2 Metadata creds request returned error'
158+
}
159+
));
160+
return;
161+
}
162+
try {
163+
var credentials = JSON.parse(credData);
164+
cb(null, credentials);
165+
} catch (parseError) {
166+
cb(parseError);
167+
}
168+
});
169+
});
170+
},
171+
86172
/**
87173
* Loads a set of credentials stored in the instance metadata service
88174
*
@@ -95,7 +181,6 @@ AWS.MetadataService = inherit({
95181
*/
96182
loadCredentials: function loadCredentials(callback) {
97183
var self = this;
98-
var basePath = '/latest/meta-data/iam/security-credentials/';
99184
self.loadCredentialsCallbacks.push(callback);
100185
if (self.loadCredentialsCallbacks.length > 1) { return; }
101186

@@ -106,23 +191,41 @@ AWS.MetadataService = inherit({
106191
}
107192
}
108193

109-
self.request(basePath, function (err, roleName) {
110-
if (err) callbacks(err);
111-
else {
112-
roleName = roleName.split('\n')[0]; // grab first (and only) role
113-
self.request(basePath + roleName, function (credErr, credData) {
114-
if (credErr) callbacks(credErr);
115-
else {
116-
try {
117-
var credentials = JSON.parse(credData);
118-
callbacks(null, credentials);
119-
} catch (parseError) {
120-
callbacks(parseError);
121-
}
194+
if (self.disableFetchToken) {
195+
self.fetchCredentials({}, callbacks);
196+
} else {
197+
self.fetchMetadataToken(function(tokenError, token) {
198+
if (tokenError) {
199+
if (tokenError.code === 'TimeoutError') {
200+
self.disableFetchToken = true;
201+
} else if (tokenError.retryable === true) {
202+
callbacks(AWS.util.error(
203+
tokenError,
204+
{
205+
message: 'EC2 Metadata token request returned error'
206+
}
207+
));
208+
return;
209+
} else if (tokenError.statusCode === 400) {
210+
callbacks(AWS.util.error(
211+
tokenError,
212+
{
213+
message: 'EC2 Metadata token request returned 400'
214+
}
215+
));
216+
return;
122217
}
123-
});
124-
}
125-
});
218+
}
219+
var options = {};
220+
if (token) {
221+
options.headers = {
222+
'x-aws-ec2-metadata-token': token
223+
};
224+
}
225+
self.fetchCredentials(options, callbacks);
226+
});
227+
228+
}
126229
}
127230
});
128231

lib/util.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -893,7 +893,10 @@ var util = {
893893
} else {
894894
var retryAfter = parseInt(httpResponse.headers['retry-after'], 10) * 1000 || 0;
895895
var err = util.error(new Error(),
896-
{ retryable: statusCode >= 500 || statusCode === 429 }
896+
{
897+
statusCode: statusCode,
898+
retryable: statusCode >= 500 || statusCode === 429
899+
}
897900
);
898901
if (retryAfter && err.retryable) err.retryAfter = retryAfter;
899902
errCallback(err);

0 commit comments

Comments
 (0)