@@ -12,12 +12,20 @@ import { InstanceMetadataCredentials } from "./types";
12
12
import { getInstanceMetadataEndpoint } from "./utils/getInstanceMetadataEndpoint" ;
13
13
import { staticStabilityProvider } from "./utils/staticStabilityProvider" ;
14
14
15
- const IMDS_PATH = "/latest/meta-data/iam/security-credentials/" ;
16
- const IMDS_TOKEN_PATH = "/latest/api/token " ;
15
+ const IMDS_LEGACY_PATH = "/latest/meta-data/iam/security-credentials/" ;
16
+ const IMDS_EXTENDED_PATH = "/latest/meta-data/iam/security-credentials-extended/ " ;
17
17
const AWS_EC2_METADATA_V1_DISABLED = "AWS_EC2_METADATA_V1_DISABLED" ;
18
18
const PROFILE_AWS_EC2_METADATA_V1_DISABLED = "ec2_metadata_v1_disabled" ;
19
+ const IMDS_TOKEN_PATH = "/latest/api/token" ;
19
20
const X_AWS_EC2_METADATA_TOKEN = "x-aws-ec2-metadata-token" ;
20
21
22
+ // Environment variables and config keys
23
+
24
+ const ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED" ;
25
+ const CONFIG_IMDS_DISABLED = "disable_ec2_metadata" ;
26
+ const ENV_PROFILE_NAME = "AWS_EC2_INSTANCE_PROFILE_NAME" ;
27
+ const CONFIG_PROFILE_NAME = "ec2_instance_profile_name" ;
28
+
21
29
/**
22
30
* @internal
23
31
*
@@ -36,10 +44,12 @@ const getInstanceMetadataProvider = (init: RemoteProviderInit = {}) => {
36
44
const { logger, profile } = init ;
37
45
const { timeout, maxRetries } = providerConfigFromInit ( init ) ;
38
46
47
+
39
48
const getCredentials = async ( maxRetries : number , options : RequestOptions ) => {
40
49
const isImdsV1Fallback = disableFetchToken || options . headers ?. [ X_AWS_EC2_METADATA_TOKEN ] == null ;
41
50
42
51
if ( isImdsV1Fallback ) {
52
+ await checkIfImdsDisabled ( profile , logger ) ;
43
53
let fallbackBlockedFromProfile = false ;
44
54
let fallbackBlockedFromProcessEnv = false ;
45
55
@@ -84,20 +94,7 @@ const getInstanceMetadataProvider = (init: RemoteProviderInit = {}) => {
84
94
}
85
95
}
86
96
87
- const imdsProfile = (
88
- await retry < string > ( async ( ) => {
89
- let profile : string ;
90
- try {
91
- profile = await getProfile ( options ) ;
92
- } catch ( err ) {
93
- if ( err . statusCode === 401 ) {
94
- disableFetchToken = false ;
95
- }
96
- throw err ;
97
- }
98
- return profile ;
99
- } , maxRetries )
100
- ) . trim ( ) ;
97
+ const imdsProfile = await getImdsProfileHelper ( options , maxRetries , init , profile ) ;
101
98
102
99
return retry ( async ( ) => {
103
100
let creds : AwsCredentialIdentity ;
@@ -113,8 +110,10 @@ const getInstanceMetadataProvider = (init: RemoteProviderInit = {}) => {
113
110
} , maxRetries ) ;
114
111
} ;
115
112
116
- return async ( ) => {
113
+
114
+ return async ( ) => {
117
115
const endpoint = await getInstanceMetadataEndpoint ( ) ;
116
+ await checkIfImdsDisabled ( profile , logger ) ;
118
117
if ( disableFetchToken ) {
119
118
logger ?. debug ( "AWS SDK Instance Metadata" , "using v1 fallback (no token fetch)" ) ;
120
119
return getCredentials ( maxRetries , { ...endpoint , timeout } ) ;
@@ -144,6 +143,55 @@ const getInstanceMetadataProvider = (init: RemoteProviderInit = {}) => {
144
143
} ;
145
144
} ;
146
145
146
+ /**
147
+ * @internal
148
+ * Gets IMDS profile with proper error handling and retries
149
+ */
150
+ const getImdsProfileHelper = async (
151
+ options : RequestOptions ,
152
+ maxRetries : number ,
153
+ init : RemoteProviderInit = { } ,
154
+ profile ?: string
155
+ ) : Promise < string > => {
156
+ let apiVersion : "unknown" | "extended" | "legacy" = "unknown" ;
157
+ let resolvedProfile : string | null = null ;
158
+
159
+ return retry < string > ( async ( ) => {
160
+ // First check if a profile name is configured
161
+ const configuredName = await getConfiguredProfileName ( init , profile ) ;
162
+ if ( configuredName ) {
163
+ return configuredName ;
164
+ }
165
+ if ( resolvedProfile ) {
166
+ return resolvedProfile ;
167
+ }
168
+ // If no configured name, fetch profile name from IMDS
169
+ try {
170
+ // Try extended API first
171
+ try {
172
+ const response = await httpRequest ( { ...options , path : IMDS_EXTENDED_PATH } ) ;
173
+ resolvedProfile = response . toString ( ) . trim ( ) ;
174
+ if ( apiVersion === "unknown" ) {
175
+ apiVersion = "extended" ;
176
+ }
177
+ return resolvedProfile ;
178
+ } catch ( error ) {
179
+ if ( error ?. statusCode === 404 && apiVersion === "unknown" ) {
180
+ apiVersion = "legacy" ;
181
+ const response = await httpRequest ( { ...options , path : IMDS_LEGACY_PATH } ) ;
182
+ resolvedProfile = response . toString ( ) . trim ( ) ;
183
+ return resolvedProfile ;
184
+ } else {
185
+ throw error ;
186
+ }
187
+ }
188
+ } catch ( err ) {
189
+ throw err ;
190
+ }
191
+ } , maxRetries ) ;
192
+ } ;
193
+
194
+
147
195
const getMetadataToken = async ( options : RequestOptions ) =>
148
196
httpRequest ( {
149
197
...options ,
@@ -154,23 +202,109 @@ const getMetadataToken = async (options: RequestOptions) =>
154
202
} ,
155
203
} ) ;
156
204
157
- const getProfile = async ( options : RequestOptions ) => ( await httpRequest ( { ...options , path : IMDS_PATH } ) ) . toString ( ) ;
205
+ /**
206
+ * @internal
207
+ * Checks if IMDS credential fetching is disabled through configuration
208
+ */
209
+ const checkIfImdsDisabled = async ( profile ?: string , logger ?: any ) : Promise < void > => {
210
+ // Load configuration in priority order
211
+ const disableImds = await loadConfig (
212
+ {
213
+ // Check environment variable
214
+ environmentVariableSelector : ( env ) => {
215
+ const envValue = env [ ENV_IMDS_DISABLED ] ;
216
+ return envValue === "true" ;
217
+ } ,
218
+ // Check config file
219
+ configFileSelector : ( profile ) => {
220
+ const profileValue = profile [ CONFIG_IMDS_DISABLED ] ;
221
+ return profileValue === "true" ;
222
+ } ,
223
+ default : false ,
224
+ } ,
225
+ { profile }
226
+ ) ( ) ;
227
+
228
+ // If IMDS is disabled, throw error
229
+ if ( disableImds ) {
230
+ throw new CredentialsProviderError ( "IMDS credential fetching is disabled" , { logger } ) ;
231
+ }
232
+ } ;
233
+
234
+ /**
235
+ * @internal
236
+ * Gets configured profile name from various sources
237
+ */
238
+ const getConfiguredProfileName = async ( init : RemoteProviderInit , profile ?: string ) : Promise < string | null > => {
239
+ // Load configuration in priority order
240
+ const profileName = await loadConfig (
241
+ {
242
+ // Check environment variable
243
+ environmentVariableSelector : ( env ) => env [ ENV_PROFILE_NAME ] ,
244
+ // Check config file
245
+ configFileSelector : ( profile ) => profile [ CONFIG_PROFILE_NAME ] ,
246
+ default : null ,
247
+ } ,
248
+ { profile }
249
+ ) ( ) ;
250
+
251
+ // Check runtime config (highest priority)
252
+ const name = init . ec2InstanceProfileName || profileName ;
253
+
254
+ // Validate if name is provided but empty
255
+ if ( typeof name === 'string' && name . trim ( ) === "" ) {
256
+ throw new CredentialsProviderError ( "EC2 instance profile name cannot be empty" ) ;
257
+ }
258
+
259
+ return name ;
260
+ } ;
261
+
158
262
263
+ /**
264
+ * @internal
265
+ * Gets credentials from profile
266
+ */
159
267
const getCredentialsFromProfile = async ( profile : string , options : RequestOptions , init : RemoteProviderInit ) => {
160
- const credentialsResponse = JSON . parse (
161
- (
162
- await httpRequest ( {
163
- ...options ,
164
- path : IMDS_PATH + profile ,
165
- } )
166
- ) . toString ( )
167
- ) ;
268
+ // Try extended API first
269
+ try {
270
+ return await getCredentialsFromPath ( IMDS_EXTENDED_PATH + profile , options ) ;
271
+ } catch ( error ) {
272
+ // If extended API returns 404, fall back to legacy API
273
+ if ( error . statusCode === 404 ) {
274
+ try {
275
+ return await getCredentialsFromPath ( IMDS_LEGACY_PATH + profile , options ) ;
276
+ } catch ( legacyError ) {
277
+ if ( legacyError . statusCode === 404 && init . ec2InstanceProfileName === undefined ) {
278
+ // If legacy API also returns 404 and we're using a cached profile name,
279
+ // the profile might have changed - clear cache and retry
280
+ const resolvedProfile = null ;
281
+ const newProfileName = await getImdsProfileHelper ( options , init . maxRetries ?? 3 , init , profile ) ;
282
+ return getCredentialsFromProfile ( newProfileName , options , init ) ;
283
+ }
284
+ throw legacyError ;
285
+ }
286
+ }
287
+ throw error ;
288
+ }
289
+ } ;
168
290
291
+ /**
292
+ * @internal
293
+ * Gets credentials from specified IMDS path
294
+ */
295
+ async function getCredentialsFromPath ( path : string , options : RequestOptions ) {
296
+ const response = await httpRequest ( {
297
+ ...options ,
298
+ path,
299
+ } ) ;
300
+
301
+ const credentialsResponse = JSON . parse ( response . toString ( ) ) ;
302
+
303
+ // Validate response
169
304
if ( ! isImdsCredentials ( credentialsResponse ) ) {
170
- throw new CredentialsProviderError ( "Invalid response received from instance metadata service." , {
171
- logger : init . logger ,
172
- } ) ;
305
+ throw new CredentialsProviderError ( "Invalid response received from instance metadata service." ) ;
173
306
}
174
-
307
+
308
+ // Convert IMDS credentials format to standard format
175
309
return fromImdsCredentials ( credentialsResponse ) ;
176
- } ;
310
+ }
0 commit comments