Skip to content

Commit c865c2a

Browse files
committed
feat: add support for accountId in imds
1 parent f4ab06a commit c865c2a

File tree

2 files changed

+170
-32
lines changed

2 files changed

+170
-32
lines changed

packages/credential-provider-imds/src/fromInstanceMetadata.ts

Lines changed: 165 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,20 @@ import { InstanceMetadataCredentials } from "./types";
1212
import { getInstanceMetadataEndpoint } from "./utils/getInstanceMetadataEndpoint";
1313
import { 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/";
1717
const AWS_EC2_METADATA_V1_DISABLED = "AWS_EC2_METADATA_V1_DISABLED";
1818
const PROFILE_AWS_EC2_METADATA_V1_DISABLED = "ec2_metadata_v1_disabled";
19+
const IMDS_TOKEN_PATH = "/latest/api/token";
1920
const 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+
147195
const 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+
*/
159267
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+
};
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+
}

packages/credential-provider-imds/src/remoteProvider/RemoteProviderInit.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const DEFAULT_TIMEOUT = 1000;
1010
/**
1111
* @internal
1212
*/
13-
export const DEFAULT_MAX_RETRIES = 0;
13+
export const DEFAULT_MAX_RETRIES = 3;
1414

1515
/**
1616
* @public
@@ -36,6 +36,10 @@ export interface RemoteProviderInit extends Partial<RemoteProviderConfig> {
3636
* Only used in the IMDS credential provider.
3737
*/
3838
ec2MetadataV1Disabled?: boolean;
39+
/**
40+
* Explicitly specify EC2 instance profile name (IAM role) to use on the EC2 instance.
41+
*/
42+
ec2InstanceProfileName?: string;
3943
/**
4044
* AWS_PROFILE.
4145
*/

0 commit comments

Comments
 (0)