1
0
Fork 0
mirror of synced 2026-06-05 12:48:19 +00:00

Compare commits

...

1 commit

Author SHA1 Message Date
Tom Keller
f440984505 chore: replay 6.2 devel changes onto main
* chore(deps-dev): bump vitest from 3.2.4 to 4.1.5 (#1748)

* chore(deps-dev): bump vitest from 3.2.4 to 4.1.5

Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.2.4 to 4.1.5.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.5/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 4.1.5
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps-dev): update @vitest/coverage-v8

Bump @vitest/coverage-v8 to ^4.1.5 to match peer dependency, and move
vi.mock('node:fs') calls to the top level of test files to reflect
actual hoisting semantics required by vitest 4.x.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Keller <kellertk@amazon.com>
(cherry picked from commit 78f374f6d1)

* chore(deps): bump @actions/core from 2.0.3 to 3.0.1 (#1746)

* chore(deps): bump @actions/core from 2.0.3 to 3.0.1

Bumps [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core) from 2.0.3 to 3.0.1.
- [Changelog](https://github.com/actions/toolkit/blob/main/packages/core/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/core)

---
updated-dependencies:
- dependency-name: "@actions/core"
  dependency-version: 3.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: update test mocks for @actions/core ESM

@actions/core v3 ships as an ESM module with non-configurable exports,
breaking vi.spyOn(). Switch to vi.mock('@actions/core') which
intercepts at the module loader level.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Keller <kellertk@amazon.com>
(cherry picked from commit 64d8e82527)

* chore(deps): bump @aws-sdk/client-sts from 3.1043.0 to 3.1044.0 (#1754)

Bumps [@aws-sdk/client-sts](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-sts) from 3.1043.0 to 3.1044.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-sts/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1044.0/clients/client-sts)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-sts"
  dependency-version: 3.1042.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 4cfda40a13)

* chore: bump unit test node version (#1758)

(cherry picked from commit 39d1702721)

* chore: automatic major version tagging (#1565)

* Update release-please.yml to auto-update version tag

* chore: configure release-please auto floating tag

---------

Co-authored-by: Tom Keller <kellertk@amazon.com>
(cherry picked from commit c36525a567)

* feat: Allow custom session tags to be passed when assuming a role (#1759)

* Add possibility to input custom session tags

* Use json for input to custom-tags, add documentation for custom-tags

* Add more examples

* Simplify example to avoid parse error

* Add input validation for custom tags

* Fix unit tests for custom-tags

* Add debugging message

* Skip failing test for now

* Build package

* Remove some unused validation for custom tags

* feat: add validation for custom session tags

Harden the custom-tags feature against misuse and
misconfiguration:

- Validate input is a JSON object (reject arrays, primitives, null)
- Enforce STS tag constraints: key length (128), value length (256),
  allowed characters
- Reject nested object/array values that would silently stringify to
  '[object Object]'
- Block overriding default session tags (GitHub, Repository,
  Workflow, etc.)
- Enforce 50-tag session limit
- Warn when custom-tags used with OIDC or web identity
- Fix missing await on helpers test assertion
- Remove unused CUSTOM_TAGS_JSON_INPUTS fixture
- Normalize test mocking to vi.mocked() pattern

---------

Co-authored-by: Sylvain Verly <sylvain.verly@gmail.com>
(cherry picked from commit 61f50f630f)

* chore: configure codeql to ignore generated code (#1760)

(cherry picked from commit dc2353e57a)

* feat: support custom STS endpoints (#1762)

Closes #1067. This is a advanced option
and is not needed for most deployments.

(cherry picked from commit 8d52d05d7a)

* chore: automate README version bumping (#1763)

Closes #1420.

(cherry picked from commit 07ada0fe07)

* feat: add more retry logic and better logging (#1764)

Wraps exportAccountId and validateCredentials calls in retryAndBackoff.
Closes #1681. Adds a label parameter to retryAndBackoff for better
info-level log messages.

(cherry picked from commit 540d0c13ae)

* feat: add regex validation to role-session-name (#1765)

Previously invalid role session names would get errors from the STS
API instead of this action rejecting them, causing unnecessary retries.
Now we check them and fail early.

Closes #1656. That FR recommended that we sanitize the name before
sending to STS, but instead we error to not silently change the user's
selected session name (avoiding the potential security sharp edge)

(cherry picked from commit e35449909c)

* chore: update documentation for environment workflows (#1766)

Closes #1238.

(cherry picked from commit 3f7e1b63d7)

* chore(deps): bump @aws-sdk/client-sts from 3.1044.0 to 3.1045.0 (#1767)

Bumps [@aws-sdk/client-sts](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-sts) from 3.1044.0 to 3.1045.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-sts/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1045.0/clients/client-sts)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-sts"
  dependency-version: 3.1045.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit a388f23f7d)

* chore(deps-dev): bump @vitest/coverage-v8 from 4.1.5 to 4.1.6 (#1768)

Bumps [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) from 4.1.5 to 4.1.6.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.6/packages/coverage-v8)

---
updated-dependencies:
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 4.1.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 1fb495c4b2)

* chore(deps-dev): bump @smithy/property-provider from 4.2.14 to 4.3.1 (#1771)

Bumps [@smithy/property-provider](https://github.com/smithy-lang/smithy-typescript/tree/HEAD/packages/property-provider) from 4.2.14 to 4.3.1.
- [Release notes](https://github.com/smithy-lang/smithy-typescript/releases)
- [Changelog](https://github.com/smithy-lang/smithy-typescript/blob/main/packages/property-provider/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-typescript/commits/@smithy/property-provider@4.3.1/packages/property-provider)

---
updated-dependencies:
- dependency-name: "@smithy/property-provider"
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 1ab31502aa)

* chore(deps): bump @smithy/node-http-handler from 4.6.1 to 4.7.1 (#1770)

Bumps [@smithy/node-http-handler](https://github.com/smithy-lang/smithy-typescript/tree/HEAD/packages/node-http-handler) from 4.6.1 to 4.7.1.
- [Release notes](https://github.com/smithy-lang/smithy-typescript/releases)
- [Changelog](https://github.com/smithy-lang/smithy-typescript/blob/main/packages/node-http-handler/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-typescript/commits/@smithy/node-http-handler@4.7.1/packages/node-http-handler)

---
updated-dependencies:
- dependency-name: "@smithy/node-http-handler"
  dependency-version: 4.7.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit dbd503f368)

* chore(deps-dev): bump @biomejs/biome from 2.4.14 to 2.4.15 (#1772)

Bumps [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) from 2.4.14 to 2.4.15.
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.4.15/packages/@biomejs/biome)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.4.15
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 7521c55910)

* chore(deps-dev): bump @types/node from 25.6.0 to 25.7.0 (#1773)

Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.6.0 to 25.7.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit ef734cca81)

* feat: expose run id in STS client user-agent (#1774)

* feat: expose run id in STS client user-agent

Closes #483.
This commit modifies the user-agent string so that it includes the
GITHUB_RUN_ID and the GITHUB_RUN_ATTEMPT, in the format typically used
by the SDK. User agent strings are logged to CloudTrail, allowing users
to correlate CloudTrail events with GHA runs. We took this approach
instead of logging the ACCESS_KEY_ID as suggested in the issue to avoid
logging sensitive information.

* feat: add github_action to ua string

(cherry picked from commit 29d1be3027)

* feat: add additional session tags by default (#1775)

Closes #390.
Note that 50 session tags are the AWS default, and this commit changes
our default set from 7 tags to 15 tags. This commit includes logic to
split the tags into "required" vs "overridable". Required tags are this
action's previous defaults and could never be overridden. Overridable
tags are the new set and can be overridden by custom-tags. The action
will not add tags if the addition plus the required plus the user's
custom tags exceed the AWS limit of 50 total tags. This ensures
backwards compat for the tag additions.

(cherry picked from commit e0ba768507)

* chore: document forgejo compatibility (#1776)

* chore: document forgejo compatibility

* chore: linting fixes

(cherry picked from commit f35a7d7d7e)

* fix: skip credential check on output-env-credentials: false (#1778)

Closes #1554.

(cherry picked from commit 58e7c47adf)

* chore: update README for additional claim support (#1779)

* chore: update README for additional claim support

* chore: lint fix (whitespace)

(cherry picked from commit 713aaabfec)

* chore(deps): bump @smithy/node-http-handler from 4.7.1 to 4.7.3 (#1781)

Bumps [@smithy/node-http-handler](https://github.com/smithy-lang/smithy-typescript/tree/HEAD/packages/node-http-handler) from 4.7.1 to 4.7.3.
- [Release notes](https://github.com/smithy-lang/smithy-typescript/releases)
- [Changelog](https://github.com/smithy-lang/smithy-typescript/blob/main/packages/node-http-handler/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-typescript/commits/@smithy/node-http-handler@4.7.3/packages/node-http-handler)

---
updated-dependencies:
- dependency-name: "@smithy/node-http-handler"
  dependency-version: 4.7.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit a7c33ae483)

* chore(deps-dev): bump @types/node from 25.7.0 to 25.9.0 (#1785)

Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.7.0 to 25.9.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit ffde832a1d)

* chore(deps-dev): bump @aws-sdk/credential-provider-env (#1784)

Bumps [@aws-sdk/credential-provider-env](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/credential-provider-env) from 3.972.34 to 3.972.38.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/credential-provider-env/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/credential-provider-env)

---
updated-dependencies:
- dependency-name: "@aws-sdk/credential-provider-env"
  dependency-version: 3.972.38
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit bc1093db1d)

* chore(deps-dev): bump @smithy/property-provider from 4.3.1 to 4.3.3 (#1783)

Bumps [@smithy/property-provider](https://github.com/smithy-lang/smithy-typescript/tree/HEAD/packages/property-provider) from 4.3.1 to 4.3.3.
- [Release notes](https://github.com/smithy-lang/smithy-typescript/releases)
- [Changelog](https://github.com/smithy-lang/smithy-typescript/blob/main/packages/property-provider/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-typescript/commits/@smithy/property-provider@4.3.3/packages/property-provider)

---
updated-dependencies:
- dependency-name: "@smithy/property-provider"
  dependency-version: 4.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit fe6ad3af19)

* chore(deps): bump @aws-sdk/client-sts from 3.1045.0 to 3.1049.0 (#1782)

Bumps [@aws-sdk/client-sts](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-sts) from 3.1045.0 to 3.1049.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-sts/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1049.0/clients/client-sts)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-sts"
  dependency-version: 3.1049.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 4684f47f89)

* chore: reconcile lockfile and test formatting

* chore: Update dist

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Michael Lehmann <lehmanmj@amazon.com>
Co-authored-by: Sylvain Verly <sylvain.verly@gmail.com>
2026-05-28 14:28:57 -07:00
21 changed files with 11250 additions and 15333 deletions

5
.github/codeql/codeql-config.yml vendored Normal file
View file

@ -0,0 +1,5 @@
name: "CodeQL config"
paths-ignore:
- dist
- node_modules

View file

@ -36,6 +36,7 @@
${{ secrets.OSDS_PACKAGING_ROLE }}
- name: Run release-please
id: release
uses: googleapis/release-please-action@v4
with:
release-type: node
@ -43,3 +44,33 @@
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
- name: Checkout Again
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Tag Major Version
if: ${{ steps.release.outputs.release_created }}
run: |
git config user.name "GitHub Actions"
git config user.email "github-aws-sdk-osds-automation@amazon.com"
if git rev-parse "v${{ steps.release.outputs.major }}" >/dev/null 2>&1; then
git tag -d "v${{ steps.release.outputs.major }}"
git push origin ":v${{ steps.release.outputs.major }}"
fi
git tag -a "v${{ steps.release.outputs.major }}" -m "Release v${{ steps.release.outputs.major }}"
git push origin "v${{ steps.release.outputs.major }}"
- name: Update README version references
if: ${{ steps.release.outputs.release_created }}
run: |
sed -i 's|configure-aws-credentials@v[0-9]*\.[0-9]*\.[0-9]*|configure-aws-credentials@${{ steps.release.outputs.tag_name }}|g' README.md
if git diff --quiet README.md; then
echo "README already up to date"
else
echo "::add-mask::${{ env.OSDS_ACCESS_TOKEN }}"
git remote set-url origin https://${{ env.OSDS_ACCESS_TOKEN }}@github.com/aws-actions/configure-aws-credentials.git
git add README.md
git commit -m "docs: update README version references to ${{ steps.release.outputs.tag_name }}"
git push --force origin
fi

View file

@ -18,9 +18,9 @@ jobs:
- name: "Checkout repository"
uses: actions/checkout@v5
- name: "Setup node"
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v6.4.0
with:
node-version: 20
node-version: 24
- name: "Install dependencies"
run: npm ci
- name: "Run tests"

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
.history
node_modules
coverage
.DS_Store

211
README.md
View file

@ -1,17 +1,16 @@
# Configure AWS Credentials
Authenticate to AWS in GitHub Actions! Works especially well with
Authenticate to AWS in GitHub Actions (and others)! Works especially well with
[AWS Secrets Manager][secretsmanager].
[secretsmanager]:
https://github.com/aws-actions/aws-secretsmanager-get-secrets
[secretsmanager]: https://github.com/aws-actions/aws-secretsmanager-get-secrets
## Quick Start (OIDC, recommended)
1. Create an IAM Identity Provider in your AWS account for GitHub OIDC. (See
[OIDC configuration](#oidc-configuration-details) below for details.)
2. Create an IAM Role in your AWS account with a trust policy that allows
GitHub Actions to assume it. (Expand the sections below) <details>
2. Create an IAM Role in your AWS account with a trust policy that allows GitHub
Actions to assume it. (Expand the sections below) <details>
<summary>GitHub OIDC Trust Policy</summary>
```json
@ -37,6 +36,12 @@ Authenticate to AWS in GitHub Actions! Works especially well with
</details>
Note: if you are running in a GitHub environment based workflow, the value
for the Sub claim will be different, in the form of
`repo:<GITHUB_ORG>/<GITHUB_REPOSITORY>:environment:<ENVIRONMENT_NAME>`.
Adjust the trust policy accordingly if you are using environment-based
workflows.
3. Attach permissions to the IAM Role that allow it to access the AWS resources
you need.
4. Add the following to your GitHub Actions workflow: <details>
@ -63,9 +68,9 @@ Authenticate to AWS in GitHub Actions! Works especially well with
</details>
That's it! Your GitHub Actions workflow can now access AWS resources using
the IAM Role you created. Other authentication scenarios are also supported
(see below).
That's it! Your GitHub Actions workflow can now access AWS resources using the
IAM Role you created. Other authentication scenarios are also supported (see
below).
## Security Recommendations
@ -81,8 +86,8 @@ Authenticate to AWS in GitHub Actions! Works especially well with
of the credentials used in workflows.
- Periodically rotate any long-lived credentials that you use.
- Store sensitive information in a secure way, such as using
[AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) or
[GitHub Secrets][gh-secrets].
[AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) or [GitHub
Secrets][gh-secrets].
- Be especially careful about running Actions in non-ephemeral environments, or
[triggering workflows on `pull_request_target`](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target)
events.
@ -105,11 +110,12 @@ by specifying different inputs.
5. Use credentials stored in the Action environment to fetch temporary
credentials via STS AssumeRole.
Because we use the AWS JavaScript SDK, we always will use the
[credential resolution flow for Node.js][cred-resolution].
Because we use the AWS JavaScript SDK, we always will use the [credential
resolution flow for Node.js][cred-resolution].
[cred-resolution]:
https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html
Depending on your inputs, the action might override parts of this flow.
<details>
@ -131,8 +137,8 @@ enabling this option._
Additionally, **`aws-region`** is always required.
_Note: If you use GitHub Enterprise Server, you may need to adjust examples
here to match your environment._
_Note: If you use GitHub Enterprise Server, you may need to adjust examples here
to match your environment._
## Additional Options
@ -145,7 +151,7 @@ detail.
<summary>Options list and descriptions</summary>
| Option | Description | Required |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| aws-region | Which AWS region to use | Yes |
| aws-profile | Name of the AWS profile to configure. When provided, credentials are written to `~/.aws/credentials` and `~/.aws/config` files. This enables configuring multiple profiles in a single workflow. Name cannot contain whitespace, square brackets, or slashes. When set, credentials will not be exported as environment variables unless `output-env-credentials` is manually set to true. | No |
| overwrite-aws-profile | Overwrite the given AWS profile if it already exists. When set to false or not set, an error will be thrown if the profile already exists. | No |
@ -210,8 +216,8 @@ Profile names may not contain whitespace, square brackets, or forward or
backslashes.
Writing to a profile will prevent credentials being written to the environment
by default. Use `output-env-credentials: true` if you would like the
credentials to also be exported as environment variables.
by default. Use `output-env-credentials: true` if you would like the credentials
to also be exported as environment variables.
By default, the action will not overwrite existing profiles. If you would like
to overwrite a profile, set the `overwrite-aws-profile` input to `true`.
@ -226,8 +232,8 @@ extreme care to ensure that this is safe in your environment and you do not leak
valid credentials unintentionally. Writing to configuration files is intended
for unusual authentication scenarios._
For using profiles with static IAM User Credentials or when using one
role to assume another, role chaining is needed:
For using profiles with static IAM User Credentials or when using one role to
assume another, role chaining is needed:
<details>
@ -248,9 +254,9 @@ specify the profile name as an environment variable in the job step:
AWS_PROFILE: MyProfile1
```
If you are using one role to assume another while using profiles, the
subsequent steps must set `role-chaining: true` and specify the prior profile's
name as step environment variables:
If you are using one role to assume another while using profiles, the subsequent
steps must set `role-chaining: true` and specify the prior profile's name as
step environment variables:
```yaml
- name: Configure AWS credentials
@ -282,8 +288,8 @@ from the environment. To skip this step, set the `AWS_SKIP_CLEANUP_STEP`
environment variable to `true`:
```yaml
env:
AWS_SKIP_CLEANUP_STEP: 'true'
env:
AWS_SKIP_CLEANUP_STEP: "true"
```
#### Use an HTTP proxy
@ -316,11 +322,12 @@ HTTP_PROXY="http://companydomain.com:3128"
#### Special characters in AWS_SECRET_ACCESS_KEY
Some edge cases are unable to properly parse an `AWS_SECRET_ACCESS_KEY` if it
contains special characters. For more information, please see the
[AWS CLI documentation][aws-cli-troubleshooting].
contains special characters. For more information, please see the [AWS CLI
documentation][aws-cli-troubleshooting].
[aws-cli-troubleshooting]:
https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-troubleshooting.html#tshoot-signature-does-not-match
If you set the `special-characters-workaround` option, this action will
continually retry fetching credentials until we get one that does not have
special characters. This option overrides the `disable-retry` and
@ -337,13 +344,15 @@ _Note: you might find it helpful to set the `role-session-name` to
`${{ github.run_id }}` so as to clarify in audit logs which AWS actions were
performed by which workflow run._
The session will be tagged with the following tags: (Refer to
[GitHub's documentation for `GITHUB_` environment variable
definitions][gh-env-vars])
The session will be tagged with the following tags: (Refer to [GitHub's
documentation for `GITHUB_` environment variable definitions][gh-env-vars])
[gh-env-vars]:
https://docs.github.com/en/actions/reference/workflows-and-actions/variables#default-environment-variables
**Protected tags** are always emitted when session tags are used, and cannot be
overridden via `custom-tags`:
| Key | Value |
| ---------- | ----------------- |
| GitHub | "Actions" |
@ -351,21 +360,43 @@ definitions][gh-env-vars])
| Workflow | GITHUB_WORKFLOW |
| Action | GITHUB_ACTION |
| Actor | GITHUB_ACTOR |
| Branch | GITHUB_REF |
| Commit | GITHUB_SHA |
| Branch | GITHUB_REF |
**Overrideable tags** are automatically added to the set of default session tags
but may be overridden via `custom-tags`. AWS has a maximum limit of 50 session
tags; tags from this list are dropped in reverse priority order if your
`custom-tags` set plus the protected set exceeds this limit.
| Key | Value | Priority |
| --------------- | ----------------------- | -------- |
| EventName | GITHUB_EVENT_NAME | 1 |
| BaseRef | GITHUB_BASE_REF | 2 |
| HeadRef | GITHUB_HEAD_REF | 3 |
| RefName | GITHUB_REF_NAME | 4 |
| RunId | GITHUB_RUN_ID | 5 |
| RefType | GITHUB_REF_TYPE | 6 |
| Job | GITHUB_JOB | 7 |
| TriggeringActor | GITHUB_TRIGGERING_ACTOR | 8 |
Tags whose source environment variable is unset are omitted (e.g., `BaseRef` and
`HeadRef` are only set on `pull_request` events).
_Note: all tag values must conform to
[the tag 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 characters, the characters
will be replaced with an '\*'._
[the tag requirements][sts-tag-requirements].
Values longer than 256 characters will be truncated, and characters outside the
allowed set will be replaced with an underscore (`_`).\_
[sts-tag-requirements]:
https://docs.aws.amazon.com/STS/latest/APIReference/API_Tag.html
The action will use session tagging by default unless you are using OIDC.
To [forward session tags to subsequent sessions in a role
chain][session-tag-chaining], you can use
[session-tag-chaining]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_role-chaining
[session-tag-chaining]:
https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_role-chaining
the `transitive-tag-keys` input to specify the keys of the tags to be passed.
@ -382,6 +413,23 @@ with:
Actor
```
### Custom session tags
You can add custom session tags using the `custom-tags` input, which accepts a
JSON object. Custom tags cannot override protected tags, but they can override
overrideable tags (in which case the overrideable tag's slot is freed for the
next overrideable tag in the priority list, if any).
```yaml
uses: aws-actions/configure-aws-credentials@v6
with:
custom-tags: '{"Environment": "Production", "Team": "Platform"}'
```
_Note: custom tags are not supported when using OIDC or web identity token
authentication. In those flows, session tags are controlled by the identity
provider's token claims._
### Session policies
Session policies are not required, but they allow you to limit the scope of the
@ -452,6 +500,12 @@ with:
</details>
### Custom STS endpoint
Use the `sts-endpoint` input to override the AWS STS endpoint URL. Most users
should not set this option and instead let the SDK derive the correct endpoint
from the specified region.
## OIDC Configuration Details
We recommend using
@ -516,41 +570,42 @@ aws iam create-open-id-connect-provider \
### Claims and scoping permissions
To align with the Amazon IAM best practice of
[granting least privilege][least-privilege],
To align with the Amazon IAM best practice of [granting least
privilege][least-privilege], the assume role policy document should contain a
[`Condition`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html)
that restricts which workflows can assume the role. Without any condition, any
GitHub user or repository could potentially assume the role.
GitHub provides a number of additional claims in the OIDC token that you can use
in your IAM policies to scope down permissions. Early versions of this action
only supported the `sub` and `aud` claims, but AWS IAM and GitHub have since
added support for `sub` claim customization and a variety of additional
claims ([1][gh-blog-oidc], [2][sub-claim-custom]).
> **Warning:** Avoid `ForAllValues:` in `Allow` statements. These operators
> return true when the claim is absent or misspelled, which can lead to
> uninended access. Instead, use `StringEquals` or `StringLike` operators to
> check for specific claim values.
[least-privilege]:
https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege
the assume role policy document should contain a
[`Condition`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html)
that specifies a subject (`sub`) allowed to assume the role.
[GitHub also recommends](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#defining-trust-conditions-on-cloud-roles-using-oidc-claims)
filtering for the correct audience (`aud`). See
[AWS IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#condition-keys-wif)
on which claims you can filter for in your trust policies.
[gh-blog-oidc]:
https://aws.amazon.com/about-aws/whats-new/2026/01/aws-sts-supports-validation-identity-provider-claims/
[sub-claim-custom]:
https://docs.github.com/en/rest/actions/oidc?apiVersion=2026-03-10
Without a subject (`sub`) condition, any GitHub user or repository could
potentially assume the role. The subject can be scoped to a GitHub organization
and repository as shown in the CloudFormation template. However, scoping it down
to your org and repo may cause the role assumption to fail in some cases. See
[Example subject claims](https://docs.github.com/en/actions/reference/security/oidc#example-subject-claims)
for specific details on what the subject value will be depending on your
workflow. You can also
[customize your subject claim](https://docs.github.com/en/actions/reference/security/oidc#customizing-the-token-claims)
if you want full control over the information you can filter for in your trust
policy. If you aren't sure what your subject (`sub`) key is, you can add the
#### Inspecting the token
If you aren't sure what claim values your workflow is producing, the
[`actions-oidc-debugger`](https://github.com/github/actions-oidc-debugger)
action to your workflow to see the value of the subject (`sub`) key, as well as
other claims.
action will print the decoded JWT payload. Run it in a private repository
only — the token itself is short-lived but the claim values may be sensitive.
Additional claim conditions can be added for higher specificity as explained in
the
[GitHub documentation][gh-oidc-hardening].
See the GitHub [security-hardening guide][gh-oidc-hardening] for further
discussion of trust conditions and threat modeling.
[gh-oidc-hardening]:
https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect
Due to implementation details, not every OIDC claim is presently supported by
IAM.
### Further information about OIDC
@ -562,6 +617,28 @@ For further information on OIDC and GitHub Actions, please see:
- [GitHub docs: Configuring OpenID Connect in Amazon Web Services](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services)
- [GitHub changelog: GitHub Actions: Secure cloud deployments with OpenID Connect](https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/)
## Compatibility with non-GitHub Actions environments
This action has been sucessfully tested with
Codeberg/[Forgejo Actions](https://forgejo.org/docs/next/user/actions/overview/)
and should be generally compatible with any CI/CD environment that sets the
correct `GITHUB_` environment variables. For use with Foregejo, please review
the
[runner differences with GitHub's action runners][forgejo-gh-differences].
[forgejo-gh-differences]:
https://forgejo.org/docs/next/user/actions/github-actions/#known-list-of-differences
The main difference to be aware of is that Forgejo uses the
`enable-openid-connect` flag to enable OIDC instad of GitHub's `id-token: write`
permission. Forgejo also uses a slightly different syntax for the workflow
definition file, omitting some subkeys.
For OIDC use, the issuer name for the IAM IdP for GitHub Actions is
`token.actions.githubusercontent.com`. For Forgejo Actions it is
`[foregejo instance url]/api/actions`. As an example, Codeberg would use
`codeberg.org/api/actions` as the issuer URL when configuring the IAM Identity
Provider. The audience would still be `sts.amazonaws.com` by default.
## Examples
### AssumeRoleWithWebIdentity
@ -659,6 +736,13 @@ This example shows that you can reference the fetched credentials as outputs if
the `aws-session-token` input in a situation where session tokens are fetched
and passed to this action.
If you only want the credentials available as _step outputs_ and not exported to
the environment (for example, on a self-hosted runner where you do not want the
assumed-role credentials to shadow an existing EC2 instance profile), pair
`output-credentials: true` with `output-env-credentials: false`. In that mode,
the action does not run its post-credential SDK-pickup validation step, since
the credentials were never written to the environment.
### Configure multiple AWS profiles in a single workflow
```yaml
@ -691,8 +775,8 @@ and passed to this action.
This example shows how to configure multiple named AWS profiles in a single
workflow. When using the `aws-profile` input, credentials are written to
`~/.aws/credentials` and `~/.aws/config` files, allowing you to reference
different profiles using the `--profile` flag with AWS CLI, SDKs, CDK, and
other tools.
different profiles using the `--profile` flag with AWS CLI, SDKs, CDK, and other
tools.
Each profile is independent and can authenticate to different AWS accounts or
use different roles. This is particularly useful for multi-account deployments
@ -705,6 +789,7 @@ Starting with version 5.0.0, this action uses semantic-style release tags and
[immutable-releases]:
https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases
A floating version tag (vN) is also provided for convenience: this tag will move
to the latest major version (vN -> vN.2.1, vM -> vM.0.0, etc.).

View file

@ -644,7 +644,7 @@ Apache License
The following npm package may be included in this product:
- @aws-sdk/client-sts@3.1053.0
- @aws-sdk/client-sts@3.1049.0
This package contains the following license:
@ -854,8 +854,8 @@ Apache License
The following npm packages may be included in this product:
- @aws-sdk/signature-v4-multi-region@3.996.28
- @smithy/core@3.24.4
- @aws-sdk/signature-v4-multi-region@3.996.27
- @smithy/core@3.24.5
- @smithy/types@4.14.2
These packages each contain the following license:
@ -1224,7 +1224,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The following npm package may be included in this product:
- @actions/http-client@3.0.2
- @actions/http-client@4.0.1
This package contains the following license:
@ -1254,7 +1254,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The following npm package may be included in this product:
- @aws-sdk/core@3.974.13
- @aws-sdk/core@3.974.15
This package contains the following license:
@ -1674,18 +1674,18 @@ Apache License
The following npm packages may be included in this product:
- @aws-sdk/credential-provider-env@3.972.39
- @aws-sdk/credential-provider-ini@3.972.43
- @aws-sdk/credential-provider-node@3.972.44
- @aws-sdk/token-providers@3.1052.0
- @aws-sdk/credential-provider-env@3.972.41
- @aws-sdk/credential-provider-ini@3.972.42
- @aws-sdk/credential-provider-node@3.972.43
- @aws-sdk/token-providers@3.1049.0
- @aws-sdk/types@3.973.9
- @aws-sdk/util-locate-window@3.965.5
- @aws-sdk/xml-builder@3.972.25
- @smithy/credential-provider-imds@4.3.4
- @smithy/fetch-http-handler@5.4.4
- @aws-sdk/xml-builder@3.972.26
- @smithy/credential-provider-imds@4.3.3
- @smithy/fetch-http-handler@5.4.3
- @smithy/is-array-buffer@2.2.0
- @smithy/node-http-handler@4.7.4
- @smithy/signature-v4@5.4.4
- @smithy/node-http-handler@4.7.3
- @smithy/signature-v4@5.4.5
- @smithy/util-buffer-from@2.2.0
- @smithy/util-utf8@2.3.0
@ -1897,9 +1897,9 @@ Apache License
The following npm packages may be included in this product:
- @aws-sdk/credential-provider-process@3.972.39
- @aws-sdk/credential-provider-sso@3.972.43
- @aws-sdk/credential-provider-web-identity@3.972.43
- @aws-sdk/credential-provider-process@3.972.38
- @aws-sdk/credential-provider-sso@3.972.42
- @aws-sdk/credential-provider-web-identity@3.972.42
These packages each contain the following license:
@ -2109,9 +2109,9 @@ Apache License
The following npm packages may be included in this product:
- @aws-sdk/credential-provider-http@3.972.41
- @aws-sdk/credential-provider-login@3.972.43
- @aws-sdk/nested-clients@3.997.11
- @aws-sdk/credential-provider-http@3.972.40
- @aws-sdk/credential-provider-login@3.972.42
- @aws-sdk/nested-clients@3.997.10
These packages each contain the following license:
@ -2335,8 +2335,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The following npm packages may be included in this product:
- @nodable/entities@2.1.0
- netmask@2.1.0
- @nodable/entities@2.1.1
- quickjs-wasi@2.2.0
- xml-naming@0.1.0
@ -2346,6 +2345,36 @@ MIT
-----------
The following npm package may be included in this product:
- netmask@2.1.1
This package contains the following license:
MIT License
Copyright (c) 2011 Olivier Poitrey rs@rhapsodyk.net
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-----------
The following npm package may be included in this product:
- fast-xml-parser@5.7.3
@ -2468,7 +2497,7 @@ SOFTWARE.
The following npm package may be included in this product:
- undici@6.24.0
- undici@6.25.0
This package contains the following license:
@ -2610,7 +2639,7 @@ THE SOFTWARE.
The following npm package may be included in this product:
- socks@2.8.7
- socks@2.8.8
This package contains the following license:
@ -2698,9 +2727,9 @@ SOFTWARE.
The following npm packages may be included in this product:
- @actions/core@2.0.2
- @actions/exec@2.0.0
- @actions/io@2.0.0
- @actions/core@3.0.1
- @actions/exec@3.0.0
- @actions/io@3.0.2
These packages each contain the following license:

View file

@ -1,4 +1,3 @@
name: '"Configure AWS Credentials" Action for GitHub Actions'
description: Configures AWS credentials for use in subsequent steps in a GitHub Action workflow
runs:
@ -101,6 +100,12 @@ inputs:
action-timeout-s:
required: false
description: A global timeout in seconds for the action. When the timeout is reached, the action immediately exits. The default is to run without a timeout.
custom-tags:
description: Additional tags to apply to the assumed role session. Must be a JSON object provided as a string.
required: false
sts-endpoint:
description: Custom STS endpoint URL. Use this to point to an STS-compatible API (e.g. MinIO, LocalStack) instead of the default AWS STS endpoint for the region.
required: false
outputs:
aws-account-id:

3256
dist/cleanup/index.js generated vendored

File diff suppressed because it is too large Load diff

9865
dist/index.js generated vendored

File diff suppressed because it is too large Load diff

3514
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,11 @@
{
"name": "configure-aws-credentials",
"description": "A GitHub Action to configure AWS credentials",
"version": "6.1.2",
"version": "6.1.1",
"scripts": {
"build": "tsc",
"lint": "biome check --error-on-warnings ./src && markdownlint -i node_modules -i CHANGELOG.md '**/*.md'",
"lint:fix": "biome check --write ./src && markdownlint -i node_modules -i CHANGELOG.md -f '**/*.md'",
"lint": "biome check --error-on-warnings ./src ./test && markdownlint -i node_modules -i CHANGELOG.md '**/*.md'",
"lint:fix": "biome check --write ./src ./test && markdownlint -i node_modules -i CHANGELOG.md -f '**/*.md'",
"package": "esbuild src/index.ts --bundle --platform=node --target=node24 --outfile=dist/index.js && esbuild src/cleanup/index.ts --bundle --platform=node --target=node24 --outfile=dist/cleanup/index.js && npm run license",
"test": "npm run lint && vitest run && npm run build",
"clean": "del-cli coverage test-reports node_modules",
@ -18,13 +18,13 @@
},
"devDependencies": {
"@aws-sdk/credential-provider-env": "^3.972.39",
"@biomejs/biome": "2.4.13",
"@biomejs/biome": "2.4.15",
"@smithy/property-provider": "^4.3.4",
"@types/node": "^25.9.1",
"@vitest/coverage-v8": "4.1.5",
"aws-sdk-client-mock": "^4.1.0",
"esbuild": "^0.28.0",
"generate-license-file": "^4.2.1",
"generate-license-file": "^4.1.1",
"json-schema": "^0.4.0",
"markdownlint-cli": "^0.48.0",
"memfs": "^4.57.2",
@ -33,9 +33,9 @@
"vitest": "4.1.5"
},
"dependencies": {
"@actions/core": "^2.0.2",
"@aws-sdk/client-sts": "^3.1053.0",
"@smithy/node-http-handler": "^4.6.1",
"@actions/core": "^3.0.1",
"@aws-sdk/client-sts": "^3.1049.0",
"@smithy/node-http-handler": "^4.7.3",
"proxy-agent": "^8.0.1"
},
"keywords": [

View file

@ -3,15 +3,18 @@ import { STSClient } from '@aws-sdk/client-sts';
import type { AwsCredentialIdentity } from '@aws-sdk/types';
import { NodeHttpHandler } from '@smithy/node-http-handler';
import { ProxyAgent } from 'proxy-agent';
import { errorMessage, getCallerIdentity } from './helpers';
import { buildCustomUserAgent, errorMessage, getCallerIdentity } from './helpers';
import { ProxyResolver } from './ProxyResolver';
const USER_AGENT = 'configure-aws-credentials-for-github-actions';
if (!process.env.AWS_EXECUTION_ENV) {
process.env.AWS_EXECUTION_ENV = 'GitHubActions';
}
export interface CredentialsClientProps {
region?: string;
proxyServer?: string;
noProxy?: string;
stsEndpoint?: string;
roleChaining: boolean;
}
@ -19,6 +22,7 @@ export class CredentialsClient {
public region?: string;
private _stsClient?: STSClient;
private readonly requestHandler?: NodeHttpHandler;
private readonly stsEndpoint?: string;
private roleChaining?: boolean;
constructor(props: CredentialsClientProps) {
@ -41,19 +45,20 @@ export class CredentialsClient {
httpAgent: handler,
});
}
if (props.stsEndpoint) {
this.stsEndpoint = props.stsEndpoint;
}
this.roleChaining = props.roleChaining;
}
public get stsClient(): STSClient {
if (!this._stsClient || this.roleChaining) {
const config = { customUserAgent: USER_AGENT } as {
customUserAgent: string;
region?: string;
requestHandler?: NodeHttpHandler;
};
if (this.region !== undefined) config.region = this.region;
if (this.requestHandler !== undefined) config.requestHandler = this.requestHandler;
this._stsClient = new STSClient(config);
this._stsClient = new STSClient({
customUserAgent: buildCustomUserAgent(),
...(this.region !== undefined && { region: this.region }),
...(this.stsEndpoint !== undefined && { endpoint: this.stsEndpoint }),
...(this.requestHandler !== undefined && { requestHandler: this.requestHandler }),
});
}
return this._stsClient;
}

View file

@ -77,6 +77,98 @@ export interface assumeRoleParams {
webIdentityToken?: string;
inlineSessionPolicy?: string;
managedSessionPolicies?: { arn: string }[];
customTags?: string;
}
const TAG_KEY_REGEX = /^[\p{L}\p{Z}\p{N}_.:/=+\-@]+$/u;
const TAG_VALUE_REGEX = /^[\p{L}\p{Z}\p{N}_.:/=+\-@]*$/u;
const MAX_TAG_KEY_LENGTH = 128;
const MAX_TAG_VALUE_LENGTH = 256;
const MAX_SESSION_TAGS = 50;
// Identity/audit primitives. Always emitted and cannot be overridden by custom-tags.
const PROTECTED_TAG_SOURCES: ReadonlyArray<{ key: string; envVar: string }> = [
{ key: 'Repository', envVar: 'GITHUB_REPOSITORY' },
{ key: 'Workflow', envVar: 'GITHUB_WORKFLOW' },
{ key: 'Action', envVar: 'GITHUB_ACTION' },
{ key: 'Actor', envVar: 'GITHUB_ACTOR' },
{ key: 'Commit', envVar: 'GITHUB_SHA' },
{ key: 'Branch', envVar: 'GITHUB_REF' },
];
// Convenience metadata. Custom-tags may override (suppresses the default for that key).
// Listed in priority order; lower-priority entries are dropped first if the user's custom-tags
// would push the total above MAX_SESSION_TAGS.
const OVERRIDEABLE_TAG_SOURCES_BY_PRIORITY: ReadonlyArray<{ key: string; envVar: string }> = [
{ key: 'EventName', envVar: 'GITHUB_EVENT_NAME' },
{ key: 'BaseRef', envVar: 'GITHUB_BASE_REF' },
{ key: 'HeadRef', envVar: 'GITHUB_HEAD_REF' },
{ key: 'RefName', envVar: 'GITHUB_REF_NAME' },
{ key: 'RunId', envVar: 'GITHUB_RUN_ID' },
{ key: 'RefType', envVar: 'GITHUB_REF_TYPE' },
{ key: 'Job', envVar: 'GITHUB_JOB' },
{ key: 'TriggeringActor', envVar: 'GITHUB_TRIGGERING_ACTOR' },
];
const PROTECTED_TAG_KEYS = new Set<string>(['GitHub', ...PROTECTED_TAG_SOURCES.map((s) => s.key)]);
export function parseAndValidateCustomTags(customTags: string, existingTags: Tag[]): Tag[] {
let parsed: unknown;
try {
parsed = JSON.parse(customTags);
} catch {
throw new Error('custom-tags: input is not valid JSON');
}
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
throw new Error('custom-tags: input must be a JSON object (not an array or primitive)');
}
const newTags: Tag[] = [];
for (const [key, value] of Object.entries(parsed)) {
if (typeof value === 'object') {
throw new Error(
`custom-tags: value for key '${key}' must be a string, number, or boolean (not an object or array)`,
);
}
const stringValue = String(value);
if (key.length === 0 || key.length > MAX_TAG_KEY_LENGTH) {
throw new Error(`custom-tags: key '${key}' must be between 1 and ${MAX_TAG_KEY_LENGTH} characters`);
}
if (stringValue.length > MAX_TAG_VALUE_LENGTH) {
throw new Error(
`custom-tags: value for key '${key}' exceeds maximum length of ${MAX_TAG_VALUE_LENGTH} characters`,
);
}
if (!TAG_KEY_REGEX.test(key)) {
throw new Error(
`custom-tags: key '${key}' contains invalid characters. Allowed: unicode letters, digits, spaces, and _.:/=+-@`,
);
}
if (stringValue.length > 0 && !TAG_VALUE_REGEX.test(stringValue)) {
throw new Error(
`custom-tags: value for key '${key}' contains invalid characters. Allowed: unicode letters, digits, spaces, and _.:/=+-@`,
);
}
if (PROTECTED_TAG_KEYS.has(key)) {
throw new Error(
`custom-tags: key '${key}' conflicts with a protected session tag set by this action and cannot be overridden`,
);
}
newTags.push({ Key: key, Value: stringValue });
}
if (existingTags.length + newTags.length > MAX_SESSION_TAGS) {
throw new Error(
`custom-tags: total session tags (${existingTags.length + newTags.length}) would exceed the AWS limit of ${MAX_SESSION_TAGS}`,
);
}
return newTags;
}
export async function assumeRole(params: assumeRoleParams) {
@ -93,6 +185,7 @@ export async function assumeRole(params: assumeRoleParams) {
webIdentityToken,
inlineSessionPolicy,
managedSessionPolicies,
customTags,
} = { ...params };
// Load GitHub environment variables
@ -101,26 +194,37 @@ export async function assumeRole(params: assumeRoleParams) {
throw new Error('Missing required environment variables. Are you running in GitHub Actions?');
}
// Load role session tags
const tagArray: Tag[] = [
{ Key: 'GitHub', Value: 'Actions' },
{ Key: 'Repository', Value: GITHUB_REPOSITORY },
{ Key: 'Workflow', Value: sanitizeGitHubVariables(GITHUB_WORKFLOW) },
{ Key: 'Action', Value: GITHUB_ACTION },
{ Key: 'Actor', Value: sanitizeGitHubVariables(GITHUB_ACTOR) },
{ Key: 'Commit', Value: GITHUB_SHA },
];
if (process.env.GITHUB_REF) {
tagArray.push({
Key: 'Branch',
Value: sanitizeGitHubVariables(process.env.GITHUB_REF),
});
// Build session tags. Values are sanitized because the AWS tag value spec is more
// restrictive than permissible characters in environment variables.
const protectedTags: Tag[] = [{ Key: 'GitHub', Value: 'Actions' }];
for (const { key, envVar } of PROTECTED_TAG_SOURCES) {
const value = process.env[envVar];
if (value) {
protectedTags.push({ Key: key, Value: sanitizeGitHubVariables(value) });
}
}
const parsedCustomTags: Tag[] = customTags ? parseAndValidateCustomTags(customTags, protectedTags) : [];
const customTagKeys = new Set(parsedCustomTags.map((t) => t.Key));
const availableOverrideableSlots = MAX_SESSION_TAGS - protectedTags.length - parsedCustomTags.length;
const overrideableTags: Tag[] = [];
for (const { key, envVar } of OVERRIDEABLE_TAG_SOURCES_BY_PRIORITY) {
if (overrideableTags.length >= availableOverrideableSlots) break;
if (customTagKeys.has(key)) continue;
const value = process.env[envVar];
if (value) {
overrideableTags.push({ Key: key, Value: sanitizeGitHubVariables(value) });
}
}
const tagArray: Tag[] = [...protectedTags, ...overrideableTags, ...parsedCustomTags];
const tags = roleSkipSessionTagging ? undefined : tagArray;
if (!tags) {
core.debug('Role session tagging has been skipped.');
} else {
core.debug(`${tags.length} role session tags are being used.`);
core.debug(`${tags.length} role session tags are being used:`);
}
//only populate transitiveTagKeys array if user is actually using session tagging

View file

@ -3,11 +3,32 @@ import * as path from 'node:path';
import * as core from '@actions/core';
import type { Credentials, STSClient } from '@aws-sdk/client-sts';
import { GetCallerIdentityCommand } from '@aws-sdk/client-sts';
import type { UserAgent } from '@smithy/types';
import type { CredentialsClient } from './CredentialsClient';
const MAX_TAG_VALUE_LENGTH = 256;
const SANITIZATION_CHARACTER = '_';
const SPECIAL_CHARS_REGEX = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+/;
const USER_AGENT_PREFIX = 'configure-aws-credentials-for-github-actions';
const UA_FIELDS: ReadonlyArray<{ env: string; label: string; pattern: RegExp }> = [
{ env: 'GITHUB_ACTION', label: 'action', pattern: /^[A-Za-z0-9_-]{1,128}$/ },
{ env: 'GITHUB_RUN_ID', label: 'run_id', pattern: /^[0-9]{1,20}$/ },
{ env: 'GITHUB_RUN_ATTEMPT', label: 'attempt', pattern: /^[0-9]{1,10}$/ },
];
export function buildCustomUserAgent(): UserAgent {
const tokens: UserAgent = [[USER_AGENT_PREFIX]];
for (const { env, label, pattern } of UA_FIELDS) {
const value = process.env[env];
if (value === undefined) continue;
if (pattern.test(value)) {
tokens.push(['md', `${label}#${value}`]);
} else {
core.warning(`${env} has unexpected format; omitting from User-Agent`);
}
}
return tokens;
}
export function translateEnvVariables() {
const envVars = [
@ -191,6 +212,7 @@ export async function retryAndBackoff<T>(
maxRetries = 12,
retries = 0,
base = 50,
label?: string,
): Promise<T> {
try {
return await fn();
@ -202,20 +224,21 @@ export async function retryAndBackoff<T>(
// It's retryable, so sleep and retry.
const delay = Math.random() * (2 ** retries * base);
const nextRetry = retries + 1;
const opName = label ? ` ${label}` : '';
core.debug(
`retryAndBackoff: attempt ${nextRetry} of ${maxRetries} failed: ${errorMessage(err)}. ` +
core.info(
`Retry${opName}: attempt ${nextRetry} of ${maxRetries} failed: ${errorMessage(err)}. ` +
`Retrying after ${Math.floor(delay)}ms.`,
);
await sleep(delay);
if (nextRetry >= maxRetries) {
core.debug('retryAndBackoff: reached max retries; giving up.');
core.info(`Retry${opName}: reached max retries (${maxRetries}); giving up.`);
throw err;
}
return await retryAndBackoff(fn, isRetryable, maxRetries, nextRetry, base);
return await retryAndBackoff(fn, isRetryable, maxRetries, nextRetry, base, label);
}
}

View file

@ -19,6 +19,7 @@ import { writeProfileFiles } from './profileManager';
const DEFAULT_ROLE_DURATION = 3600; // One hour (seconds)
const ROLE_SESSION_NAME = 'GitHubActions';
const REGION_REGEX = /^[a-z0-9-]+$/g;
const ROLE_SESSION_NAME_REGEX = /^[\w+=,.@-]*$/;
export async function run() {
try {
@ -43,6 +44,7 @@ export async function run() {
const roleSkipSessionTagging = getBooleanInput('role-skip-session-tagging', { required: false });
const transitiveTagKeys = core.getMultilineInput('transitive-tag-keys', { required: false });
const proxyServer = core.getInput('http-proxy', { required: false }) || process.env.HTTP_PROXY;
const customTags = core.getInput('custom-tags', { required: false });
const inlineSessionPolicy = core.getInput('inline-session-policy', { required: false });
const managedSessionPolicies = core.getMultilineInput('managed-session-policies', { required: false }).map((p) => {
return { arn: p };
@ -63,6 +65,7 @@ export async function run() {
.map((s) => s.trim());
const forceSkipOidc = getBooleanInput('force-skip-oidc', { required: false });
const noProxy = core.getInput('no-proxy', { required: false });
const stsEndpoint = core.getInput('sts-endpoint', { required: false });
const globalTimeout = Number.parseInt(core.getInput('action-timeout-s', { required: false })) || 0;
let timeoutId: NodeJS.Timeout | undefined;
@ -88,6 +91,9 @@ export async function run() {
maxRetries = 1;
}
const withRetry = <T>(fn: () => Promise<T>, label: string): Promise<T> =>
retryAndBackoff(fn, !disableRetry, maxRetries, 0, 50, label);
// Logic to decide whether to attempt to use OIDC or not
const useGitHubOIDCProvider = () => {
if (forceSkipOidc) return false;
@ -124,15 +130,33 @@ export async function run() {
throw new Error(`Region is not valid: ${region}`);
}
if (roleSessionName.length < 2 || roleSessionName.length > 64) {
throw new Error(
`Role session name must be between 2 and 64 characters, got ${roleSessionName.length}: '${roleSessionName}'`,
);
}
if (!roleSessionName.match(ROLE_SESSION_NAME_REGEX)) {
throw new Error(
`Role session name is not valid: '${roleSessionName}'. Must satisfy regular expression pattern: [\\w+=,.@-]*`,
);
}
exportRegion(region, outputEnvCredentials);
// Instantiate credentials client
const clientProps: { region: string; proxyServer?: string; noProxy?: string; roleChaining: boolean } = {
const clientProps: {
region: string;
proxyServer?: string;
noProxy?: string;
stsEndpoint?: string;
roleChaining: boolean;
} = {
region,
roleChaining,
};
if (proxyServer) clientProps.proxyServer = proxyServer;
if (noProxy) clientProps.noProxy = noProxy;
if (stsEndpoint) clientProps.stsEndpoint = stsEndpoint;
const credentialsClient = new CredentialsClient(clientProps);
let sourceAccountId: string;
let webIdentityToken: string;
@ -152,13 +176,9 @@ export async function run() {
// Else, export credentials provided as input
if (useGitHubOIDCProvider()) {
try {
webIdentityToken = await retryAndBackoff(
async () => {
webIdentityToken = await withRetry(async () => {
return core.getIDToken(audience);
},
!disableRetry,
maxRetries,
);
}, 'getIDToken');
} catch (error) {
throw new Error(`getIDToken call failed: ${errorMessage(error)}`);
}
@ -179,23 +199,40 @@ export async function run() {
}
} else if (!webIdentityTokenFile && !roleChaining) {
// Proceed only if credentials can be picked up
await credentialsClient.validateCredentials(undefined, roleChaining, expectedAccountIds);
sourceAccountId = await exportAccountId(credentialsClient, maskAccountId);
await withRetry(
() => credentialsClient.validateCredentials(undefined, roleChaining, expectedAccountIds),
'validateCredentials',
);
sourceAccountId = await withRetry(() => exportAccountId(credentialsClient, maskAccountId), 'exportAccountId');
}
if (AccessKeyId || roleChaining) {
// Validate that the SDK can actually pick up credentials.
// This validates cases where this action is using existing environment credentials,
// and cases where the user intended to provide input credentials but the secrets inputs resolved to empty strings.
await credentialsClient.validateCredentials(AccessKeyId, roleChaining, expectedAccountIds);
sourceAccountId = await exportAccountId(credentialsClient, maskAccountId);
// Skip when output-env-credentials is false: input IAM keys were not written to env, so
// the default chain would resolve to ambient runner credentials and the access-key check
// would spuriously fail (see #1554).
if (outputEnvCredentials) {
await withRetry(
() => credentialsClient.validateCredentials(AccessKeyId, roleChaining, expectedAccountIds),
'validateCredentials',
);
sourceAccountId = await withRetry(() => exportAccountId(credentialsClient, maskAccountId), 'exportAccountId');
}
}
if (customTags && (useGitHubOIDCProvider() || webIdentityTokenFile)) {
core.warning(
"'custom-tags' is set but will be ignored because session tags cannot be applied when using OIDC or web identity token authentication. " +
'Tags are controlled by the identity provider token claims in these authentication flows.',
);
}
// Get role credentials if configured to do so
if (roleToAssume) {
let roleCredentials: AssumeRoleCommandOutput;
do {
roleCredentials = await retryAndBackoff(
async () => {
roleCredentials = await withRetry(async () => {
return assumeRole({
credentialsClient,
sourceAccountId,
@ -209,29 +246,30 @@ export async function run() {
webIdentityToken,
inlineSessionPolicy,
managedSessionPolicies,
customTags,
});
},
!disableRetry,
maxRetries,
);
}, 'AssumeRole');
} while (specialCharacterWorkaround && !verifyKeys(roleCredentials.Credentials));
core.info(`Authenticated as assumedRoleId ${roleCredentials.AssumedRoleUser?.AssumedRoleId}`);
exportCredentials(roleCredentials.Credentials, outputCredentials, outputEnvCredentials);
// We need to validate the credentials in 2 of our use-cases
// First: self-hosted runners. If the GITHUB_ACTIONS environment variable
// is set to `true` then we are NOT in a self-hosted runner.
// Second: Customer provided credentials manually (IAM User keys stored in GH Secrets)
// If we are using a profile, don't validate credentials yet (since they most likely won't be in the environment).
// Wait until after creds are written to the profile file to try validation.
if ((!process.env.GITHUB_ACTIONS || AccessKeyId) && !awsProfile) {
await credentialsClient.validateCredentials(
// Validate that the SDK can pick up the assumed-role credentials from the environment.
// Skip when output-env-credentials is false: the credentials were never written to env,
// so the default credential provider chain would resolve to ambient runner credentials
// (e.g. an EC2 instance profile) and the access-key-id check would spuriously fail.
// Skip when using a profile: validation runs after the profile file is written below.
if ((!process.env.GITHUB_ACTIONS || AccessKeyId) && !awsProfile && outputEnvCredentials) {
await withRetry(
() =>
credentialsClient.validateCredentials(
roleCredentials.Credentials?.AccessKeyId,
roleChaining,
expectedAccountIds,
),
'validateCredentials',
);
}
if (outputEnvCredentials) {
await exportAccountId(credentialsClient, maskAccountId);
await withRetry(() => exportAccountId(credentialsClient, maskAccountId), 'exportAccountId');
}
// Write profile files if profile mode is enabled
@ -244,10 +282,14 @@ export async function run() {
// We then validate the credentials to make sure they work.
if (AccessKeyId || !process.env.GITHUB_ACTIONS) {
writeProfileFiles(awsProfile, roleCredentials.Credentials, region, true);
await credentialsClient.validateCredentials(
roleCredentials.Credentials.AccessKeyId,
await withRetry(
() =>
credentialsClient.validateCredentials(
roleCredentials.Credentials?.AccessKeyId,
roleChaining,
expectedAccountIds,
),
'validateCredentials',
);
} else {
writeProfileFiles(awsProfile, roleCredentials.Credentials, region, overwriteAwsProfile);

View file

@ -1,9 +1,5 @@
import * as core from '@actions/core';
import {
AssumeRoleWithWebIdentityCommand,
GetCallerIdentityCommand,
STSClient,
} from '@aws-sdk/client-sts';
import { AssumeRoleWithWebIdentityCommand, GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts';
import { mockClient } from 'aws-sdk-client-mock';
import { fs, vol } from 'memfs';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

View file

@ -5,20 +5,15 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup } from '../src/cleanup';
import mocks from './mockinputs.test';
vi.mock('@actions/core');
const mockedSTSClient = mockClient(STSClient);
describe('Configure AWS Credentials cleanup', {}, () => {
beforeEach(() => {
// Reset mock state
vi.restoreAllMocks();
vi.resetAllMocks();
mockedSTSClient.reset();
// Mock GitHub Actions core functions
vi.spyOn(core, 'exportVariable').mockImplementation((_n, _v) => {});
vi.spyOn(core, 'setSecret').mockImplementation((_s) => {});
vi.spyOn(core, 'setFailed').mockImplementation((_m) => {});
vi.spyOn(core, 'setOutput').mockImplementation((_n, _v) => {});
vi.spyOn(core, 'debug').mockImplementation((_m) => {});
vi.spyOn(core, 'info').mockImplementation((_m) => {});
vi.mocked(core.getInput).mockReturnValue('');
process.env = {
...mocks.envs,
AWS_ACCESS_KEY_ID: 'CLEANUPTEST',
@ -39,7 +34,7 @@ describe('Configure AWS Credentials cleanup', {}, () => {
expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', '');
});
it('also clears AWS_PROFILE when aws-profile was set', {}, () => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
vi.mocked(core.getInput).mockImplementation((name: string) => {
if (name === 'aws-profile') return 'my-profile';
if (name === 'output-env-credentials') return 'true';
return '';
@ -50,7 +45,7 @@ describe('Configure AWS Credentials cleanup', {}, () => {
expect(core.exportVariable).toHaveBeenCalledWith('AWS_PROFILE', '');
});
it('skips env cleanup when aws-profile is set without output-env-credentials', {}, () => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
vi.mocked(core.getInput).mockImplementation((name: string) => {
if (name === 'aws-profile') return 'my-profile';
return '';
});
@ -59,14 +54,14 @@ describe('Configure AWS Credentials cleanup', {}, () => {
expect(core.exportVariable).toHaveBeenCalledTimes(0);
});
it('handles errors', {}, () => {
vi.spyOn(core, 'exportVariable').mockImplementationOnce(() => {
vi.mocked(core.exportVariable).mockImplementationOnce(() => {
throw new Error('Test error');
});
cleanup();
expect(core.setFailed).toHaveBeenCalled();
});
it(`doesn't export credentials as empty env variables if asked not to`, {}, () => {
vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput(mocks.NO_ENV_CREDS_INPUTS));
vi.mocked(core.getInput).mockImplementation(mocks.getInput(mocks.NO_ENV_CREDS_INPUTS));
cleanup();
expect(core.exportVariable).toHaveBeenCalledTimes(0);
});

View file

@ -28,6 +28,29 @@ describe('Configure AWS Credentials helpers', {}, () => {
await expect(helpers.retryAndBackoff(fn, false)).rejects.toMatch('i am not retryable');
expect(fn).toHaveBeenCalledTimes(1);
});
it('retries and logs with label at info level', {}, async () => {
helpers.withsleep(() => Promise.resolve());
const fn = vi.fn().mockRejectedValueOnce(new Error('transient')).mockResolvedValueOnce('success');
const result = await helpers.retryAndBackoff(fn, true, 3, 0, 50, 'TestOp');
expect(result).toBe('success');
expect(fn).toHaveBeenCalledTimes(2);
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('Retry TestOp: attempt 1 of 3 failed'));
helpers.reset();
});
it('logs max retries reached with label', {}, async () => {
helpers.withsleep(() => Promise.resolve());
const fn = vi.fn().mockRejectedValue(new Error('persistent'));
await expect(helpers.retryAndBackoff(fn, true, 2, 0, 50, 'TestOp')).rejects.toThrow('persistent');
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('Retry TestOp: reached max retries (2)'));
helpers.reset();
});
it('retries without a label (backward compat)', {}, async () => {
helpers.withsleep(() => Promise.resolve());
const fn = vi.fn().mockRejectedValueOnce(new Error('transient')).mockResolvedValueOnce('ok');
await helpers.retryAndBackoff(fn, true, 3);
expect(core.info).toHaveBeenCalledWith(expect.stringContaining('Retry: attempt 1 of 3 failed'));
helpers.reset();
});
it('can output creds when told to', {}, () => {
vi.spyOn(core, 'setOutput').mockImplementation(() => {});
vi.spyOn(core, 'setSecret').mockImplementation(() => {});
@ -165,63 +188,40 @@ describe('Configure AWS Credentials helpers', {}, () => {
'/var/run/secrets/eks.amazonaws.com/serviceaccount/..2026_05_28_00_00_00.123/token',
'jwt-token',
);
fs.symlinkSync(
'..2026_05_28_00_00_00.123',
'/var/run/secrets/eks.amazonaws.com/serviceaccount/..data',
);
fs.symlinkSync(
'..data/token',
'/var/run/secrets/eks.amazonaws.com/serviceaccount/token',
);
fs.symlinkSync('..2026_05_28_00_00_00.123', '/var/run/secrets/eks.amazonaws.com/serviceaccount/..data');
fs.symlinkSync('..data/token', '/var/run/secrets/eks.amazonaws.com/serviceaccount/token');
expect(helpers.readFileUtf8('/var/run/secrets/eks.amazonaws.com/serviceaccount/token')).toBe('jwt-token');
},
);
it.skipIf(process.platform === 'win32')(
'still refuses symlinks at lookalike paths outside the allowlist',
() => {
it.skipIf(process.platform === 'win32')('still refuses symlinks at lookalike paths outside the allowlist', () => {
fs.mkdirSync('/var/run/secrets/eks.amazonaws.com/serviceaccount', { recursive: true });
fs.writeFileSync('/var/run/secrets/eks.amazonaws.com/serviceaccount/secret', 'jwt-token');
fs.symlinkSync(
'/var/run/secrets/eks.amazonaws.com/serviceaccount/secret',
'/var/run/secrets/eks.amazonaws.com/serviceaccount/token2',
);
expect(() =>
helpers.readFileUtf8('/var/run/secrets/eks.amazonaws.com/serviceaccount/token2'),
).toThrow(/Refusing .* \(.* symbolic link\)/);
},
expect(() => helpers.readFileUtf8('/var/run/secrets/eks.amazonaws.com/serviceaccount/token2')).toThrow(
/Refusing .* \(.* symbolic link\)/,
);
});
});
describe('isAllowListed', {}, () => {
it.skipIf(process.platform === 'win32')('matches the canonical kubelet projected-token path', () => {
expect(
helpers.isAllowListed('/var/run/secrets/eks.amazonaws.com/serviceaccount/token'),
).toBe(true);
expect(
helpers.isAllowListed('/var/run/secrets/kubernetes.io/serviceaccount/token'),
).toBe(true);
expect(helpers.isAllowListed('/var/run/secrets/eks.amazonaws.com/serviceaccount/token')).toBe(true);
expect(helpers.isAllowListed('/var/run/secrets/kubernetes.io/serviceaccount/token')).toBe(true);
});
it.skipIf(process.platform === 'win32')('rejects nested or unrelated paths', () => {
expect(helpers.isAllowListed('/var/run/secrets/serviceaccount/token')).toBe(false);
expect(
helpers.isAllowListed('/var/run/secrets/a/b/serviceaccount/token'),
).toBe(false);
expect(
helpers.isAllowListed('/var/run/secrets/eks.amazonaws.com/serviceaccount/token2'),
).toBe(false);
expect(
helpers.isAllowListed('/etc/var/run/secrets/foo/serviceaccount/token'),
).toBe(false);
expect(helpers.isAllowListed('/var/run/secrets/a/b/serviceaccount/token')).toBe(false);
expect(helpers.isAllowListed('/var/run/secrets/eks.amazonaws.com/serviceaccount/token2')).toBe(false);
expect(helpers.isAllowListed('/etc/var/run/secrets/foo/serviceaccount/token')).toBe(false);
});
it.skipIf(process.platform === 'win32')('normalizes path traversal attempts', () => {
expect(
helpers.isAllowListed(
'/var/run/secrets/foo/serviceaccount/../../../../etc/passwd',
),
).toBe(false);
expect(helpers.isAllowListed('/var/run/secrets/foo/serviceaccount/../../../../etc/passwd')).toBe(false);
});
});

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,46 @@ const inputs = {
'aws-region': 'fake-region-1',
'special-characters-workaround': 'true',
},
CUSTOM_TAGS_INVALID_JSON_INPUTS: {
'aws-access-key-id': 'MYAWSACCESSKEYID',
'aws-secret-access-key': 'MYAWSSECRETACCESSKEY',
'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE',
'aws-region': 'fake-region-1',
'retry-max-attempts': '1',
'custom-tags': 'not a json',
},
CUSTOM_TAGS_ARRAY_INPUTS: {
'aws-access-key-id': 'MYAWSACCESSKEYID',
'aws-secret-access-key': 'MYAWSSECRETACCESSKEY',
'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE',
'aws-region': 'fake-region-1',
'retry-max-attempts': '1',
'custom-tags': '[1, 2, 3]',
},
CUSTOM_TAGS_RESERVED_KEY_INPUTS: {
'aws-access-key-id': 'MYAWSACCESSKEYID',
'aws-secret-access-key': 'MYAWSSECRETACCESSKEY',
'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE',
'aws-region': 'fake-region-1',
'retry-max-attempts': '1',
'custom-tags': JSON.stringify({ Repository: 'evil-repo' }),
},
CUSTOM_TAGS_INVALID_KEY_CHARS_INPUTS: {
'aws-access-key-id': 'MYAWSACCESSKEYID',
'aws-secret-access-key': 'MYAWSSECRETACCESSKEY',
'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE',
'aws-region': 'fake-region-1',
'retry-max-attempts': '1',
'custom-tags': JSON.stringify({ 'invalid{key}': 'value' }),
},
CUSTOM_TAGS_OBJECT_INPUTS: {
'aws-access-key-id': 'MYAWSACCESSKEYID',
'aws-secret-access-key': 'MYAWSSECRETACCESSKEY',
'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE',
'aws-region': 'fake-region-1',
'retry-max-attempts': '1',
'custom-tags': JSON.stringify({ Environment: 'Production', Team: 'DevOps' }),
},
IAM_USER_INPUTS: {
'aws-access-key-id': 'MYAWSACCESSKEYID',
'aws-secret-access-key': 'MYAWSSECRETACCESSKEY',
@ -43,6 +83,14 @@ const inputs = {
'output-env-credentials': 'false',
'output-credentials': 'true',
},
IAM_ASSUMEROLE_NO_ENV_INPUTS: {
'aws-access-key-id': 'MYAWSACCESSKEYID',
'aws-secret-access-key': 'MYAWSSECRETACCESSKEY',
'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE',
'aws-region': 'fake-region-1',
'output-env-credentials': 'false',
'output-credentials': 'true',
},
};
const envs = {
@ -53,6 +101,15 @@ const envs = {
GITHUB_SHA: 'MY-COMMIT-ID',
GITHUB_WORKSPACE: '/home/github',
GITHUB_ACTIONS: 'true',
GITHUB_REF: 'refs/pull/42/merge',
GITHUB_EVENT_NAME: 'pull_request',
GITHUB_RUN_ID: '16412345678',
GITHUB_JOB: 'build',
GITHUB_REF_NAME: 'feature-branch',
GITHUB_REF_TYPE: 'branch',
GITHUB_BASE_REF: 'main',
GITHUB_HEAD_REF: 'feature-branch',
GITHUB_TRIGGERING_ACTOR: 'MY-USERNAME[bot]',
};
const outputs = {

View file

@ -96,7 +96,10 @@ describe('Profile Manager', {}, () => {
});
it('round-trips through parseIni', {}, () => {
const data = { dev: { aws_access_key_id: 'AKIA', aws_secret_access_key: 'secret' }, 'profile prod': { region: 'us-west-2' } };
const data = {
dev: { aws_access_key_id: 'AKIA', aws_secret_access_key: 'secret' },
'profile prod': { region: 'us-west-2' },
};
const roundTripped = parseIni(stringifyIni(data));
expect(roundTripped).toEqual(data);
});
@ -197,10 +200,15 @@ describe('Profile Manager', {}, () => {
const filePath = '/home/runner/.aws/credentials';
fs.mkdirSync('/home/runner/.aws', { recursive: true });
mergeProfileSection(filePath, 'dev', {
mergeProfileSection(
filePath,
'dev',
{
aws_access_key_id: 'AKIAIOSFODNN7EXAMPLE',
aws_secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
}, false);
},
false,
);
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parseIni(content);
@ -215,16 +223,26 @@ describe('Profile Manager', {}, () => {
fs.mkdirSync('/home/runner/.aws', { recursive: true });
// Create initial profile
mergeProfileSection(filePath, 'dev', {
mergeProfileSection(
filePath,
'dev',
{
aws_access_key_id: 'AKIAIOSFODNN7EXAMPLE',
aws_secret_access_key: 'devSecretKey',
}, false);
},
false,
);
// Add second profile
mergeProfileSection(filePath, 'prod', {
mergeProfileSection(
filePath,
'prod',
{
aws_access_key_id: 'AKIAPRODEXAMPLE',
aws_secret_access_key: 'prodSecretKey',
}, false);
},
false,
);
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parseIni(content);
@ -240,18 +258,28 @@ describe('Profile Manager', {}, () => {
fs.mkdirSync('/home/runner/.aws', { recursive: true });
// Create initial profile
mergeProfileSection(filePath, 'dev', {
mergeProfileSection(
filePath,
'dev',
{
aws_access_key_id: 'OLD_KEY',
aws_secret_access_key: 'oldSecretKey',
aws_session_token: 'oldSessionToken'
}, false);
aws_session_token: 'oldSessionToken',
},
false,
);
// Overwrite with new credentials
mergeProfileSection(filePath, 'dev', {
mergeProfileSection(
filePath,
'dev',
{
aws_access_key_id: 'NEW_KEY',
aws_secret_access_key: 'newSecretKey',
aws_session_token: 'newSessionToken',
}, true);
},
true,
);
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parseIni(content);
@ -266,17 +294,27 @@ describe('Profile Manager', {}, () => {
fs.mkdirSync('/home/runner/.aws', { recursive: true });
// Create profile with session token
mergeProfileSection(filePath, 'dev', {
mergeProfileSection(
filePath,
'dev',
{
aws_access_key_id: 'AKIA',
aws_secret_access_key: 'secret',
aws_session_token: 'old-token',
}, false);
},
false,
);
// Overwrite without session token
mergeProfileSection(filePath, 'dev', {
mergeProfileSection(
filePath,
'dev',
{
aws_access_key_id: 'AKIA2',
aws_secret_access_key: 'secret2',
}, true);
},
true,
);
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parseIni(content);
@ -291,10 +329,15 @@ describe('Profile Manager', {}, () => {
fs.mkdirSync('/home/runner/.aws', { recursive: true });
fs.writeFileSync(filePath, '', { mode: 0o600 });
mergeProfileSection(filePath, 'dev', {
mergeProfileSection(
filePath,
'dev',
{
aws_access_key_id: 'AKIA',
aws_secret_access_key: 'secret',
}, false);
},
false,
);
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parseIni(content);
@ -308,17 +351,31 @@ describe('Profile Manager', {}, () => {
fs.mkdirSync('/home/runner/.aws', { recursive: true });
// Create initial profile
mergeProfileSection(filePath, 'dev', {
mergeProfileSection(
filePath,
'dev',
{
aws_access_key_id: 'OLD_KEY',
aws_secret_access_key: 'oldSecretKey',
}, false);
},
false,
);
// Overwrite with new credentials
expect(() => mergeProfileSection(filePath, 'dev', {
expect(() =>
mergeProfileSection(
filePath,
'dev',
{
aws_access_key_id: 'NEW_KEY',
aws_secret_access_key: 'newSecretKey',
aws_session_token: 'sessionToken',
}, false)).toThrow(`Profile with name "dev" already exists. Please use the overwrite-aws-profile input if you want to overwrite existing profiles.`);
},
false,
),
).toThrow(
`Profile with name "dev" already exists. Please use the overwrite-aws-profile input if you want to overwrite existing profiles.`,
);
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parseIni(content);
@ -469,7 +526,7 @@ describe('Profile Manager', {}, () => {
SecretAccessKey: 'secret',
},
'us-east-1',
false
false,
),
).toThrow('whitespace');
});
@ -487,7 +544,7 @@ describe('Profile Manager', {}, () => {
SecretAccessKey: 'secret',
},
'us-east-1',
false
false,
);
expect(fs.existsSync('/custom/credentials')).toBe(true);
@ -502,7 +559,7 @@ describe('Profile Manager', {}, () => {
SecretAccessKey: 'secret',
},
'us-east-1',
false
false,
);
expect(core.info).toHaveBeenCalledWith('Writing credentials to profile: dev');
@ -518,12 +575,7 @@ describe('Profile Manager', {}, () => {
'[personal]\naws_access_key_id=AKIAPERSONAL\naws_secret_access_key=personalSecret\naws_session_token=personalToken\n',
);
writeProfileFiles(
'dev',
{ AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' },
'us-east-1',
false,
);
writeProfileFiles('dev', { AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' }, 'us-east-1', false);
const content = fs.readFileSync(credsPath, 'utf-8');
const parsed = parseIni(content);
@ -541,17 +593,9 @@ describe('Profile Manager', {}, () => {
it('preserves pre-existing config with extra keys', {}, () => {
const configPath = getProfileFilePaths().config;
fs.mkdirSync(require('node:path').dirname(configPath), { recursive: true });
fs.writeFileSync(
configPath,
'[profile personal]\nregion=eu-west-1\noutput=json\ncli_pager=\n',
);
fs.writeFileSync(configPath, '[profile personal]\nregion=eu-west-1\noutput=json\ncli_pager=\n');
writeProfileFiles(
'dev',
{ AccessKeyId: 'AKIA', SecretAccessKey: 'secret' },
'us-east-1',
false
);
writeProfileFiles('dev', { AccessKeyId: 'AKIA', SecretAccessKey: 'secret' }, 'us-east-1', false);
const content = fs.readFileSync(configPath, 'utf-8');
const parsed = parseIni(content);
@ -567,17 +611,9 @@ describe('Profile Manager', {}, () => {
it('preserves pre-existing default profile when writing a named profile', {}, () => {
const credsPath = getProfileFilePaths().credentials;
fs.mkdirSync(require('node:path').dirname(credsPath), { recursive: true });
fs.writeFileSync(
credsPath,
'[default]\naws_access_key_id=AKIADEFAULT\naws_secret_access_key=defaultSecret\n',
);
fs.writeFileSync(credsPath, '[default]\naws_access_key_id=AKIADEFAULT\naws_secret_access_key=defaultSecret\n');
writeProfileFiles(
'dev',
{ AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' },
'us-west-2',
false
);
writeProfileFiles('dev', { AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' }, 'us-west-2', false);
const content = fs.readFileSync(credsPath, 'utf-8');
const parsed = parseIni(content);
@ -597,12 +633,7 @@ describe('Profile Manager', {}, () => {
'# My important comment\n[personal]\naws_access_key_id=AKIA\naws_secret_access_key=secret\n',
);
writeProfileFiles(
'dev',
{ AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' },
'us-east-1',
false
);
writeProfileFiles('dev', { AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' }, 'us-east-1', false);
const content = fs.readFileSync(credsPath, 'utf-8') as string;
@ -636,12 +667,7 @@ describe('Profile Manager', {}, () => {
fs.mkdirSync('/custom-creds', { recursive: true });
writeProfileFiles(
'dev',
{ AccessKeyId: 'AKIA', SecretAccessKey: 'secret' },
'us-east-1',
false
);
writeProfileFiles('dev', { AccessKeyId: 'AKIA', SecretAccessKey: 'secret' }, 'us-east-1', false);
expect(fs.existsSync('/custom-creds/credentials')).toBe(true);
// Config file should be at the default path (under homedir)
@ -658,7 +684,7 @@ describe('Profile Manager', {}, () => {
SessionToken: 'FwoGZXIvYXdzEBYaDEXAMPLE',
},
'us-east-1',
false
false,
);
const credsPath = getProfileFilePaths().credentials;
@ -677,24 +703,16 @@ describe('Profile Manager', {}, () => {
'aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n' +
'aws_session_token = FwoGZXIvYXdzEBYaDEXAMPLE\n',
);
expect(configContent).toBe(
'[profile dev]\n' +
'region = us-east-1\n',
);
expect(configContent).toBe('[profile dev]\n' + 'region = us-east-1\n');
});
it('golden file for multi-profile output', {}, () => {
writeProfileFiles(
'dev',
{ AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' },
'us-east-1',
false
);
writeProfileFiles('dev', { AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' }, 'us-east-1', false);
writeProfileFiles(
'prod',
{ AccessKeyId: 'AKIAPROD', SecretAccessKey: 'prodSecret', SessionToken: 'prodToken' },
'us-west-2',
false
false,
);
const credsPath = getProfileFilePaths().credentials;
@ -714,11 +732,7 @@ describe('Profile Manager', {}, () => {
'aws_session_token = prodToken\n',
);
expect(configContent).toBe(
'[profile dev]\n' +
'region = us-east-1\n' +
'\n' +
'[profile prod]\n' +
'region = us-west-2\n',
'[profile dev]\n' + 'region = us-east-1\n' + '\n' + '[profile prod]\n' + 'region = us-west-2\n',
);
});
});