Skip to content

Commit

Permalink
feat: allow-list option for aws account id
Browse files Browse the repository at this point in the history
Users may set the `allowed-account-ids` input variable to define a
comma-separated list of accounts that may be authenticated against.

Resolves aws-actions#432
  • Loading branch information
jacksonwelsh committed Dec 18, 2022
1 parent 90d1b38 commit d0d9c0e
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 3 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ We recommend following [Amazon IAM best practices](https://docs.aws.amazon.com/I
* [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege) to the credentials used in GitHub Actions workflows. Grant only the permissions required to perform the actions in your GitHub Actions workflows.
* [Monitor the activity](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#keep-a-log) of the credentials used in GitHub Actions workflows.

To prevent accidental deploys to the incorrect environment, you may define a comma-separated list of allowed account IDs to configure credentials for:

```yaml
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
role-to-assume: arn:aws:iam::123456789100:role/my-github-actions-role
aws-region: us-east-2
allowed-account-ids: 123456789100
```

## Assuming a Role
We recommend using [GitHub's OIDC provider](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services) to get short-lived credentials needed for your actions.
Specifying `role-to-assume` **without** providing an `aws-access-key-id` or a `web-identity-token-file` will signal to the action that you wish to use the OIDC provider.
Expand Down
6 changes: 4 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ inputs:
role-skip-session-tagging:
description: 'Skip session tagging during role assumption'
required: false
http-proxy:
description: 'Proxy to use for the AWS SDK agent'
allowed-account-ids:
description: >-
Comma-separated list of allowed AWS account IDs to prevent accidental
deploys to the wrong environment.
required: false
outputs:
aws-account-id:
Expand Down
25 changes: 24 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const MAX_TAG_VALUE_LENGTH = 256;
const SANITIZATION_CHARACTER = '_';
const ROLE_SESSION_NAME = 'GitHubActions';
const REGION_REGEX = /^[a-z0-9-]+$/g;
const ACCOUNT_ID_LIST_REGEX = /^\d{12}(,\s?\d{12})*$/

async function assumeRole(params) {
// Assume a role to get short-lived credentials using longer-lived credentials.
Expand Down Expand Up @@ -300,13 +301,22 @@ async function run() {
const roleSkipSessionTagging = roleSkipSessionTaggingInput.toLowerCase() === 'true';
const webIdentityTokenFile = core.getInput('web-identity-token-file', { required: false });
const proxyServer = core.getInput('http-proxy', { required: false });
const allowedAccountIds = core.getInput('allowed-account-ids', { required: false });

if (!region.match(REGION_REGEX)) {
throw new Error(`Region is not valid: ${region}`);
}

exportRegion(region);

if (allowedAccountIds && !allowedAccountIds.match(ACCOUNT_ID_LIST_REGEX)) {
let errorMessage = "Allowed account ID list is not valid, must be comma-separated list of 12-digit IDs";
if (maskAccountId.toLowerCase() == 'false') {
errorMessage += `: ${allowedAccountIds}`;
}
throw new Error(errorMessage);
}

// This wraps the logic for deciding if we should rely on the GH OIDC provider since we may need to reference
// the decision in a few differennt places. Consolidating it here makes the logic clearer elsewhere.
const useGitHubOIDCProvider = () => {
Expand Down Expand Up @@ -375,7 +385,20 @@ async function run() {
if (!process.env.GITHUB_ACTIONS || accessKeyId) {
await validateCredentials(roleCredentials.accessKeyId);
}
await exportAccountId(maskAccountId, region);
sourceAccountId = await exportAccountId(maskAccountId, region);
}

// Check if configured account ID is in the provided allow-list
if (allowedAccountIds) {
// Convert string to a list and trim whitespace.
const accountIdList = allowedAccountIds.split(",").map((id) => id.trim());
if (!accountIdList.includes(sourceAccountId)) {
let errorMessage = "Account ID of the provided credentials is not in 'allowed-account-ids'";
if (maskAccountId.toLowerCase() == 'false') {
errorMessage += `: ${sourceAccountId}`;
}
throw new Error(errorMessage);
}
}
}
catch (error) {
Expand Down
31 changes: 31 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,37 @@ describe('Configure AWS Credentials', () => {

await run();
});

test('denies accounts not defined in allow-account-ids', async () => {
process.env.SHOW_STACK_TRACE = 'false';

core.getInput = jest
.fn()
.mockImplementation(mockGetInput({ ...DEFAULT_INPUTS, 'allowed-account-ids': '000000000000' }));

await run();
expect(core.setFailed).toHaveBeenCalledWith(`Account ID of the provided credentials is not in 'allowed-account-ids'`);
});

test('allows accounts defined in allow-account-ids', async () => {
core.getInput = jest
.fn()
.mockImplementation(mockGetInput({ ...DEFAULT_INPUTS, 'allowed-account-ids': `${FAKE_ACCOUNT_ID}, 000000000000` }));

await run();
});

test('throws if account id list is invalid', async () => {
process.env.SHOW_STACK_TRACE = 'false';

// account id is too short
core.getInput = jest
.fn()
.mockImplementation(mockGetInput({ ...DEFAULT_INPUTS, 'allowed-account-ids': '0' }));

await run();
expect(core.setFailed).toHaveBeenCalledWith('Allowed account ID list is not valid, must be comma-separated list of 12-digit IDs');
})

describe('proxy settings', () => {

Expand Down

0 comments on commit d0d9c0e

Please sign in to comment.