diff --git a/README.md b/README.md index b62845a51..ece0d9ddb 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 }} diff --git a/action.yml b/action.yml index a2e8312d2..ef82373b5 100644 --- a/action.yml +++ b/action.yml @@ -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' diff --git a/dist/index.js b/dist/index.js index 675f8e7ef..2e10286c0 100644 --- a/dist/index.js +++ b/dist/index.js @@ -49238,7 +49238,7 @@ function loadCredentials() { }); } -async function validateCredentials(expectedAccessKeyId) { +async function validateCredentials(expectedAccessKeyId, roleChaining) { let credentials; try { credentials = await loadCredentials(); @@ -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'); + } } } @@ -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 }); @@ -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. @@ -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); } diff --git a/index.js b/index.js index 06c546b16..418063980 100644 --- a/index.js +++ b/index.js @@ -211,7 +211,7 @@ function loadCredentials() { }); } -async function validateCredentials(expectedAccessKeyId) { +async function validateCredentials(expectedAccessKeyId, roleChaining) { let credentials; try { credentials = await loadCredentials(); @@ -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'); + } } } @@ -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 }); @@ -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. @@ -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); }