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

Add role-chaining support #688

Merged
merged 7 commits into from
May 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,18 @@ There are four different supported ways to retrieve credentials. 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.
`web-identity-token-file`, or setting `role-chaining`, will signal to the action that you wish to use the
OIDC provider. If `role-chaining` is `true`, existing credentials in the environment will be used to assume `role-to-assume`.

The following table describes which identity is used based on which values are supplied to the Action:

| **Identity Used** | `aws-access-key-id` | `role-to-assume` | `web-identity-token-file` |
| --------------------------------------------------------------- | ------------------- | ---------------- | ------------------------- |
| [✅ Recommended] Assume Role directly using GitHub OIDC provider | | ✔ | |
| IAM User | ✔ | | |
| Assume Role using IAM User credentials | ✔ | ✔ | |
| Assume Role using WebIdentity Token File credentials | | ✔ | ✔ |
| **Identity Used** | `aws-access-key-id` | `role-to-assume` | `web-identity-token-file` | `role-chaining` |
| --------------------------------------------------------------- | ------------------- | ---------------- | ------------------------- | - |
| [✅ Recommended] Assume Role directly using GitHub OIDC provider | | ✔ | | |
| IAM User | ✔ | | | |
| Assume Role using IAM User credentials | ✔ | ✔ | | |
| Assume Role using WebIdentity Token File credentials | | ✔ | ✔ | |
| Assume Role using existing credentials | | ✔ | | ✔ |

### Credential Lifetime
The default session duration is **1 hour** when using the OIDC provider to
Expand Down Expand Up @@ -148,6 +149,23 @@ In this example, the Action will load the OIDC token from the GitHub-provided en
```yaml
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-region: us-east-2
role-to-assume: arn:aws:iam::123456789100:role/my-github-actions-role
role-session-name: MySessionName
- name: Configure other AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-region: us-east-2
role-to-assume: arn:aws:iam::987654321000:role/my-second-role
role-session-name: MySessionName
role-chaining: true
```
In this two-step example, the first step will use OIDC to assume the role `arn:aws:iam::123456789100:role/my-github-actions-role` just as in the prior example. Following that, a second step will use this role to assume a different role, `arn:aws:iam::987654321000:role/my-second-role`.

```yaml
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ inputs:
http-proxy:
description: 'Proxy to use for the AWS SDK agent'
required: false
role-chaining:
description: 'Use existing credentials from the environment to assume a new role'
required: false
outputs:
aws-account-id:
description: 'The AWS account ID for the provided credentials'
Expand Down
20 changes: 13 additions & 7 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49238,7 +49238,7 @@ function loadCredentials() {
});
}

async function validateCredentials(expectedAccessKeyId) {
async function validateCredentials(expectedAccessKeyId, roleChaining) {
let credentials;
try {
credentials = await loadCredentials();
Expand All @@ -49250,10 +49250,12 @@ async function validateCredentials(expectedAccessKeyId) {
throw new Error(`Credentials could not be loaded, please check your action inputs: ${error.message}`);
}

const actualAccessKeyId = credentials.accessKeyId;
if (!roleChaining) {
const actualAccessKeyId = credentials.accessKeyId;

if (expectedAccessKeyId && expectedAccessKeyId != actualAccessKeyId) {
throw new Error('Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action');
if (expectedAccessKeyId && expectedAccessKeyId != actualAccessKeyId) {
throw new Error('Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action');
}
}
}

Expand Down Expand Up @@ -49319,11 +49321,14 @@ async function run() {
const maskAccountId = core.getInput('mask-aws-account-id', { required: false });
const roleToAssume = core.getInput('role-to-assume', {required: false});
const roleExternalId = core.getInput('role-external-id', { required: false });
const roleChainingInput = core.getInput('role-chaining', { required: false }) || 'false';
const roleChaining = roleChainingInput.toLowerCase() === 'true';
let roleDurationSeconds = core.getInput('role-duration-seconds', {required: false})
|| (sessionToken && SESSION_ROLE_DURATION)
|| (roleChaining && SESSION_ROLE_DURATION)
|| MAX_ACTION_RUNTIME;
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 webIdentityTokenFile = core.getInput('web-identity-token-file', { required: false });
const proxyServer = core.getInput('http-proxy', { required: false });
Expand All @@ -49341,7 +49346,8 @@ async function run() {
// environment variable and they won't be providing a web idenity token file or access key either.
// V2 of the action might relax this a bit and create an explicit precedence for these so that customers
// can provide as much info as they want and we will follow the established credential loading precedence.
return roleToAssume && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN && !accessKeyId && !webIdentityTokenFile

return roleToAssume && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN && !accessKeyId && !webIdentityTokenFile && !roleChaining
}

// Always export the source credentials and account ID.
Expand Down Expand Up @@ -49375,7 +49381,7 @@ async function run() {
// cases where this action is on a self-hosted runner that doesn't have credentials
// configured correctly, and cases where the user intended to provide input
// credentials but the secrets inputs resolved to empty strings.
await validateCredentials(accessKeyId);
await validateCredentials(accessKeyId, roleChaining);

sourceAccountId = await exportAccountId(maskAccountId, region);
}
Expand Down
20 changes: 13 additions & 7 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ function loadCredentials() {
});
}

async function validateCredentials(expectedAccessKeyId) {
async function validateCredentials(expectedAccessKeyId, roleChaining) {
let credentials;
try {
credentials = await loadCredentials();
Expand All @@ -223,10 +223,12 @@ async function validateCredentials(expectedAccessKeyId) {
throw new Error(`Credentials could not be loaded, please check your action inputs: ${error.message}`);
}

const actualAccessKeyId = credentials.accessKeyId;
if (!roleChaining) {
const actualAccessKeyId = credentials.accessKeyId;

if (expectedAccessKeyId && expectedAccessKeyId != actualAccessKeyId) {
throw new Error('Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action');
if (expectedAccessKeyId && expectedAccessKeyId != actualAccessKeyId) {
throw new Error('Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action');
}
}
}

Expand Down Expand Up @@ -292,11 +294,14 @@ async function run() {
const maskAccountId = core.getInput('mask-aws-account-id', { required: false });
const roleToAssume = core.getInput('role-to-assume', {required: false});
const roleExternalId = core.getInput('role-external-id', { required: false });
const roleChainingInput = core.getInput('role-chaining', { required: false }) || 'false';
const roleChaining = roleChainingInput.toLowerCase() === 'true';
let roleDurationSeconds = core.getInput('role-duration-seconds', {required: false})
|| (sessionToken && SESSION_ROLE_DURATION)
|| (roleChaining && SESSION_ROLE_DURATION)
|| MAX_ACTION_RUNTIME;
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 webIdentityTokenFile = core.getInput('web-identity-token-file', { required: false });
const proxyServer = core.getInput('http-proxy', { required: false });
Expand All @@ -314,7 +319,8 @@ async function run() {
// environment variable and they won't be providing a web idenity token file or access key either.
// V2 of the action might relax this a bit and create an explicit precedence for these so that customers
// can provide as much info as they want and we will follow the established credential loading precedence.
return roleToAssume && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN && !accessKeyId && !webIdentityTokenFile

return roleToAssume && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN && !accessKeyId && !webIdentityTokenFile && !roleChaining
}

// Always export the source credentials and account ID.
Expand Down Expand Up @@ -348,7 +354,7 @@ async function run() {
// cases where this action is on a self-hosted runner that doesn't have credentials
// configured correctly, and cases where the user intended to provide input
// credentials but the secrets inputs resolved to empty strings.
await validateCredentials(accessKeyId);
await validateCredentials(accessKeyId, roleChaining);

sourceAccountId = await exportAccountId(maskAccountId, region);
}
Expand Down