From f85dec7edc19466dd8d98ace3a75610a3cd8acf5 Mon Sep 17 00:00:00 2001 From: Tom Keller Date: Thu, 14 May 2026 15:35:29 -0700 Subject: [PATCH 1/4] feat: container credentials provider support Closes #1546 Adds initial support for the container credentials provider. When used with the force-skip-oidc flag, allows ECS/CodeBuild to be used as a source. This was supported by the SDK but our pre run checks did not consider it a valid credential source. --- action.yml | 2 +- src/assumeRole.ts | 1 + src/index.ts | 14 +++++++-- test/index.test.ts | 77 +++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 90 insertions(+), 4 deletions(-) diff --git a/action.yml b/action.yml index 318c99d..fba8ae1 100644 --- a/action.yml +++ b/action.yml @@ -96,7 +96,7 @@ inputs: description: An option comma-delimited list of expected AWS account IDs. The action will fail if we receive credentials for the wrong account. force-skip-oidc: required: false - description: When enabled, this option will skip using GitHub OIDC provider even if the id-token permission is set. This is sometimes useful when using IAM instance credentials. + description: When enabled, this option will skip using GitHub OIDC provider even if the id-token permission is set. This is sometimes useful when using IAM instance credentials, or when running on a self-hosted runner with container-sourced credentials. 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. diff --git a/src/assumeRole.ts b/src/assumeRole.ts index 700532b..33975f2 100644 --- a/src/assumeRole.ts +++ b/src/assumeRole.ts @@ -43,6 +43,7 @@ async function assumeRoleWithWebIdentityTokenFile( try { const webIdentityToken = fs.readFileSync(webIdentityTokenFilePath, 'utf8'); delete params.Tags; + delete params.TransitiveTagKeys; const creds = await client.send( new AssumeRoleWithWebIdentityCommand({ ...params, diff --git a/src/index.ts b/src/index.ts index 2153a32..00b91ef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -77,12 +77,22 @@ export async function run() { }, globalTimeout * 1000); } - if (forceSkipOidc && roleToAssume && !AccessKeyId && !webIdentityTokenFile) { + // Container-sourced credentials are exposed by the AWS SDK default chain via these env vars. They count as a valid + // non-OIDC source for the force-skip-oidc guard. + const hasContainerCredentials = !!( + process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI + ); + + if (forceSkipOidc && roleToAssume && !AccessKeyId && !webIdentityTokenFile && !hasContainerCredentials) { throw new Error( - "If 'force-skip-oidc' is true and 'role-to-assume' is set, 'aws-access-key-id' or 'web-identity-token-file' must be set", + "If 'force-skip-oidc' is true and 'role-to-assume' is set, 'aws-access-key-id', 'web-identity-token-file', or container credentials must be available", ); } + if (forceSkipOidc && hasContainerCredentials && !AccessKeyId && !webIdentityTokenFile) { + core.info('Using container credentials from AWS_CONTAINER_CREDENTIALS_* environment variables'); + } + if (specialCharacterWorkaround) { // 😳 disableRetry = false; diff --git a/test/index.test.ts b/test/index.test.ts index 3970951..8ebe526 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -202,6 +202,18 @@ describe('Configure AWS Credentials', {}, () => { expect(core.setOutput).toHaveBeenCalledTimes(2); expect(core.setFailed).not.toHaveBeenCalled(); }); + it('does not send Tags or TransitiveTagKeys to AssumeRoleWithWebIdentity', async () => { + // AssumeRoleWithWebIdentity reads session tags from JWT claims, not the request. + // Both fields must be stripped before the STS call. + vi.mocked(core.getMultilineInput).mockImplementation((name: string) => { + if (name === 'transitive-tag-keys') return ['Repository']; + return []; + }); + await run(); + const callInput = mockedSTSClient.commandCalls(AssumeRoleWithWebIdentityCommand)[0].args[0].input; + expect(callInput.Tags).toBeUndefined(); + expect(callInput.TransitiveTagKeys).toBeUndefined(); + }); }); describe('Assume existing role', {}, () => { @@ -731,7 +743,70 @@ describe('Configure AWS Credentials', {}, () => { await run(); expect(core.setFailed).toHaveBeenCalledWith( - "If 'force-skip-oidc' is true and 'role-to-assume' is set, 'aws-access-key-id' or 'web-identity-token-file' must be set", + "If 'force-skip-oidc' is true and 'role-to-assume' is set, 'aws-access-key-id', 'web-identity-token-file', or container credentials must be available", + ); + }); + + it('uses container credentials with force-skip-oidc and role-to-assume (CodeBuild/ECS runner, #1546)', async () => { + vi.mocked(core.getInput).mockImplementation( + mocks.getInput({ + 'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE', + 'aws-region': 'fake-region-1', + 'force-skip-oidc': 'true', + }), + ); + vi.mocked(core.getIDToken).mockResolvedValue('testoidctoken'); + mockedSTSClient.on(AssumeRoleCommand).resolves(mocks.outputs.STS_CREDENTIALS); + mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); + // Simulate the container-metadata creds the SDK would pull from 169.254.170.2. + // biome-ignore lint/suspicious/noExplicitAny: any required to mock private method + vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials').mockResolvedValue({ + accessKeyId: 'CONTAINERAWSACCESSKEYID', + }); + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token'; + process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI = '/v2/credentials/abc-123'; + + await run(); + expect(core.getIDToken).not.toHaveBeenCalled(); + expect(core.info).toHaveBeenCalledWith( + 'Using container credentials from AWS_CONTAINER_CREDENTIALS_* environment variables', + ); + expect(core.info).toHaveBeenCalledWith('Assuming role with user credentials'); + expect(core.setFailed).not.toHaveBeenCalled(); + }); + + it('uses container credentials via AWS_CONTAINER_CREDENTIALS_FULL_URI', async () => { + vi.mocked(core.getInput).mockImplementation( + mocks.getInput({ + 'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE', + 'aws-region': 'fake-region-1', + 'force-skip-oidc': 'true', + }), + ); + mockedSTSClient.on(AssumeRoleCommand).resolves(mocks.outputs.STS_CREDENTIALS); + mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); + // biome-ignore lint/suspicious/noExplicitAny: any required to mock private method + vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials').mockResolvedValue({ + accessKeyId: 'CONTAINERAWSACCESSKEYID', + }); + process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI = 'http://169.254.170.23/credentials'; + + await run(); + expect(core.setFailed).not.toHaveBeenCalled(); + }); + + it('still errors when container env vars are absent and no other creds are provided', async () => { + vi.mocked(core.getInput).mockImplementation( + mocks.getInput({ + 'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE', + 'aws-region': 'fake-region-1', + 'force-skip-oidc': 'true', + }), + ); + // Neither AWS_CONTAINER_CREDENTIALS_RELATIVE_URI nor _FULL_URI is set (mocks.envs has neither). + await run(); + expect(core.setFailed).toHaveBeenCalledWith( + expect.stringContaining("'aws-access-key-id', 'web-identity-token-file', or container credentials"), ); }); From bdfbf63996135eee7cc6a4472645cd0c37895e9f Mon Sep 17 00:00:00 2001 From: Michael Lehmann Date: Wed, 27 May 2026 10:20:30 -0700 Subject: [PATCH 2/4] revert force-skip-oidc changes --- action.yml | 2 +- src/index.ts | 14 ++-------- test/index.test.ts | 65 +--------------------------------------------- 3 files changed, 4 insertions(+), 77 deletions(-) diff --git a/action.yml b/action.yml index fba8ae1..318c99d 100644 --- a/action.yml +++ b/action.yml @@ -96,7 +96,7 @@ inputs: description: An option comma-delimited list of expected AWS account IDs. The action will fail if we receive credentials for the wrong account. force-skip-oidc: required: false - description: When enabled, this option will skip using GitHub OIDC provider even if the id-token permission is set. This is sometimes useful when using IAM instance credentials, or when running on a self-hosted runner with container-sourced credentials. + description: When enabled, this option will skip using GitHub OIDC provider even if the id-token permission is set. This is sometimes useful when using IAM instance credentials. 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. diff --git a/src/index.ts b/src/index.ts index 00b91ef..2153a32 100644 --- a/src/index.ts +++ b/src/index.ts @@ -77,22 +77,12 @@ export async function run() { }, globalTimeout * 1000); } - // Container-sourced credentials are exposed by the AWS SDK default chain via these env vars. They count as a valid - // non-OIDC source for the force-skip-oidc guard. - const hasContainerCredentials = !!( - process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI - ); - - if (forceSkipOidc && roleToAssume && !AccessKeyId && !webIdentityTokenFile && !hasContainerCredentials) { + if (forceSkipOidc && roleToAssume && !AccessKeyId && !webIdentityTokenFile) { throw new Error( - "If 'force-skip-oidc' is true and 'role-to-assume' is set, 'aws-access-key-id', 'web-identity-token-file', or container credentials must be available", + "If 'force-skip-oidc' is true and 'role-to-assume' is set, 'aws-access-key-id' or 'web-identity-token-file' must be set", ); } - if (forceSkipOidc && hasContainerCredentials && !AccessKeyId && !webIdentityTokenFile) { - core.info('Using container credentials from AWS_CONTAINER_CREDENTIALS_* environment variables'); - } - if (specialCharacterWorkaround) { // 😳 disableRetry = false; diff --git a/test/index.test.ts b/test/index.test.ts index 8ebe526..93f6abe 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -743,70 +743,7 @@ describe('Configure AWS Credentials', {}, () => { await run(); expect(core.setFailed).toHaveBeenCalledWith( - "If 'force-skip-oidc' is true and 'role-to-assume' is set, 'aws-access-key-id', 'web-identity-token-file', or container credentials must be available", - ); - }); - - it('uses container credentials with force-skip-oidc and role-to-assume (CodeBuild/ECS runner, #1546)', async () => { - vi.mocked(core.getInput).mockImplementation( - mocks.getInput({ - 'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE', - 'aws-region': 'fake-region-1', - 'force-skip-oidc': 'true', - }), - ); - vi.mocked(core.getIDToken).mockResolvedValue('testoidctoken'); - mockedSTSClient.on(AssumeRoleCommand).resolves(mocks.outputs.STS_CREDENTIALS); - mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); - // Simulate the container-metadata creds the SDK would pull from 169.254.170.2. - // biome-ignore lint/suspicious/noExplicitAny: any required to mock private method - vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials').mockResolvedValue({ - accessKeyId: 'CONTAINERAWSACCESSKEYID', - }); - process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token'; - process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI = '/v2/credentials/abc-123'; - - await run(); - expect(core.getIDToken).not.toHaveBeenCalled(); - expect(core.info).toHaveBeenCalledWith( - 'Using container credentials from AWS_CONTAINER_CREDENTIALS_* environment variables', - ); - expect(core.info).toHaveBeenCalledWith('Assuming role with user credentials'); - expect(core.setFailed).not.toHaveBeenCalled(); - }); - - it('uses container credentials via AWS_CONTAINER_CREDENTIALS_FULL_URI', async () => { - vi.mocked(core.getInput).mockImplementation( - mocks.getInput({ - 'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE', - 'aws-region': 'fake-region-1', - 'force-skip-oidc': 'true', - }), - ); - mockedSTSClient.on(AssumeRoleCommand).resolves(mocks.outputs.STS_CREDENTIALS); - mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); - // biome-ignore lint/suspicious/noExplicitAny: any required to mock private method - vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials').mockResolvedValue({ - accessKeyId: 'CONTAINERAWSACCESSKEYID', - }); - process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI = 'http://169.254.170.23/credentials'; - - await run(); - expect(core.setFailed).not.toHaveBeenCalled(); - }); - - it('still errors when container env vars are absent and no other creds are provided', async () => { - vi.mocked(core.getInput).mockImplementation( - mocks.getInput({ - 'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE', - 'aws-region': 'fake-region-1', - 'force-skip-oidc': 'true', - }), - ); - // Neither AWS_CONTAINER_CREDENTIALS_RELATIVE_URI nor _FULL_URI is set (mocks.envs has neither). - await run(); - expect(core.setFailed).toHaveBeenCalledWith( - expect.stringContaining("'aws-access-key-id', 'web-identity-token-file', or container credentials"), + "If 'force-skip-oidc' is true and 'role-to-assume' is set, 'aws-access-key-id' or 'web-identity-token-file' must be set", ); }); From 4ad9baccc5222bef228f374fea47abf85bfbf093 Mon Sep 17 00:00:00 2001 From: Michael Lehmann Date: Wed, 27 May 2026 10:48:34 -0700 Subject: [PATCH 3/4] README changes for Container Credentials and adding missing input options --- README.md | 11 +++++++++++ action.yml | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index df38357..879f3b7 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,7 @@ detail. | role-session-name | Defaults to "GitHubActions", but may be changed if required. | No | | role-skip-session-tagging | Skips session tagging if set. | No | | transitive-tag-keys | Define a list of transitive tag keys to pass when assuming a role. | No | +| custom-tags | Additional tags to apply to the assumed role session. Must be a JSON object provided as a string. Custom tags are not usable with OIDC or web identity token authentication. | No | | inline-session-policy | You may further restrict the assumed role policy by defining an inline policy here. | No | | managed-session-policies | You may further restrict the assumed role policy by specifying a managed policy here. | No | | output-credentials | When set, outputs fetched credentials as action step output. (Outputs aws-access-key-id, aws-secret-access-key, aws-session-token, aws-account-id, authenticated-arn, and aws-expiration). Defaults to false. | No | @@ -180,6 +181,8 @@ detail. | allowed-account-ids | A comma-delimited list of expected AWS account IDs. The action will fail if we receive credentials for the wrong account. | No | | force-skip-oidc | When set, the action will skip using GitHub OIDC provider even if the id-token permission is set. | No | | action-timeout-s | Global timeout for the action in seconds. If set to a value greater than 0, the action will fail if it takes longer than this time to complete. | No | +| no-proxy | Hosts to skip for the proxy configuration. | No | +| sts-endpoint | 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. | No | @@ -617,6 +620,14 @@ 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/) +## Running in AWS Containers + +To run this action using self-hosted action runners on AWS Containers such as +Codebuild or EKS, you may need to set `role-chaining: true`. + +If you are using EKS and encountering an error related to the packed size of +session tags, set `role-skip-session-tagging: true`. + ## Compatibility with non-GitHub Actions environments This action has been sucessfully tested with diff --git a/action.yml b/action.yml index 318c99d..fbe5923 100644 --- a/action.yml +++ b/action.yml @@ -34,7 +34,7 @@ inputs: description: Use the web identity token file from the provided file system path in order to assume an IAM role using a web identity, e.g. from within an Amazon EKS worker node. required: false role-chaining: - description: Use existing credentials from the environment to assume a new role, rather than providing credentials as input. + description: Use existing credentials from the environment to assume a new role, rather than providing credentials as input. This is sometimes useful when running on a self-hosted runner with container-sourced credentials. required: false audience: description: The audience to use for the OIDC provider From cc3d630aff95bd22bd09ed6040365e56e79e13ac Mon Sep 17 00:00:00 2001 From: Michael Lehmann Date: Wed, 27 May 2026 11:02:26 -0700 Subject: [PATCH 4/4] linting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 879f3b7..71358ed 100644 --- a/README.md +++ b/README.md @@ -623,7 +623,7 @@ For further information on OIDC and GitHub Actions, please see: ## Running in AWS Containers To run this action using self-hosted action runners on AWS Containers such as -Codebuild or EKS, you may need to set `role-chaining: true`. +Codebuild or EKS, you may need to set `role-chaining: true`. If you are using EKS and encountering an error related to the packed size of session tags, set `role-skip-session-tagging: true`.