Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: do not call metadata server if security creds and region are retrievable through environment vars #1493

Merged
merged 9 commits into from
Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 49 additions & 22 deletions src/auth/awsclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import {GaxiosOptions} from 'gaxios';

import {AwsRequestSigner} from './awsrequestsigner';
import {AwsRequestSigner, AwsSecurityCredentials} from './awsrequestsigner';
import {
BaseExternalAccountClient,
BaseExternalAccountClientOptions,
Expand Down Expand Up @@ -48,7 +48,7 @@ export interface AwsClientOptions extends BaseExternalAccountClientOptions {
/**
* Interface defining the AWS security-credentials endpoint response.
*/
interface AwsSecurityCredentials {
interface AwsSecurityCredentialsResponse {
Code: string;
LastUpdated: string;
Type: string;
Expand Down Expand Up @@ -101,7 +101,7 @@ export class AwsClient extends BaseExternalAccountClient {
this.awsRequestSigner = null;
this.region = '';

// data validators
// Data validators.
this.validateEnvironmentId();
this.validateMetadataServerURLs();
}
Expand Down Expand Up @@ -168,7 +168,12 @@ export class AwsClient extends BaseExternalAccountClient {
// Initialize AWS request signer if not already initialized.
if (!this.awsRequestSigner) {
const metadataHeaders: Headers = {};
if (this.imdsV2SessionTokenUrl) {
// Only retrieve the IMDSv2 session token if both the security credentials and region are
// not retrievable through the environment.
lsirac marked this conversation as resolved.
Show resolved Hide resolved
// The credential config contains all the URLs by default but clients may be running this
// where the metadata server is not available and returning the credentials through the environment.
// Removing this check may break them.
if (this.shouldUseMetadataServer() && this.imdsV2SessionTokenUrl) {
metadataHeaders['x-aws-ec2-metadata-token'] =
await this.getImdsV2SessionToken();
}
Expand All @@ -177,16 +182,8 @@ export class AwsClient extends BaseExternalAccountClient {
this.awsRequestSigner = new AwsRequestSigner(async () => {
// Check environment variables for permanent credentials first.
// https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
if (
process.env['AWS_ACCESS_KEY_ID'] &&
process.env['AWS_SECRET_ACCESS_KEY']
) {
return {
accessKeyId: process.env['AWS_ACCESS_KEY_ID']!,
secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY']!,
// This is normally not available for permanent credentials.
token: process.env['AWS_SESSION_TOKEN'],
};
if (this.securityCredentialsFromEnv) {
return this.securityCredentialsFromEnv;
}
// Since the role on a VM can change, we don't need to cache it.
const roleName = await this.getAwsRoleName(metadataHeaders);
Expand Down Expand Up @@ -273,8 +270,8 @@ export class AwsClient extends BaseExternalAccountClient {
private async getAwsRegion(headers: Headers): Promise<string> {
// Priority order for region determination:
// AWS_REGION > AWS_DEFAULT_REGION > metadata server.
if (process.env['AWS_REGION'] || process.env['AWS_DEFAULT_REGION']) {
return (process.env['AWS_REGION'] || process.env['AWS_DEFAULT_REGION'])!;
if (this.regionFromEnv) {
return this.regionFromEnv;
}
if (!this.regionUrl) {
throw new Error(
Expand Down Expand Up @@ -327,12 +324,42 @@ export class AwsClient extends BaseExternalAccountClient {
private async getAwsSecurityCredentials(
roleName: string,
headers: Headers
): Promise<AwsSecurityCredentials> {
const response = await this.transporter.request<AwsSecurityCredentials>({
url: `${this.securityCredentialsUrl}/${roleName}`,
responseType: 'json',
headers: headers,
});
): Promise<AwsSecurityCredentialsResponse> {
const response =
await this.transporter.request<AwsSecurityCredentialsResponse>({
url: `${this.securityCredentialsUrl}/${roleName}`,
responseType: 'json',
headers: headers,
});
return response.data;
}

private shouldUseMetadataServer(): boolean {
lsirac marked this conversation as resolved.
Show resolved Hide resolved
// The metadata server must be used when either the AWS region or AWS security
// credentials cannot be retrieved through their defined environment variables.
return !this.regionFromEnv || !this.securityCredentialsFromEnv;
}

private get regionFromEnv(): string | null {
// The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION.
// Only one is required.
return (
process.env['AWS_REGION'] || process.env['AWS_DEFAULT_REGION'] || null
);
}

private get securityCredentialsFromEnv(): AwsSecurityCredentials | null {
// Both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required.
if (
process.env['AWS_ACCESS_KEY_ID'] &&
process.env['AWS_SECRET_ACCESS_KEY']
) {
return {
accessKeyId: process.env['AWS_ACCESS_KEY_ID'],
secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY'],
token: process.env['AWS_SESSION_TOKEN'],
};
}
return null;
}
}
2 changes: 1 addition & 1 deletion src/auth/awsrequestsigner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ interface AwsAuthHeaderMap {
* These are either determined from AWS security_credentials endpoint or
* AWS environment variables.
*/
interface AwsSecurityCredentials {
export interface AwsSecurityCredentials {
accessKeyId: string;
secretAccessKey: string;
token?: string;
Expand Down
182 changes: 169 additions & 13 deletions test/test.awsclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,24 @@ describe('AwsClient', () => {
'https://sts.{region}.amazonaws.com?' +
'Action=GetCallerIdentity&Version=2011-06-15',
};
const awsCredentialSourceWithImdsv2 = Object.assign(
{imdsv2_session_token_url: `${metadataBaseUrl}/latest/api/token`},
awsCredentialSource
);
const awsOptions = {
type: 'external_account',
audience,
subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request',
token_url: getTokenUrl(),
credential_source: awsCredentialSource,
};
const awsOptionsWithImdsv2 = {
type: 'external_account',
audience,
subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request',
token_url: getTokenUrl(),
credential_source: awsCredentialSourceWithImdsv2,
};
const awsOptionsWithSA = Object.assign(
{
service_account_impersonation_url: getServiceAccountImpersonationUrl(),
Expand Down Expand Up @@ -385,19 +396,7 @@ describe('AwsClient', () => {
.reply(200, awsSecurityCredentials)
);

const credentialSourceWithSessionTokenUrl = Object.assign(
{imdsv2_session_token_url: `${metadataBaseUrl}/latest/api/token`},
awsCredentialSource
);
const awsOptionsWithSessionTokenUrl = {
type: 'external_account',
audience,
subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request',
token_url: getTokenUrl(),
credential_source: credentialSourceWithSessionTokenUrl,
};

const client = new AwsClient(awsOptionsWithSessionTokenUrl);
const client = new AwsClient(awsOptionsWithImdsv2);
const subjectToken = await client.retrieveSubjectToken();

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

assert.deepEqual(subjectToken, expectedSubjectTokenNoToken);
});

it('should resolve on success for permanent creds with imdsv2', async () => {
process.env.AWS_ACCESS_KEY_ID = accessKeyId;
process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey;

const scopes: nock.Scope[] = [];
scopes.push(
nock(metadataBaseUrl, {
reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'},
})
.put('/latest/api/token')
.reply(200, awsSessionToken)
);

scopes.push(
nock(metadataBaseUrl, {
reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken},
})
.get('/latest/meta-data/placement/availability-zone')
.reply(200, `${awsRegion}b`)
);

const client = new AwsClient(awsOptionsWithImdsv2);
const subjectToken = await client.retrieveSubjectToken();

assert.deepEqual(subjectToken, expectedSubjectTokenNoToken);
scopes.forEach(scope => scope.done());
});

it('should resolve on success for temporary creds with imdsv2', async () => {
process.env.AWS_ACCESS_KEY_ID = accessKeyId;
process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey;
process.env.AWS_SESSION_TOKEN = token;

const scopes: nock.Scope[] = [];
scopes.push(
nock(metadataBaseUrl, {
reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'},
})
.put('/latest/api/token')
.reply(200, awsSessionToken)
);

scopes.push(
nock(metadataBaseUrl, {
reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken},
})
.get('/latest/meta-data/placement/availability-zone')
.reply(200, `${awsRegion}b`)
);

const client = new AwsClient(awsOptionsWithImdsv2);
const subjectToken = await client.retrieveSubjectToken();

assert.deepEqual(subjectToken, expectedSubjectToken);
scopes.forEach(scope => scope.done());
});

it('should not call metadata server with imdsv2 if creds are retrievable through env', async () => {
process.env.AWS_ACCESS_KEY_ID = accessKeyId;
process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey;
process.env.AWS_REGION = awsRegion;

const client = new AwsClient(awsOptionsWithImdsv2);
const subjectToken = await client.retrieveSubjectToken();

assert.deepEqual(subjectToken, expectedSubjectTokenNoToken);
});

it('should call metadata server with imdsv2 if creds are not retrievable through env', async () => {
process.env.AWS_REGION = awsRegion;

const scopes: nock.Scope[] = [];
scopes.push(
nock(metadataBaseUrl, {
reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'},
})
.put('/latest/api/token')
.reply(200, awsSessionToken)
);

scopes.push(
nock(metadataBaseUrl, {
reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken},
})
.get('/latest/meta-data/iam/security-credentials')
.reply(200, awsRole)
.get(`/latest/meta-data/iam/security-credentials/${awsRole}`)
.reply(200, awsSecurityCredentials)
);

const client = new AwsClient(awsOptionsWithImdsv2);
const subjectToken = await client.retrieveSubjectToken();

assert.deepEqual(subjectToken, expectedSubjectToken);
scopes.forEach(scope => scope.done());
});

it('should call metadata server with imdsv2 if secret access key is not not retrievable through env', async () => {
process.env.AWS_REGION = awsRegion;
process.env.AWS_ACCESS_KEY_ID = accessKeyId;

const scopes: nock.Scope[] = [];
scopes.push(
nock(metadataBaseUrl, {
reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'},
})
.put('/latest/api/token')
.reply(200, awsSessionToken)
);

scopes.push(
nock(metadataBaseUrl, {
reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken},
})
.get('/latest/meta-data/iam/security-credentials')
.reply(200, awsRole)
.get(`/latest/meta-data/iam/security-credentials/${awsRole}`)
.reply(200, awsSecurityCredentials)
);

const client = new AwsClient(awsOptionsWithImdsv2);
const subjectToken = await client.retrieveSubjectToken();

assert.deepEqual(subjectToken, expectedSubjectToken);
scopes.forEach(scope => scope.done());
});

it('should call metadata server with imdsv2 if access key is not not retrievable through env', async () => {
process.env.AWS_DEFAULT_REGION = awsRegion;
process.env.AWS_SECRET_ACCESS_KEY = accessKeyId;

const scopes: nock.Scope[] = [];
scopes.push(
nock(metadataBaseUrl, {
reqheaders: {'x-aws-ec2-metadata-token-ttl-seconds': '300'},
})
.put('/latest/api/token')
.reply(200, awsSessionToken)
);

scopes.push(
nock(metadataBaseUrl, {
reqheaders: {'x-aws-ec2-metadata-token': awsSessionToken},
})
.get('/latest/meta-data/iam/security-credentials')
.reply(200, awsRole)
.get(`/latest/meta-data/iam/security-credentials/${awsRole}`)
.reply(200, awsSecurityCredentials)
);

const client = new AwsClient(awsOptionsWithImdsv2);
const subjectToken = await client.retrieveSubjectToken();

assert.deepEqual(subjectToken, expectedSubjectToken);
scopes.forEach(scope => scope.done());
});
});

describe('getAccessToken()', () => {
Expand Down