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 4 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
66 changes: 52 additions & 14 deletions src/auth/awsclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ interface AwsSecurityCredentials {
Expiration: string;
}

/**
* Interface defining the AWS security-credentials retrieved from environment variables.
*/
interface AwsSecurityCredentialsFromEnvironment {
accessKeyId: string;
secretAccessKey: string;
token?: string;
}

/**
* AWS external account client. This is used for AWS workloads, where
* AWS STS GetCallerIdentity serialized signed requests are exchanged for
Expand Down Expand Up @@ -101,7 +110,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 +177,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 +191,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.securityCredentialsFromEnvironment) {
return this.securityCredentialsFromEnvironment;
}
// 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 +279,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.regionFromEnvironment) {
return this.regionFromEnvironment;
}
if (!this.regionUrl) {
throw new Error(
Expand Down Expand Up @@ -335,4 +341,36 @@ export class AwsClient extends BaseExternalAccountClient {
});
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.regionFromEnvironment || !this.securityCredentialsFromEnvironment
);
}

private get regionFromEnvironment(): string | undefined {
// 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'];
}

private get securityCredentialsFromEnvironment():
| AwsSecurityCredentialsFromEnvironment
| undefined {
let awsSecurityCredentials;
// Check if both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are available.
if (
process.env['AWS_ACCESS_KEY_ID'] &&
process.env['AWS_SECRET_ACCESS_KEY']
) {
awsSecurityCredentials = {
accessKeyId: process.env['AWS_ACCESS_KEY_ID'],
secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY'],
token: process.env['AWS_SESSION_TOKEN'],
};
}
return awsSecurityCredentials;
}
}
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