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

feat: Add the ability to use a web identity token file #240

Merged
merged 9 commits into from
Aug 3, 2021
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 if using [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.
nesta219 marked this conversation as resolved.
Show resolved Hide resolved

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: >-
Read the web identity token file from the provided file system path in order to
assume an IAM role using a web identity on an EKS worker node
nesta219 marked this conversation as resolved.
Show resolved Hide resolved
required: false
role-duration-seconds:
description: "Role duration in seconds (default: 6 hours)"
required: false
Expand Down
33 changes: 22 additions & 11 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const core = require('@actions/core');
const aws = require('aws-sdk');
const assert = require('assert');
const fs = require('fs').promises;

// 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 +23,8 @@ async function assumeRole(params) {
roleDurationSeconds,
roleSessionName,
region,
roleSkipSessionTagging
roleSkipSessionTagging,
webIdentityTokenFile
} = params;
assert(
[sourceAccountId, roleToAssume, roleDurationSeconds, roleSessionName, region].every(isDefined),
Expand Down Expand Up @@ -74,15 +76,22 @@ 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;

if(isDefined(webIdentityTokenFile)) {
assumeRoleRequest.WebIdentityToken = await fs.readFile(webIdentityTokenFile, 'utf8');
nesta219 marked this conversation as resolved.
Show resolved Hide resolved
assumeFunction = sts.assumeRoleWithWebIdentity;
}

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 +220,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 +259,8 @@ async function run() {
roleExternalId,
roleDurationSeconds,
roleSessionName,
roleSkipSessionTagging
roleSkipSessionTagging,
webIdentityTokenFile
});
exportCredentials(roleCredentials);
await validateCredentials(roleCredentials.accessKeyId);
Expand Down
47 changes: 47 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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 +56,19 @@ jest.mock('aws-sdk', () => {
STS: jest.fn(() => ({
getCallerIdentity: mockStsCallerIdentity,
assumeRole: mockStsAssumeRole,
assumeRoleWithWebIdentity: mockStsAssumeRoleWithWebIdentity
}))
};
});

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

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

Expand Down Expand Up @@ -119,6 +129,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 +531,29 @@ describe('Configure AWS Credentials', () => {
})
});

test('web identity token file provided', 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',
Tags: [
{Key: 'GitHub', Value: 'Actions'},
{Key: 'Repository', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REPOSITORY},
{Key: 'Workflow', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_WORKFLOW},
{Key: 'Action', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_ACTION},
{Key: 'Actor', Value: GITHUB_ACTOR_SANITIZED},
{Key: 'Commit', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_SHA},
{Key: 'Branch', Value: ENVIRONMENT_VARIABLE_OVERRIDES.GITHUB_REF},
]
})
});

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