Skip to content

Commit

Permalink
feat: Add the ability to use a web identity token file (#240)
Browse files Browse the repository at this point in the history
* feat: Add the ability to use a web identity token file

* mark web identity token file as not required

* fix indentation

* better docs and added support for relative vs absolute paths

* bind sts context and adjust fs calls

* exclude tags if using web identity token file

* fix readme aand adjust tag removal logic

* undo re-ordering of lines

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
nesta219 and mergify[bot] committed Aug 3, 2021
1 parent ef6b971 commit 8053174
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 11 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,17 @@ with:
```
In this case, your runner's credentials must have permissions to assume the role.

You can also assume a role using a web identity token file, such as if using [Amazon EKS IRSA](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts-technical-overview.html). Pods running in EKS worker nodes that do not run as root can use this file to assume a role with a web identity.

You can configure your workflow as follows in order to use this file:
```yaml
uses: aws-actions/configure-aws-credentials@v1
with:
aws-region: us-east-2
role-to-assume: my-github-actions-role
web-identity-token-file: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
```

### Use with the AWS CLI

This workflow does _not_ install the [AWS CLI](https://aws.amazon.com/cli/) into your environment. Self-hosted runners that intend to run this action prior to executing `aws` commands need to have the AWS CLI [installed](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) if it's not already present.
Expand Down
5 changes: 5 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ inputs:
environment with the assumed role credentials rather than with the provided
credentials
required: false
web-identity-token-file:
description: >-
Use the web identity token file from the provided file system path in order to
assume an IAM role using a web identity. E.g., from within an Amazon EKS worker node
required: false
role-duration-seconds:
description: "Role duration in seconds (default: 6 hours)"
required: false
Expand Down
51 changes: 40 additions & 11 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const core = require('@actions/core');
const aws = require('aws-sdk');
const assert = require('assert');
const fs = require('fs');
const path = require('path');

// The max time that a GitHub action is allowed to run is 6 hours.
// That seems like a reasonable default to use if no role duration is defined.
Expand All @@ -22,7 +24,8 @@ async function assumeRole(params) {
roleDurationSeconds,
roleSessionName,
region,
roleSkipSessionTagging
roleSkipSessionTagging,
webIdentityTokenFile
} = params;
assert(
[sourceAccountId, roleToAssume, roleDurationSeconds, roleSessionName, region].every(isDefined),
Expand All @@ -42,6 +45,7 @@ async function assumeRole(params) {
// Supports only 'aws' partition. Customers in other partitions ('aws-cn') will need to provide full ARN
roleArn = `arn:aws:iam::${sourceAccountId}:role/${roleArn}`;
}

const tagArray = [
{Key: 'GitHub', Value: 'Actions'},
{Key: 'Repository', Value: GITHUB_REPOSITORY},
Expand Down Expand Up @@ -74,15 +78,38 @@ async function assumeRole(params) {
assumeRoleRequest.ExternalId = roleExternalId;
}

return sts.assumeRole(assumeRoleRequest)
.promise()
.then(function (data) {
return {
accessKeyId: data.Credentials.AccessKeyId,
secretAccessKey: data.Credentials.SecretAccessKey,
sessionToken: data.Credentials.SessionToken,
};
});
let assumeFunction = sts.assumeRole.bind(sts);

if(isDefined(webIdentityTokenFile)) {
core.debug("webIdentityTokenFile provided. Will call sts:AssumeRoleWithWebIdentity and take session tags from token contents.")
delete assumeRoleRequest.Tags;

const webIdentityTokenFilePath = path.isAbsolute(webIdentityTokenFile) ?
webIdentityTokenFile :
path.join(process.env.GITHUB_WORKSPACE, webIdentityTokenFile);

if (!fs.existsSync(webIdentityTokenFilePath)) {
throw new Error(`Web identity token file does not exist: ${webIdentityTokenFilePath}`);
}

try {
assumeRoleRequest.WebIdentityToken = await fs.promises.readFile(webIdentityTokenFilePath, 'utf8');
assumeFunction = sts.assumeRoleWithWebIdentity.bind(sts);
} catch(error) {
throw new Error(`Web identity token file could not be read: ${error.message}`);
}

}

return assumeFunction(assumeRoleRequest)
.promise()
.then(function (data) {
return {
accessKeyId: data.Credentials.AccessKeyId,
secretAccessKey: data.Credentials.SecretAccessKey,
sessionToken: data.Credentials.SessionToken,
};
});
}

function sanitizeGithubActor(actor) {
Expand Down Expand Up @@ -211,6 +238,7 @@ async function run() {
const roleSessionName = core.getInput('role-session-name', { required: false }) || ROLE_SESSION_NAME;
const roleSkipSessionTaggingInput = core.getInput('role-skip-session-tagging', { required: false })|| 'false';
const roleSkipSessionTagging = roleSkipSessionTaggingInput.toLowerCase() === 'true';
const webIdentityTokenFile = core.getInput('web-identity-token-file', { required: false })

if (!region.match(REGION_REGEX)) {
throw new Error(`Region is not valid: ${region}`);
Expand Down Expand Up @@ -249,7 +277,8 @@ async function run() {
roleExternalId,
roleDurationSeconds,
roleSessionName,
roleSkipSessionTagging
roleSkipSessionTagging,
webIdentityTokenFile
});
exportCredentials(roleCredentials);
await validateCredentials(roleCredentials.accessKeyId);
Expand Down
54 changes: 54 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const ENVIRONMENT_VARIABLE_OVERRIDES = {
GITHUB_ACTOR: 'MY-USERNAME[bot]',
GITHUB_SHA: 'MY-COMMIT-ID',
GITHUB_REF: 'MY-BRANCH',
GITHUB_WORKSPACE: '/home/github'
};
const GITHUB_ACTOR_SANITIZED = 'MY-USERNAME_bot_'

Expand All @@ -46,6 +47,7 @@ const ASSUME_ROLE_INPUTS = {...CREDS_INPUTS, 'role-to-assume': ROLE_ARN, 'aws-re

const mockStsCallerIdentity = jest.fn();
const mockStsAssumeRole = jest.fn();
const mockStsAssumeRoleWithWebIdentity = jest.fn();

jest.mock('aws-sdk', () => {
return {
Expand All @@ -55,10 +57,20 @@ jest.mock('aws-sdk', () => {
STS: jest.fn(() => ({
getCallerIdentity: mockStsCallerIdentity,
assumeRole: mockStsAssumeRole,
assumeRoleWithWebIdentity: mockStsAssumeRoleWithWebIdentity
}))
};
});

jest.mock('fs', () => {
return {
promises: {
readFile: jest.fn(() => Promise.resolve('testpayload')),
},
existsSync: jest.fn(() => true)
};
});

describe('Configure AWS Credentials', () => {
const OLD_ENV = process.env;

Expand Down Expand Up @@ -119,6 +131,20 @@ describe('Configure AWS Credentials', () => {
}
}
});

mockStsAssumeRoleWithWebIdentity.mockImplementation(() => {
return {
promise() {
return Promise.resolve({
Credentials: {
AccessKeyId: FAKE_STS_ACCESS_KEY_ID,
SecretAccessKey: FAKE_STS_SECRET_ACCESS_KEY,
SessionToken: FAKE_STS_SESSION_TOKEN
}
});
}
}
});
});

afterEach(() => {
Expand Down Expand Up @@ -507,6 +533,34 @@ describe('Configure AWS Credentials', () => {
})
});

test('web identity token file provided with absolute path', async () => {
core.getInput = jest
.fn()
.mockImplementation(mockGetInput({'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION, 'web-identity-token-file': '/fake/token/file'}));

await run();
expect(mockStsAssumeRoleWithWebIdentity).toHaveBeenCalledWith({
RoleArn: 'arn:aws:iam::111111111111:role/MY-ROLE',
RoleSessionName: 'GitHubActions',
DurationSeconds: 6 * 3600,
WebIdentityToken: 'testpayload'
})
});

test('web identity token file provided with relative path', async () => {
core.getInput = jest
.fn()
.mockImplementation(mockGetInput({'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION, 'web-identity-token-file': 'fake/token/file'}));

await run();
expect(mockStsAssumeRoleWithWebIdentity).toHaveBeenCalledWith({
RoleArn: 'arn:aws:iam::111111111111:role/MY-ROLE',
RoleSessionName: 'GitHubActions',
DurationSeconds: 6 * 3600,
WebIdentityToken: 'testpayload'
})
});

test('role external ID provided', async () => {
core.getInput = jest
.fn()
Expand Down

0 comments on commit 8053174

Please sign in to comment.