@@ -12,12 +12,20 @@ import { InstanceMetadataCredentials } from "./types";
1212import { getInstanceMetadataEndpoint } from "./utils/getInstanceMetadataEndpoint" ;
1313import { staticStabilityProvider } from "./utils/staticStabilityProvider" ;
1414
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/ " ;
1717const AWS_EC2_METADATA_V1_DISABLED = "AWS_EC2_METADATA_V1_DISABLED" ;
1818const PROFILE_AWS_EC2_METADATA_V1_DISABLED = "ec2_metadata_v1_disabled" ;
19+ const IMDS_TOKEN_PATH = "/latest/api/token" ;
1920const X_AWS_EC2_METADATA_TOKEN = "x-aws-ec2-metadata-token" ;
2021
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+
2129/**
2230 * @internal
2331 *
@@ -36,10 +44,12 @@ const getInstanceMetadataProvider = (init: RemoteProviderInit = {}) => {
3644 const { logger, profile } = init ;
3745 const { timeout, maxRetries } = providerConfigFromInit ( init ) ;
3846
47+
3948 const getCredentials = async ( maxRetries : number , options : RequestOptions ) => {
4049 const isImdsV1Fallback = disableFetchToken || options . headers ?. [ X_AWS_EC2_METADATA_TOKEN ] == null ;
4150
4251 if ( isImdsV1Fallback ) {
52+ await checkIfImdsDisabled ( profile , logger ) ;
4353 let fallbackBlockedFromProfile = false ;
4454 let fallbackBlockedFromProcessEnv = false ;
4555
@@ -84,20 +94,7 @@ const getInstanceMetadataProvider = (init: RemoteProviderInit = {}) => {
8494 }
8595 }
8696
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 ) ;
10198
10299 return retry ( async ( ) => {
103100 let creds : AwsCredentialIdentity ;
@@ -113,8 +110,10 @@ const getInstanceMetadataProvider = (init: RemoteProviderInit = {}) => {
113110 } , maxRetries ) ;
114111 } ;
115112
116- return async ( ) => {
113+
114+ return async ( ) => {
117115 const endpoint = await getInstanceMetadataEndpoint ( ) ;
116+ await checkIfImdsDisabled ( profile , logger ) ;
118117 if ( disableFetchToken ) {
119118 logger ?. debug ( "AWS SDK Instance Metadata" , "using v1 fallback (no token fetch)" ) ;
120119 return getCredentials ( maxRetries , { ...endpoint , timeout } ) ;
@@ -144,6 +143,55 @@ const getInstanceMetadataProvider = (init: RemoteProviderInit = {}) => {
144143 } ;
145144} ;
146145
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+
147195const getMetadataToken = async ( options : RequestOptions ) =>
148196 httpRequest ( {
149197 ...options ,
@@ -154,23 +202,109 @@ const getMetadataToken = async (options: RequestOptions) =>
154202 } ,
155203 } ) ;
156204
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+
158262
263+ /**
264+ * @internal
265+ * Gets credentials from profile
266+ */
159267const 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+ } ;
168290
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
169304 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." ) ;
173306 }
174-
307+
308+ // Convert IMDS credentials format to standard format
175309 return fromImdsCredentials ( credentialsResponse ) ;
176- } ;
310+ }
0 commit comments