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.
This commit is contained in:
parent
713aaabfec
commit
f85dec7edc
4 changed files with 90 additions and 4 deletions
|
|
@ -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.
|
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:
|
force-skip-oidc:
|
||||||
required: false
|
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:
|
action-timeout-s:
|
||||||
required: false
|
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.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ async function assumeRoleWithWebIdentityTokenFile(
|
||||||
try {
|
try {
|
||||||
const webIdentityToken = fs.readFileSync(webIdentityTokenFilePath, 'utf8');
|
const webIdentityToken = fs.readFileSync(webIdentityTokenFilePath, 'utf8');
|
||||||
delete params.Tags;
|
delete params.Tags;
|
||||||
|
delete params.TransitiveTagKeys;
|
||||||
const creds = await client.send(
|
const creds = await client.send(
|
||||||
new AssumeRoleWithWebIdentityCommand({
|
new AssumeRoleWithWebIdentityCommand({
|
||||||
...params,
|
...params,
|
||||||
|
|
|
||||||
14
src/index.ts
14
src/index.ts
|
|
@ -77,12 +77,22 @@ export async function run() {
|
||||||
}, globalTimeout * 1000);
|
}, 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(
|
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) {
|
if (specialCharacterWorkaround) {
|
||||||
// 😳
|
// 😳
|
||||||
disableRetry = false;
|
disableRetry = false;
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,18 @@ describe('Configure AWS Credentials', {}, () => {
|
||||||
expect(core.setOutput).toHaveBeenCalledTimes(2);
|
expect(core.setOutput).toHaveBeenCalledTimes(2);
|
||||||
expect(core.setFailed).not.toHaveBeenCalled();
|
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', {}, () => {
|
describe('Assume existing role', {}, () => {
|
||||||
|
|
@ -731,7 +743,70 @@ describe('Configure AWS Credentials', {}, () => {
|
||||||
|
|
||||||
await run();
|
await run();
|
||||||
expect(core.setFailed).toHaveBeenCalledWith(
|
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"),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue