mirror of
https://github.com/aws-actions/configure-aws-credentials.git
synced 2026-09-03 06:05:04 +09:00
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>
This commit is contained in:
@@ -189,6 +189,17 @@ with:
|
|||||||
```
|
```
|
||||||
In this case, your runner's credentials must have permissions to assume the role.
|
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
|
### 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.
|
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.
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ inputs:
|
|||||||
environment with the assumed role credentials rather than with the provided
|
environment with the assumed role credentials rather than with the provided
|
||||||
credentials
|
credentials
|
||||||
required: false
|
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:
|
role-duration-seconds:
|
||||||
description: "Role duration in seconds (default: 6 hours)"
|
description: "Role duration in seconds (default: 6 hours)"
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
const core = require('@actions/core');
|
const core = require('@actions/core');
|
||||||
const aws = require('aws-sdk');
|
const aws = require('aws-sdk');
|
||||||
const assert = require('assert');
|
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.
|
// 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.
|
// That seems like a reasonable default to use if no role duration is defined.
|
||||||
@@ -22,7 +24,8 @@ async function assumeRole(params) {
|
|||||||
roleDurationSeconds,
|
roleDurationSeconds,
|
||||||
roleSessionName,
|
roleSessionName,
|
||||||
region,
|
region,
|
||||||
roleSkipSessionTagging
|
roleSkipSessionTagging,
|
||||||
|
webIdentityTokenFile
|
||||||
} = params;
|
} = params;
|
||||||
assert(
|
assert(
|
||||||
[sourceAccountId, roleToAssume, roleDurationSeconds, roleSessionName, region].every(isDefined),
|
[sourceAccountId, roleToAssume, roleDurationSeconds, roleSessionName, region].every(isDefined),
|
||||||
@@ -42,6 +45,7 @@ async function assumeRole(params) {
|
|||||||
// Supports only 'aws' partition. Customers in other partitions ('aws-cn') will need to provide full ARN
|
// Supports only 'aws' partition. Customers in other partitions ('aws-cn') will need to provide full ARN
|
||||||
roleArn = `arn:aws:iam::${sourceAccountId}:role/${roleArn}`;
|
roleArn = `arn:aws:iam::${sourceAccountId}:role/${roleArn}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagArray = [
|
const tagArray = [
|
||||||
{Key: 'GitHub', Value: 'Actions'},
|
{Key: 'GitHub', Value: 'Actions'},
|
||||||
{Key: 'Repository', Value: GITHUB_REPOSITORY},
|
{Key: 'Repository', Value: GITHUB_REPOSITORY},
|
||||||
@@ -74,15 +78,38 @@ async function assumeRole(params) {
|
|||||||
assumeRoleRequest.ExternalId = roleExternalId;
|
assumeRoleRequest.ExternalId = roleExternalId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sts.assumeRole(assumeRoleRequest)
|
let assumeFunction = sts.assumeRole.bind(sts);
|
||||||
.promise()
|
|
||||||
.then(function (data) {
|
if(isDefined(webIdentityTokenFile)) {
|
||||||
return {
|
core.debug("webIdentityTokenFile provided. Will call sts:AssumeRoleWithWebIdentity and take session tags from token contents.")
|
||||||
accessKeyId: data.Credentials.AccessKeyId,
|
delete assumeRoleRequest.Tags;
|
||||||
secretAccessKey: data.Credentials.SecretAccessKey,
|
|
||||||
sessionToken: data.Credentials.SessionToken,
|
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) {
|
function sanitizeGithubActor(actor) {
|
||||||
@@ -211,6 +238,7 @@ async function run() {
|
|||||||
const roleSessionName = core.getInput('role-session-name', { required: false }) || ROLE_SESSION_NAME;
|
const roleSessionName = core.getInput('role-session-name', { required: false }) || ROLE_SESSION_NAME;
|
||||||
const roleSkipSessionTaggingInput = core.getInput('role-skip-session-tagging', { required: false })|| 'false';
|
const roleSkipSessionTaggingInput = core.getInput('role-skip-session-tagging', { required: false })|| 'false';
|
||||||
const roleSkipSessionTagging = roleSkipSessionTaggingInput.toLowerCase() === 'true';
|
const roleSkipSessionTagging = roleSkipSessionTaggingInput.toLowerCase() === 'true';
|
||||||
|
const webIdentityTokenFile = core.getInput('web-identity-token-file', { required: false })
|
||||||
|
|
||||||
if (!region.match(REGION_REGEX)) {
|
if (!region.match(REGION_REGEX)) {
|
||||||
throw new Error(`Region is not valid: ${region}`);
|
throw new Error(`Region is not valid: ${region}`);
|
||||||
@@ -249,7 +277,8 @@ async function run() {
|
|||||||
roleExternalId,
|
roleExternalId,
|
||||||
roleDurationSeconds,
|
roleDurationSeconds,
|
||||||
roleSessionName,
|
roleSessionName,
|
||||||
roleSkipSessionTagging
|
roleSkipSessionTagging,
|
||||||
|
webIdentityTokenFile
|
||||||
});
|
});
|
||||||
exportCredentials(roleCredentials);
|
exportCredentials(roleCredentials);
|
||||||
await validateCredentials(roleCredentials.accessKeyId);
|
await validateCredentials(roleCredentials.accessKeyId);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const ENVIRONMENT_VARIABLE_OVERRIDES = {
|
|||||||
GITHUB_ACTOR: 'MY-USERNAME[bot]',
|
GITHUB_ACTOR: 'MY-USERNAME[bot]',
|
||||||
GITHUB_SHA: 'MY-COMMIT-ID',
|
GITHUB_SHA: 'MY-COMMIT-ID',
|
||||||
GITHUB_REF: 'MY-BRANCH',
|
GITHUB_REF: 'MY-BRANCH',
|
||||||
|
GITHUB_WORKSPACE: '/home/github'
|
||||||
};
|
};
|
||||||
const GITHUB_ACTOR_SANITIZED = 'MY-USERNAME_bot_'
|
const GITHUB_ACTOR_SANITIZED = 'MY-USERNAME_bot_'
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ const ASSUME_ROLE_INPUTS = {...CREDS_INPUTS, 'role-to-assume': ROLE_ARN, 'aws-re
|
|||||||
|
|
||||||
const mockStsCallerIdentity = jest.fn();
|
const mockStsCallerIdentity = jest.fn();
|
||||||
const mockStsAssumeRole = jest.fn();
|
const mockStsAssumeRole = jest.fn();
|
||||||
|
const mockStsAssumeRoleWithWebIdentity = jest.fn();
|
||||||
|
|
||||||
jest.mock('aws-sdk', () => {
|
jest.mock('aws-sdk', () => {
|
||||||
return {
|
return {
|
||||||
@@ -55,10 +57,20 @@ jest.mock('aws-sdk', () => {
|
|||||||
STS: jest.fn(() => ({
|
STS: jest.fn(() => ({
|
||||||
getCallerIdentity: mockStsCallerIdentity,
|
getCallerIdentity: mockStsCallerIdentity,
|
||||||
assumeRole: mockStsAssumeRole,
|
assumeRole: mockStsAssumeRole,
|
||||||
|
assumeRoleWithWebIdentity: mockStsAssumeRoleWithWebIdentity
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock('fs', () => {
|
||||||
|
return {
|
||||||
|
promises: {
|
||||||
|
readFile: jest.fn(() => Promise.resolve('testpayload')),
|
||||||
|
},
|
||||||
|
existsSync: jest.fn(() => true)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('Configure AWS Credentials', () => {
|
describe('Configure AWS Credentials', () => {
|
||||||
const OLD_ENV = process.env;
|
const OLD_ENV = process.env;
|
||||||
|
|
||||||
@@ -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(() => {
|
afterEach(() => {
|
||||||
@@ -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 () => {
|
test('role external ID provided', async () => {
|
||||||
core.getInput = jest
|
core.getInput = jest
|
||||||
.fn()
|
.fn()
|
||||||
|
|||||||
Reference in New Issue
Block a user