Skip to content
This repository was archived by the owner on Nov 20, 2025. It is now read-only.

Commit d4de941

Browse files
fix: do not call metadata server if security creds and region are retrievable through environment vars (#1493)
* fix: do not call metadata server if security creds and region are retrievable through environment vars * comments * refactor * review * fix for consistency * lint * remove docs check * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent 74a9fff commit d4de941

File tree

3 files changed

+219
-36
lines changed

3 files changed

+219
-36
lines changed

src/auth/awsclient.ts

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import {GaxiosOptions} from 'gaxios';
1616

17-
import {AwsRequestSigner} from './awsrequestsigner';
17+
import {AwsRequestSigner, AwsSecurityCredentials} from './awsrequestsigner';
1818
import {
1919
BaseExternalAccountClient,
2020
BaseExternalAccountClientOptions,
@@ -48,7 +48,7 @@ export interface AwsClientOptions extends BaseExternalAccountClientOptions {
4848
/**
4949
* Interface defining the AWS security-credentials endpoint response.
5050
*/
51-
interface AwsSecurityCredentials {
51+
interface AwsSecurityCredentialsResponse {
5252
Code: string;
5353
LastUpdated: string;
5454
Type: string;
@@ -101,7 +101,7 @@ export class AwsClient extends BaseExternalAccountClient {
101101
this.awsRequestSigner = null;
102102
this.region = '';
103103

104-
// data validators
104+
// Data validators.
105105
this.validateEnvironmentId();
106106
this.validateMetadataServerURLs();
107107
}
@@ -168,7 +168,12 @@ export class AwsClient extends BaseExternalAccountClient {
168168
// Initialize AWS request signer if not already initialized.
169169
if (!this.awsRequestSigner) {
170170
const metadataHeaders: Headers = {};
171-
if (this.imdsV2SessionTokenUrl) {
171+
// Only retrieve the IMDSv2 session token if both the security credentials and region are
172+
// not retrievable through the environment.
173+
// The credential config contains all the URLs by default but clients may be running this
174+
// where the metadata server is not available and returning the credentials through the environment.
175+
// Removing this check may break them.
176+
if (this.shouldUseMetadataServer() && this.imdsV2SessionTokenUrl) {
172177
metadataHeaders['x-aws-ec2-metadata-token'] =
173178
await this.getImdsV2SessionToken();
174179
}
@@ -177,16 +182,8 @@ export class AwsClient extends BaseExternalAccountClient {
177182
this.awsRequestSigner = new AwsRequestSigner(async () => {
178183
// Check environment variables for permanent credentials first.
179184
// https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
180-
if (
181-
process.env['AWS_ACCESS_KEY_ID'] &&
182-
process.env['AWS_SECRET_ACCESS_KEY']
183-
) {
184-
return {
185-
accessKeyId: process.env['AWS_ACCESS_KEY_ID']!,
186-
secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY']!,
187-
// This is normally not available for permanent credentials.
188-
token: process.env['AWS_SESSION_TOKEN'],
189-
};
185+
if (this.securityCredentialsFromEnv) {
186+
return this.securityCredentialsFromEnv;
190187
}
191188
// Since the role on a VM can change, we don't need to cache it.
192189
const roleName = await this.getAwsRoleName(metadataHeaders);
@@ -273,8 +270,8 @@ export class AwsClient extends BaseExternalAccountClient {
273270
private async getAwsRegion(headers: Headers): Promise<string> {
274271
// Priority order for region determination:
275272
// AWS_REGION > AWS_DEFAULT_REGION > metadata server.
276-
if (process.env['AWS_REGION'] || process.env['AWS_DEFAULT_REGION']) {
277-
return (process.env['AWS_REGION'] || process.env['AWS_DEFAULT_REGION'])!;
273+
if (this.regionFromEnv) {
274+
return this.regionFromEnv;
278275
}
279276
if (!this.regionUrl) {
280277
throw new Error(
@@ -327,12 +324,42 @@ export class AwsClient extends BaseExternalAccountClient {
327324
private async getAwsSecurityCredentials(
328325
roleName: string,
329326
headers: Headers
330-
): Promise<AwsSecurityCredentials> {
331-
const response = await this.transporter.request<AwsSecurityCredentials>({
332-
url: `${this.securityCredentialsUrl}/${roleName}`,
333-
responseType: 'json',
334-
headers: headers,
335-
});
327+
): Promise<AwsSecurityCredentialsResponse> {
328+
const response =
329+
await this.transporter.request<AwsSecurityCredentialsResponse>({
330+
url: `${this.securityCredentialsUrl}/${roleName}`,
331+
responseType: 'json',
332+
headers: headers,
333+
});
336334
return response.data;
337335
}
336+
337+
private shouldUseMetadataServer(): boolean {
338+
// The metadata server must be used when either the AWS region or AWS security
339+
// credentials cannot be retrieved through their defined environment variables.
340+
return !this.regionFromEnv || !this.securityCredentialsFromEnv;
341+
}
342+
343+
private get regionFromEnv(): string | null {
344+
// The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION.
345+
// Only one is required.
346+
return (
347+
process.env['AWS_REGION'] || process.env['AWS_DEFAULT_REGION'] || null
348+
);
349+
}
350+
351+
private get securityCredentialsFromEnv(): AwsSecurityCredentials | null {
352+
// Both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required.
353+
if (
354+
process.env['AWS_ACCESS_KEY_ID'] &&
355+
process.env['AWS_SECRET_ACCESS_KEY']
356+
) {
357+
return {
358+
accessKeyId: process.env['AWS_ACCESS_KEY_ID'],
359+
secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY'],
360+
token: process.env['AWS_SESSION_TOKEN'],
361+
};
362+
}
363+
return null;
364+
}
338365
}

src/auth/awsrequestsigner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ interface AwsAuthHeaderMap {
4040
* These are either determined from AWS security_credentials endpoint or
4141
* AWS environment variables.
4242
*/
43-
interface AwsSecurityCredentials {
43+
export interface AwsSecurityCredentials {
4444
accessKeyId: string;
4545
secretAccessKey: string;
4646
token?: string;

test/test.awsclient.ts

Lines changed: 169 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,24 @@ describe('AwsClient', () => {
5555
'https://sts.{region}.amazonaws.com?' +
5656
'Action=GetCallerIdentity&Version=2011-06-15',
5757
};
58+
const awsCredentialSourceWithImdsv2 = Object.assign(
59+
{imdsv2_session_token_url: `${metadataBaseUrl}/latest/api/token`},
60+
awsCredentialSource
61+
);
5862
const awsOptions = {
5963
type: 'external_account',
6064
audience,
6165
subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request',
6266
token_url: getTokenUrl(),
6367
credential_source: awsCredentialSource,
6468
};
69+
const awsOptionsWithImdsv2 = {
70+
type: 'external_account',
71+
audience,
72+
subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request',
73+
token_url: getTokenUrl(),
74+
credential_source: awsCredentialSourceWithImdsv2,
75+
};
6576
const awsOptionsWithSA = Object.assign(
6677
{
6778
service_account_impersonation_url: getServiceAccountImpersonationUrl(),
@@ -385,19 +396,7 @@ describe('AwsClient', () => {
385396
.reply(200, awsSecurityCredentials)
386397
);
387398

388-
const credentialSourceWithSessionTokenUrl = Object.assign(
389-
{imdsv2_session_token_url: `${metadataBaseUrl}/latest/api/token`},
390-
awsCredentialSource
391-
);
392-
const awsOptionsWithSessionTokenUrl = {
393-
type: 'external_account',
394-
audience,
395-
subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request',
396-
token_url: getTokenUrl(),
397-
credential_source: credentialSourceWithSessionTokenUrl,
398-
};
399-
400-
const client = new AwsClient(awsOptionsWithSessionTokenUrl);
399+
const client = new AwsClient(awsOptionsWithImdsv2);
401400
const subjectToken = await client.retrieveSubjectToken();
402401

403402
assert.deepEqual(subjectToken, expectedSubjectToken);
@@ -829,6 +828,163 @@ describe('AwsClient', () => {
829828

830829
assert.deepEqual(subjectToken, expectedSubjectTokenNoToken);
831830
});
831+
832+
it('should resolve on success for permanent creds with imdsv2', async () => {
833+
process.env.AWS_ACCESS_KEY_ID = accessKeyId;
834+
process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey;
835+
836+
const scopes: nock.Scope[] = [];
837+
scopes.push(
838+
nock(metadataBaseUrl, {
839+
reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'},
840+
})
841+
.put('/latest/api/token')
842+
.reply(200, awsSessionToken)
843+
);
844+
845+
scopes.push(
846+
nock(metadataBaseUrl, {
847+
reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken},
848+
})
849+
.get('/latest/meta-data/placement/availability-zone')
850+
.reply(200, `${awsRegion}b`)
851+
);
852+
853+
const client = new AwsClient(awsOptionsWithImdsv2);
854+
const subjectToken = await client.retrieveSubjectToken();
855+
856+
assert.deepEqual(subjectToken, expectedSubjectTokenNoToken);
857+
scopes.forEach(scope => scope.done());
858+
});
859+
860+
it('should resolve on success for temporary creds with imdsv2', async () => {
861+
process.env.AWS_ACCESS_KEY_ID = accessKeyId;
862+
process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey;
863+
process.env.AWS_SESSION_TOKEN = token;
864+
865+
const scopes: nock.Scope[] = [];
866+
scopes.push(
867+
nock(metadataBaseUrl, {
868+
reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'},
869+
})
870+
.put('/latest/api/token')
871+
.reply(200, awsSessionToken)
872+
);
873+
874+
scopes.push(
875+
nock(metadataBaseUrl, {
876+
reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken},
877+
})
878+
.get('/latest/meta-data/placement/availability-zone')
879+
.reply(200, `${awsRegion}b`)
880+
);
881+
882+
const client = new AwsClient(awsOptionsWithImdsv2);
883+
const subjectToken = await client.retrieveSubjectToken();
884+
885+
assert.deepEqual(subjectToken, expectedSubjectToken);
886+
scopes.forEach(scope => scope.done());
887+
});
888+
889+
it('should not call metadata server with imdsv2 if creds are retrievable through env', async () => {
890+
process.env.AWS_ACCESS_KEY_ID = accessKeyId;
891+
process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey;
892+
process.env.AWS_REGION = awsRegion;
893+
894+
const client = new AwsClient(awsOptionsWithImdsv2);
895+
const subjectToken = await client.retrieveSubjectToken();
896+
897+
assert.deepEqual(subjectToken, expectedSubjectTokenNoToken);
898+
});
899+
900+
it('should call metadata server with imdsv2 if creds are not retrievable through env', async () => {
901+
process.env.AWS_REGION = awsRegion;
902+
903+
const scopes: nock.Scope[] = [];
904+
scopes.push(
905+
nock(metadataBaseUrl, {
906+
reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'},
907+
})
908+
.put('/latest/api/token')
909+
.reply(200, awsSessionToken)
910+
);
911+
912+
scopes.push(
913+
nock(metadataBaseUrl, {
914+
reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken},
915+
})
916+
.get('/latest/meta-data/iam/security-credentials')
917+
.reply(200, awsRole)
918+
.get(`/latest/meta-data/iam/security-credentials/${awsRole}`)
919+
.reply(200, awsSecurityCredentials)
920+
);
921+
922+
const client = new AwsClient(awsOptionsWithImdsv2);
923+
const subjectToken = await client.retrieveSubjectToken();
924+
925+
assert.deepEqual(subjectToken, expectedSubjectToken);
926+
scopes.forEach(scope => scope.done());
927+
});
928+
929+
it('should call metadata server with imdsv2 if secret access key is not not retrievable through env', async () => {
930+
process.env.AWS_REGION = awsRegion;
931+
process.env.AWS_ACCESS_KEY_ID = accessKeyId;
932+
933+
const scopes: nock.Scope[] = [];
934+
scopes.push(
935+
nock(metadataBaseUrl, {
936+
reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'},
937+
})
938+
.put('/latest/api/token')
939+
.reply(200, awsSessionToken)
940+
);
941+
942+
scopes.push(
943+
nock(metadataBaseUrl, {
944+
reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken},
945+
})
946+
.get('/latest/meta-data/iam/security-credentials')
947+
.reply(200, awsRole)
948+
.get(`/latest/meta-data/iam/security-credentials/${awsRole}`)
949+
.reply(200, awsSecurityCredentials)
950+
);
951+
952+
const client = new AwsClient(awsOptionsWithImdsv2);
953+
const subjectToken = await client.retrieveSubjectToken();
954+
955+
assert.deepEqual(subjectToken, expectedSubjectToken);
956+
scopes.forEach(scope => scope.done());
957+
});
958+
959+
it('should call metadata server with imdsv2 if access key is not not retrievable through env', async () => {
960+
process.env.AWS_DEFAULT_REGION = awsRegion;
961+
process.env.AWS_SECRET_ACCESS_KEY = accessKeyId;
962+
963+
const scopes: nock.Scope[] = [];
964+
scopes.push(
965+
nock(metadataBaseUrl, {
966+
reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'},
967+
})
968+
.put('/latest/api/token')
969+
.reply(200, awsSessionToken)
970+
);
971+
972+
scopes.push(
973+
nock(metadataBaseUrl, {
974+
reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken},
975+
})
976+
.get('/latest/meta-data/iam/security-credentials')
977+
.reply(200, awsRole)
978+
.get(`/latest/meta-data/iam/security-credentials/${awsRole}`)
979+
.reply(200, awsSecurityCredentials)
980+
);
981+
982+
const client = new AwsClient(awsOptionsWithImdsv2);
983+
const subjectToken = await client.retrieveSubjectToken();
984+
985+
assert.deepEqual(subjectToken, expectedSubjectToken);
986+
scopes.forEach(scope => scope.done());
987+
});
832988
});
833989

834990
describe('getAccessToken()', () => {

0 commit comments

Comments
 (0)