Skip to content

Commit a20ed60

Browse files
authored
feat: don't require access key credentials for self-hosted runners (#42)
1 parent ee66290 commit a20ed60

File tree

4 files changed

+97
-14
lines changed

4 files changed

+97
-14
lines changed

README.md

+23
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,29 @@ The session will have the name "GitHubActions" and be tagged with the following
130130

131131
_Note: all tag values must conform to [the requirements](https://docs.aws.amazon.com/STS/latest/APIReference/API_Tag.html). Particularly, `GITHUB_WORKFLOW` will be truncated if it's too long. If `GITHUB_ACTOR` or `GITHUB_WORKFLOW` contain invalid charcters, the characters will be replaced with an '*'._
132132

133+
## Self-hosted runners
134+
135+
If you run your GitHub Actions in a [self-hosted runner](https://help.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners) that already has access to AWS credentials, such as an EC2 instance, then you do not need to provide IAM user access key credentials to this action.
136+
137+
If no access key credentials are given in the action inputs, this action will use credentials from the runner environment using the [default methods for the AWS SDK for Javascript](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html).
138+
139+
You can use this action to simply configure the region and account ID in the environment, and then use the runner's credentials for all AWS API calls made by your Actions workflow:
140+
```yaml
141+
uses: aws-actions/configure-aws-credentials@v1
142+
with:
143+
aws-region: us-east-2
144+
```
145+
In this case, your runner's credentials must have permissions to call any AWS APIs called by your Actions workflow.
146+
147+
Or, you can use this action to assume a role, and then use the role credentials for all AWS API calls made by your Actions workflow:
148+
```yaml
149+
uses: aws-actions/configure-aws-credentials@v1
150+
with:
151+
aws-region: us-east-2
152+
role-to-assume: my-github-actions-role
153+
```
154+
In this case, your runner's credentials must have permissions to assume the role.
155+
133156
## License Summary
134157

135158
This code is made available under the MIT license.

action.yml

+10-4
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ branding:
55
color: 'orange'
66
inputs:
77
aws-access-key-id:
8-
description: 'AWS Access Key ID'
9-
required: true
8+
description: >-
9+
AWS Access Key ID. This input is required if running in the GitHub hosted environment.
10+
It is optional if running in a self-hosted environment that already has AWS credentials,
11+
for example on an EC2 instance.
12+
required: false
1013
aws-secret-access-key:
11-
description: 'AWS Secret Access Key'
12-
required: true
14+
description: >-
15+
AWS Secret Access Key. This input is required if running in the GitHub hosted environment.
16+
It is optional if running in a self-hosted environment that already has AWS credentials,
17+
for example on an EC2 instance.
18+
required: false
1319
aws-session-token:
1420
description: 'AWS Session Token'
1521
required: false

index.js

+12-4
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ function getStsClient(region) {
141141
async function run() {
142142
try {
143143
// Get inputs
144-
const accessKeyId = core.getInput('aws-access-key-id', { required: true });
145-
const secretAccessKey = core.getInput('aws-secret-access-key', { required: true });
144+
const accessKeyId = core.getInput('aws-access-key-id', { required: false });
145+
const secretAccessKey = core.getInput('aws-secret-access-key', { required: false });
146146
const region = core.getInput('aws-region', { required: true });
147147
const sessionToken = core.getInput('aws-session-token', { required: false });
148148
const maskAccountId = core.getInput('mask-aws-account-id', { required: false });
@@ -151,13 +151,21 @@ async function run() {
151151
const roleDurationSeconds = core.getInput('role-duration-seconds', {required: false}) || MAX_ACTION_RUNTIME;
152152
const roleSessionName = core.getInput('role-session-name', { required: false }) || ROLE_SESSION_NAME;
153153

154+
exportRegion(region);
155+
154156
// Always export the source credentials and account ID.
155157
// The STS client for calling AssumeRole pulls creds from the environment.
156158
// Plus, in the assume role case, if the AssumeRole call fails, we want
157159
// the source credentials and accound ID to already be masked as secrets
158160
// in any error messages.
159-
exportRegion(region);
160-
exportCredentials({accessKeyId, secretAccessKey, sessionToken});
161+
if (accessKeyId) {
162+
if (!secretAccessKey) {
163+
throw new Error("'aws-secret-access-key' must be provided if 'aws-access-key-id' is provided");
164+
}
165+
166+
exportCredentials({accessKeyId, secretAccessKey, sessionToken});
167+
}
168+
161169
const sourceAccountId = await exportAccountId(maskAccountId, region);
162170

163171
// Get role credentials if configured to do so

index.test.js

+52-6
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,17 @@ function mockGetInput(requestResponse) {
3232
return requestResponse[name]
3333
}
3434
}
35-
const REQUIRED_INPUTS = {
35+
const CREDS_INPUTS = {
3636
'aws-access-key-id': FAKE_ACCESS_KEY_ID,
3737
'aws-secret-access-key': FAKE_SECRET_ACCESS_KEY
3838
};
3939
const DEFAULT_INPUTS = {
40-
...REQUIRED_INPUTS,
40+
...CREDS_INPUTS,
4141
'aws-session-token': FAKE_SESSION_TOKEN,
4242
'aws-region': FAKE_REGION,
4343
'mask-aws-account-id': 'TRUE'
4444
};
45-
const ASSUME_ROLE_INPUTS = {...REQUIRED_INPUTS, 'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION};
45+
const ASSUME_ROLE_INPUTS = {...CREDS_INPUTS, 'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION};
4646

4747
const mockStsCallerIdentity = jest.fn();
4848
const mockStsAssumeRole = jest.fn();
@@ -118,8 +118,24 @@ describe('Configure AWS Credentials', () => {
118118
expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCOUNT_ID);
119119
});
120120

121+
test('hosted runners can pull creds from a self-hosted environment', async () => {
122+
const mockInputs = {'aws-region': FAKE_REGION};
123+
core.getInput = jest
124+
.fn()
125+
.mockImplementation(mockGetInput(mockInputs));
126+
127+
await run();
128+
expect(mockStsAssumeRole).toHaveBeenCalledTimes(0);
129+
expect(core.exportVariable).toHaveBeenCalledTimes(2);
130+
expect(core.setSecret).toHaveBeenCalledTimes(1);
131+
expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', FAKE_REGION);
132+
expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', FAKE_REGION);
133+
expect(core.setOutput).toHaveBeenCalledWith('aws-account-id', FAKE_ACCOUNT_ID);
134+
expect(core.setSecret).toHaveBeenCalledWith(FAKE_ACCOUNT_ID);
135+
});
136+
121137
test('session token is optional', async () => {
122-
const mockInputs = {...REQUIRED_INPUTS, 'aws-region': 'eu-west-1'};
138+
const mockInputs = {...CREDS_INPUTS, 'aws-region': 'eu-west-1'};
123139
core.getInput = jest
124140
.fn()
125141
.mockImplementation(mockGetInput(mockInputs));
@@ -139,7 +155,7 @@ describe('Configure AWS Credentials', () => {
139155
});
140156

141157
test('can opt out of masking account ID', async () => {
142-
const mockInputs = {...REQUIRED_INPUTS, 'aws-region': 'us-east-1', 'mask-aws-account-id': 'false'};
158+
const mockInputs = {...CREDS_INPUTS, 'aws-region': 'us-east-1', 'mask-aws-account-id': 'false'};
143159
core.getInput = jest
144160
.fn()
145161
.mockImplementation(mockGetInput(mockInputs));
@@ -218,6 +234,36 @@ describe('Configure AWS Credentials', () => {
218234
expect(core.setOutput).toHaveBeenNthCalledWith(2, 'aws-account-id', FAKE_ROLE_ACCOUNT_ID);
219235
});
220236

237+
test('assume role can pull source credentials from self-hosted environment', async () => {
238+
core.getInput = jest
239+
.fn()
240+
.mockImplementation(mockGetInput({'role-to-assume': ROLE_ARN, 'aws-region': FAKE_REGION}));
241+
242+
await run();
243+
expect(mockStsAssumeRole).toHaveBeenCalledTimes(1);
244+
expect(core.exportVariable).toHaveBeenCalledTimes(5);
245+
expect(core.setSecret).toHaveBeenCalledTimes(5);
246+
expect(core.setOutput).toHaveBeenCalledTimes(2);
247+
248+
// first the source account is exported and masked
249+
expect(core.setSecret).toHaveBeenNthCalledWith(1, FAKE_ACCOUNT_ID);
250+
expect(core.exportVariable).toHaveBeenNthCalledWith(1, 'AWS_DEFAULT_REGION', FAKE_REGION);
251+
expect(core.exportVariable).toHaveBeenNthCalledWith(2, 'AWS_REGION', FAKE_REGION);
252+
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'aws-account-id', FAKE_ACCOUNT_ID);
253+
254+
// then the role credentials are exported and masked
255+
expect(core.setSecret).toHaveBeenNthCalledWith(2, FAKE_STS_ACCESS_KEY_ID);
256+
expect(core.setSecret).toHaveBeenNthCalledWith(3, FAKE_STS_SECRET_ACCESS_KEY);
257+
expect(core.setSecret).toHaveBeenNthCalledWith(4, FAKE_STS_SESSION_TOKEN);
258+
expect(core.setSecret).toHaveBeenNthCalledWith(5, FAKE_ROLE_ACCOUNT_ID);
259+
260+
expect(core.exportVariable).toHaveBeenNthCalledWith(3, 'AWS_ACCESS_KEY_ID', FAKE_STS_ACCESS_KEY_ID);
261+
expect(core.exportVariable).toHaveBeenNthCalledWith(4, 'AWS_SECRET_ACCESS_KEY', FAKE_STS_SECRET_ACCESS_KEY);
262+
expect(core.exportVariable).toHaveBeenNthCalledWith(5, 'AWS_SESSION_TOKEN', FAKE_STS_SESSION_TOKEN);
263+
264+
expect(core.setOutput).toHaveBeenNthCalledWith(2, 'aws-account-id', FAKE_ROLE_ACCOUNT_ID);
265+
});
266+
221267
test('role assumption tags', async () => {
222268
core.getInput = jest
223269
.fn()
@@ -287,7 +333,7 @@ describe('Configure AWS Credentials', () => {
287333
test('role name provided instead of ARN', async () => {
288334
core.getInput = jest
289335
.fn()
290-
.mockImplementation(mockGetInput({...REQUIRED_INPUTS, 'role-to-assume': ROLE_NAME, 'aws-region': FAKE_REGION}));
336+
.mockImplementation(mockGetInput({...CREDS_INPUTS, 'role-to-assume': ROLE_NAME, 'aws-region': FAKE_REGION}));
291337

292338
await run();
293339
expect(mockStsAssumeRole).toHaveBeenCalledWith({

0 commit comments

Comments
 (0)