Skip to content

Commit 8053174

Browse files
feat: Add the ability to use a web identity token file (#240)
* 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>
1 parent ef6b971 commit 8053174

File tree

4 files changed

+110
-11
lines changed

4 files changed

+110
-11
lines changed

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,17 @@ with:
189189
```
190190
In this case, your runner's credentials must have permissions to assume the role.
191191

192+
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.
193+
194+
You can configure your workflow as follows in order to use this file:
195+
```yaml
196+
uses: aws-actions/configure-aws-credentials@v1
197+
with:
198+
aws-region: us-east-2
199+
role-to-assume: my-github-actions-role
200+
web-identity-token-file: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
201+
```
202+
192203
### Use with the AWS CLI
193204

194205
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.

action.yml

+5
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ inputs:
3434
environment with the assumed role credentials rather than with the provided
3535
credentials
3636
required: false
37+
web-identity-token-file:
38+
description: >-
39+
Use the web identity token file from the provided file system path in order to
40+
assume an IAM role using a web identity. E.g., from within an Amazon EKS worker node
41+
required: false
3742
role-duration-seconds:
3843
description: "Role duration in seconds (default: 6 hours)"
3944
required: false

index.js

+40-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
const core = require('@actions/core');
22
const aws = require('aws-sdk');
33
const assert = require('assert');
4+
const fs = require('fs');
5+
const path = require('path');
46

57
// The max time that a GitHub action is allowed to run is 6 hours.
68
// That seems like a reasonable default to use if no role duration is defined.
@@ -22,7 +24,8 @@ async function assumeRole(params) {
2224
roleDurationSeconds,
2325
roleSessionName,
2426
region,
25-
roleSkipSessionTagging
27+
roleSkipSessionTagging,
28+
webIdentityTokenFile
2629
} = params;
2730
assert(
2831
[sourceAccountId, roleToAssume, roleDurationSeconds, roleSessionName, region].every(isDefined),
@@ -42,6 +45,7 @@ async function assumeRole(params) {
4245
// Supports only 'aws' partition. Customers in other partitions ('aws-cn') will need to provide full ARN
4346
roleArn = `arn:aws:iam::${sourceAccountId}:role/${roleArn}`;
4447
}
48+
4549
const tagArray = [
4650
{Key: 'GitHub', Value: 'Actions'},
4751
{Key: 'Repository', Value: GITHUB_REPOSITORY},
@@ -74,15 +78,38 @@ async function assumeRole(params) {
7478
assumeRoleRequest.ExternalId = roleExternalId;
7579
}
7680

77-
return sts.assumeRole(assumeRoleRequest)
78-
.promise()
79-
.then(function (data) {
80-
return {
81-
accessKeyId: data.Credentials.AccessKeyId,
82-
secretAccessKey: data.Credentials.SecretAccessKey,
83-
sessionToken: data.Credentials.SessionToken,
84-
};
85-
});
81+
let assumeFunction = sts.assumeRole.bind(sts);
82+
83+
if(isDefined(webIdentityTokenFile)) {
84+
core.debug("webIdentityTokenFile provided. Will call sts:AssumeRoleWithWebIdentity and take session tags from token contents.")
85+
delete assumeRoleRequest.Tags;
86+
87+
const webIdentityTokenFilePath = path.isAbsolute(webIdentityTokenFile) ?
88+
webIdentityTokenFile :
89+
path.join(process.env.GITHUB_WORKSPACE, webIdentityTokenFile);
90+
91+
if (!fs.existsSync(webIdentityTokenFilePath)) {
92+
throw new Error(`Web identity token file does not exist: ${webIdentityTokenFilePath}`);
93+
}
94+
95+
try {
96+
assumeRoleRequest.WebIdentityToken = await fs.promises.readFile(webIdentityTokenFilePath, 'utf8');
97+
assumeFunction = sts.assumeRoleWithWebIdentity.bind(sts);
98+
} catch(error) {
99+
throw new Error(`Web identity token file could not be read: ${error.message}`);
100+
}
101+
102+
}
103+
104+
return assumeFunction(assumeRoleRequest)
105+
.promise()
106+
.then(function (data) {
107+
return {
108+
accessKeyId: data.Credentials.AccessKeyId,
109+
secretAccessKey: data.Credentials.SecretAccessKey,
110+
sessionToken: data.Credentials.SessionToken,
111+
};
112+
});
86113
}
87114

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

215243
if (!region.match(REGION_REGEX)) {
216244
throw new Error(`Region is not valid: ${region}`);
@@ -249,7 +277,8 @@ async function run() {
249277
roleExternalId,
250278
roleDurationSeconds,
251279
roleSessionName,
252-
roleSkipSessionTagging
280+
roleSkipSessionTagging,
281+
webIdentityTokenFile
253282
});
254283
exportCredentials(roleCredentials);
255284
await validateCredentials(roleCredentials.accessKeyId);

index.test.js

+54
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const ENVIRONMENT_VARIABLE_OVERRIDES = {
2424
GITHUB_ACTOR: 'MY-USERNAME[bot]',
2525
GITHUB_SHA: 'MY-COMMIT-ID',
2626
GITHUB_REF: 'MY-BRANCH',
27+
GITHUB_WORKSPACE: '/home/github'
2728
};
2829
const GITHUB_ACTOR_SANITIZED = 'MY-USERNAME_bot_'
2930

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

4748
const mockStsCallerIdentity = jest.fn();
4849
const mockStsAssumeRole = jest.fn();
50+
const mockStsAssumeRoleWithWebIdentity = jest.fn();
4951

5052
jest.mock('aws-sdk', () => {
5153
return {
@@ -55,10 +57,20 @@ jest.mock('aws-sdk', () => {
5557
STS: jest.fn(() => ({
5658
getCallerIdentity: mockStsCallerIdentity,
5759
assumeRole: mockStsAssumeRole,
60+
assumeRoleWithWebIdentity: mockStsAssumeRoleWithWebIdentity
5861
}))
5962
};
6063
});
6164

65+
jest.mock('fs', () => {
66+
return {
67+
promises: {
68+
readFile: jest.fn(() => Promise.resolve('testpayload')),
69+
},
70+
existsSync: jest.fn(() => true)
71+
};
72+
});
73+
6274
describe('Configure AWS Credentials', () => {
6375
const OLD_ENV = process.env;
6476

@@ -119,6 +131,20 @@ describe('Configure AWS Credentials', () => {
119131
}
120132
}
121133
});
134+
135+
mockStsAssumeRoleWithWebIdentity.mockImplementation(() => {
136+
return {
137+
promise() {
138+
return Promise.resolve({
139+
Credentials: {
140+
AccessKeyId: FAKE_STS_ACCESS_KEY_ID,
141+
SecretAccessKey: FAKE_STS_SECRET_ACCESS_KEY,
142+
SessionToken: FAKE_STS_SESSION_TOKEN
143+
}
144+
});
145+
}
146+
}
147+
});
122148
});
123149

124150
afterEach(() => {
@@ -507,6 +533,34 @@ describe('Configure AWS Credentials', () => {
507533
})
508534
});
509535

536+
test('web identity token file provided with absolute path', async () => {
537+
core.getInput = jest
538+
.fn()
539+
.mockImplementation(mockGetInput({'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION, 'web-identity-token-file': '/fake/token/file'}));
540+
541+
await run();
542+
expect(mockStsAssumeRoleWithWebIdentity).toHaveBeenCalledWith({
543+
RoleArn: 'arn:aws:iam::111111111111:role/MY-ROLE',
544+
RoleSessionName: 'GitHubActions',
545+
DurationSeconds: 6 * 3600,
546+
WebIdentityToken: 'testpayload'
547+
})
548+
});
549+
550+
test('web identity token file provided with relative path', async () => {
551+
core.getInput = jest
552+
.fn()
553+
.mockImplementation(mockGetInput({'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION, 'web-identity-token-file': 'fake/token/file'}));
554+
555+
await run();
556+
expect(mockStsAssumeRoleWithWebIdentity).toHaveBeenCalledWith({
557+
RoleArn: 'arn:aws:iam::111111111111:role/MY-ROLE',
558+
RoleSessionName: 'GitHubActions',
559+
DurationSeconds: 6 * 3600,
560+
WebIdentityToken: 'testpayload'
561+
})
562+
});
563+
510564
test('role external ID provided', async () => {
511565
core.getInput = jest
512566
.fn()

0 commit comments

Comments
 (0)