Skip to content

Commit

Permalink
fix: do not call metadata server if security creds and region are ret…
Browse files Browse the repository at this point in the history
…rievable 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>
  • Loading branch information
lsirac and gcf-owl-bot[bot] authored Nov 29, 2022
1 parent 74a9fff commit d4de941
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 36 deletions.
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.
// 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 {
// 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

0 comments on commit d4de941

Please sign in to comment.