Add role-chaining support (#688)
* Add role-chaining support * fix version in readme * minor readme adjustment --------- Co-authored-by: Milo Hyson <mhyson@tunein.com> Co-authored-by: peterwoodworth <woodwoop@amazon.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
parent
19f3a6d67f
commit
6fbd316fd1
4 changed files with 55 additions and 22 deletions
34
README.md
34
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)
|
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
|
to get short-lived credentials needed for your actions. Specifying
|
||||||
`role-to-assume` **without** providing an `aws-access-key-id` or a
|
`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
|
`web-identity-token-file`, or setting `role-chaining`, will signal to the action that you wish to use the
|
||||||
OIDC provider.
|
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:
|
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` |
|
| **Identity Used** | `aws-access-key-id` | `role-to-assume` | `web-identity-token-file` | `role-chaining` |
|
||||||
| --------------------------------------------------------------- | ------------------- | ---------------- | ------------------------- |
|
| --------------------------------------------------------------- | ------------------- | ---------------- | ------------------------- | - |
|
||||||
| [✅ Recommended] Assume Role directly using GitHub OIDC provider | | ✔ | |
|
| [✅ Recommended] Assume Role directly using GitHub OIDC provider | | ✔ | | |
|
||||||
| IAM User | ✔ | | |
|
| IAM User | ✔ | | | |
|
||||||
| Assume Role using IAM User credentials | ✔ | ✔ | |
|
| Assume Role using IAM User credentials | ✔ | ✔ | | |
|
||||||
| Assume Role using WebIdentity Token File credentials | | ✔ | ✔ |
|
| Assume Role using WebIdentity Token File credentials | | ✔ | ✔ | |
|
||||||
|
| Assume Role using existing credentials | | ✔ | | ✔ |
|
||||||
|
|
||||||
### Credential Lifetime
|
### Credential Lifetime
|
||||||
The default session duration is **1 hour** when using the OIDC provider to
|
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
|
```yaml
|
||||||
- name: Configure AWS Credentials
|
- name: Configure AWS Credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v2
|
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:
|
with:
|
||||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,9 @@ inputs:
|
||||||
http-proxy:
|
http-proxy:
|
||||||
description: 'Proxy to use for the AWS SDK agent'
|
description: 'Proxy to use for the AWS SDK agent'
|
||||||
required: false
|
required: false
|
||||||
|
role-chaining:
|
||||||
|
description: 'Use existing credentials from the environment to assume a new role'
|
||||||
|
required: false
|
||||||
outputs:
|
outputs:
|
||||||
aws-account-id:
|
aws-account-id:
|
||||||
description: 'The AWS account ID for the provided credentials'
|
description: 'The AWS account ID for the provided credentials'
|
||||||
|
|
|
||||||
14
dist/index.js
vendored
14
dist/index.js
vendored
|
|
@ -49238,7 +49238,7 @@ function loadCredentials() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function validateCredentials(expectedAccessKeyId) {
|
async function validateCredentials(expectedAccessKeyId, roleChaining) {
|
||||||
let credentials;
|
let credentials;
|
||||||
try {
|
try {
|
||||||
credentials = await loadCredentials();
|
credentials = await loadCredentials();
|
||||||
|
|
@ -49250,11 +49250,13 @@ async function validateCredentials(expectedAccessKeyId) {
|
||||||
throw new Error(`Credentials could not be loaded, please check your action inputs: ${error.message}`);
|
throw new Error(`Credentials could not be loaded, please check your action inputs: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!roleChaining) {
|
||||||
const actualAccessKeyId = credentials.accessKeyId;
|
const actualAccessKeyId = credentials.accessKeyId;
|
||||||
|
|
||||||
if (expectedAccessKeyId && expectedAccessKeyId != actualAccessKeyId) {
|
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');
|
throw new Error('Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStsClient(region) {
|
function getStsClient(region) {
|
||||||
|
|
@ -49319,11 +49321,14 @@ async function run() {
|
||||||
const maskAccountId = core.getInput('mask-aws-account-id', { required: false });
|
const maskAccountId = core.getInput('mask-aws-account-id', { required: false });
|
||||||
const roleToAssume = core.getInput('role-to-assume', {required: false});
|
const roleToAssume = core.getInput('role-to-assume', {required: false});
|
||||||
const roleExternalId = core.getInput('role-external-id', { 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})
|
let roleDurationSeconds = core.getInput('role-duration-seconds', {required: false})
|
||||||
|| (sessionToken && SESSION_ROLE_DURATION)
|
|| (sessionToken && SESSION_ROLE_DURATION)
|
||||||
|
|| (roleChaining && SESSION_ROLE_DURATION)
|
||||||
|| MAX_ACTION_RUNTIME;
|
|| MAX_ACTION_RUNTIME;
|
||||||
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 });
|
const webIdentityTokenFile = core.getInput('web-identity-token-file', { required: false });
|
||||||
const proxyServer = core.getInput('http-proxy', { 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.
|
// 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
|
// 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.
|
// 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.
|
// 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
|
// 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
|
// configured correctly, and cases where the user intended to provide input
|
||||||
// credentials but the secrets inputs resolved to empty strings.
|
// credentials but the secrets inputs resolved to empty strings.
|
||||||
await validateCredentials(accessKeyId);
|
await validateCredentials(accessKeyId, roleChaining);
|
||||||
|
|
||||||
sourceAccountId = await exportAccountId(maskAccountId, region);
|
sourceAccountId = await exportAccountId(maskAccountId, region);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
index.js
14
index.js
|
|
@ -211,7 +211,7 @@ function loadCredentials() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function validateCredentials(expectedAccessKeyId) {
|
async function validateCredentials(expectedAccessKeyId, roleChaining) {
|
||||||
let credentials;
|
let credentials;
|
||||||
try {
|
try {
|
||||||
credentials = await loadCredentials();
|
credentials = await loadCredentials();
|
||||||
|
|
@ -223,11 +223,13 @@ async function validateCredentials(expectedAccessKeyId) {
|
||||||
throw new Error(`Credentials could not be loaded, please check your action inputs: ${error.message}`);
|
throw new Error(`Credentials could not be loaded, please check your action inputs: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!roleChaining) {
|
||||||
const actualAccessKeyId = credentials.accessKeyId;
|
const actualAccessKeyId = credentials.accessKeyId;
|
||||||
|
|
||||||
if (expectedAccessKeyId && expectedAccessKeyId != actualAccessKeyId) {
|
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');
|
throw new Error('Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStsClient(region) {
|
function getStsClient(region) {
|
||||||
|
|
@ -292,11 +294,14 @@ async function run() {
|
||||||
const maskAccountId = core.getInput('mask-aws-account-id', { required: false });
|
const maskAccountId = core.getInput('mask-aws-account-id', { required: false });
|
||||||
const roleToAssume = core.getInput('role-to-assume', {required: false});
|
const roleToAssume = core.getInput('role-to-assume', {required: false});
|
||||||
const roleExternalId = core.getInput('role-external-id', { 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})
|
let roleDurationSeconds = core.getInput('role-duration-seconds', {required: false})
|
||||||
|| (sessionToken && SESSION_ROLE_DURATION)
|
|| (sessionToken && SESSION_ROLE_DURATION)
|
||||||
|
|| (roleChaining && SESSION_ROLE_DURATION)
|
||||||
|| MAX_ACTION_RUNTIME;
|
|| MAX_ACTION_RUNTIME;
|
||||||
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 });
|
const webIdentityTokenFile = core.getInput('web-identity-token-file', { required: false });
|
||||||
const proxyServer = core.getInput('http-proxy', { 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.
|
// 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
|
// 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.
|
// 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.
|
// 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
|
// 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
|
// configured correctly, and cases where the user intended to provide input
|
||||||
// credentials but the secrets inputs resolved to empty strings.
|
// credentials but the secrets inputs resolved to empty strings.
|
||||||
await validateCredentials(accessKeyId);
|
await validateCredentials(accessKeyId, roleChaining);
|
||||||
|
|
||||||
sourceAccountId = await exportAccountId(maskAccountId, region);
|
sourceAccountId = await exportAccountId(maskAccountId, region);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue