mirror of
https://github.com/aws-actions/configure-aws-credentials.git
synced 2026-09-02 05:55:10 +09:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b26f2851b7 |
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
".release-please-manifest.json": "4.0.2",
|
".release-please-manifest.json": "4.0.2",
|
||||||
"package.json": "6.0.0",
|
"package.json": "6.0.0",
|
||||||
".": "6.2.4"
|
".": "6.2.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,6 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
## [6.2.4](https://github.com/aws-actions/configure-aws-credentials/compare/v6.2.3...v6.2.4) (2026-08-31)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* account-ids handling, mask proxy as secret in logs ([#1943](https://github.com/aws-actions/configure-aws-credentials/issues/1943)) ([aa65264](https://github.com/aws-actions/configure-aws-credentials/commit/aa6526434b08748f8776b29964e3f1f5d90e7b63))
|
|
||||||
* skip backoff sleep after the final retryAndBackoff attempt ([#1937](https://github.com/aws-actions/configure-aws-credentials/issues/1937)) ([3852440](https://github.com/aws-actions/configure-aws-credentials/commit/3852440c21363386b7b790605685d08a7c1a4876))
|
|
||||||
|
|
||||||
## [6.2.3](https://github.com/aws-actions/configure-aws-credentials/compare/v6.2.2...v6.2.3) (2026-07-22)
|
## [6.2.3](https://github.com/aws-actions/configure-aws-credentials/compare/v6.2.2...v6.2.3) (2026-07-22)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+29
-52
@@ -44679,7 +44679,7 @@ var require_dist_cjs15 = __commonJS({
|
|||||||
throw lastProviderError;
|
throw lastProviderError;
|
||||||
};
|
};
|
||||||
var multipleCredentialSourceWarningEmitted = false;
|
var multipleCredentialSourceWarningEmitted = false;
|
||||||
var defaultProvider2 = (init = {}) => memoizeChain([
|
var defaultProvider = (init = {}) => memoizeChain([
|
||||||
async () => {
|
async () => {
|
||||||
const profile = init.profile ?? process.env[ENV_PROFILE2];
|
const profile = init.profile ?? process.env[ENV_PROFILE2];
|
||||||
if (profile) {
|
if (profile) {
|
||||||
@@ -44746,7 +44746,7 @@ var require_dist_cjs15 = __commonJS({
|
|||||||
var credentialsTreatedAsExpired = (credentials) => credentials?.expiration !== void 0 && credentials.expiration.getTime() - Date.now() < 3e5;
|
var credentialsTreatedAsExpired = (credentials) => credentials?.expiration !== void 0 && credentials.expiration.getTime() - Date.now() < 3e5;
|
||||||
exports2.credentialsTreatedAsExpired = credentialsTreatedAsExpired;
|
exports2.credentialsTreatedAsExpired = credentialsTreatedAsExpired;
|
||||||
exports2.credentialsWillNeedRefresh = credentialsWillNeedRefresh;
|
exports2.credentialsWillNeedRefresh = credentialsWillNeedRefresh;
|
||||||
exports2.defaultProvider = defaultProvider2;
|
exports2.defaultProvider = defaultProvider;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -44766,7 +44766,7 @@ var require_dist_cjs16 = __commonJS({
|
|||||||
var { TypeRegistry: TypeRegistry2, getSchemaSerdePlugin: getSchemaSerdePlugin2 } = (init_schema(), __toCommonJS(schema_exports));
|
var { TypeRegistry: TypeRegistry2, getSchemaSerdePlugin: getSchemaSerdePlugin2 } = (init_schema(), __toCommonJS(schema_exports));
|
||||||
var { resolveAwsSdkSigV4Config: resolveAwsSdkSigV4Config2, resolveAwsSdkSigV4AConfig: resolveAwsSdkSigV4AConfig2, AwsSdkSigV4Signer: AwsSdkSigV4Signer2, AwsSdkSigV4ASigner: AwsSdkSigV4ASigner2, NODE_SIGV4A_CONFIG_OPTIONS: NODE_SIGV4A_CONFIG_OPTIONS2, NODE_AUTH_SCHEME_PREFERENCE_OPTIONS: NODE_AUTH_SCHEME_PREFERENCE_OPTIONS2 } = (init_httpAuthSchemes2(), __toCommonJS(httpAuthSchemes_exports));
|
var { resolveAwsSdkSigV4Config: resolveAwsSdkSigV4Config2, resolveAwsSdkSigV4AConfig: resolveAwsSdkSigV4AConfig2, AwsSdkSigV4Signer: AwsSdkSigV4Signer2, AwsSdkSigV4ASigner: AwsSdkSigV4ASigner2, NODE_SIGV4A_CONFIG_OPTIONS: NODE_SIGV4A_CONFIG_OPTIONS2, NODE_AUTH_SCHEME_PREFERENCE_OPTIONS: NODE_AUTH_SCHEME_PREFERENCE_OPTIONS2 } = (init_httpAuthSchemes2(), __toCommonJS(httpAuthSchemes_exports));
|
||||||
var { SignatureV4MultiRegion: SignatureV4MultiRegion3 } = require_dist_cjs3();
|
var { SignatureV4MultiRegion: SignatureV4MultiRegion3 } = require_dist_cjs3();
|
||||||
var { defaultProvider: defaultProvider2 } = require_dist_cjs15();
|
var { defaultProvider } = require_dist_cjs15();
|
||||||
var { toUtf8: toUtf83, fromUtf8: fromUtf83, toBase64: toBase643, fromBase64: fromBase642, calculateBodyLength: calculateBodyLength2 } = (init_serde(), __toCommonJS(serde_exports));
|
var { toUtf8: toUtf83, fromUtf8: fromUtf83, toBase64: toBase643, fromBase64: fromBase642, calculateBodyLength: calculateBodyLength2 } = (init_serde(), __toCommonJS(serde_exports));
|
||||||
var { streamCollector: streamCollector7, NodeHttpHandler: NodeHttpHandler2 } = require_dist_cjs6();
|
var { streamCollector: streamCollector7, NodeHttpHandler: NodeHttpHandler2 } = require_dist_cjs6();
|
||||||
var { AwsQueryProtocol: AwsQueryProtocol2 } = (init_protocols2(), __toCommonJS(protocols_exports2));
|
var { AwsQueryProtocol: AwsQueryProtocol2 } = (init_protocols2(), __toCommonJS(protocols_exports2));
|
||||||
@@ -45882,12 +45882,12 @@ var require_dist_cjs16 = __commonJS({
|
|||||||
defaultsMode,
|
defaultsMode,
|
||||||
authSchemePreference: config?.authSchemePreference ?? loadConfig2(NODE_AUTH_SCHEME_PREFERENCE_OPTIONS2, loaderConfig),
|
authSchemePreference: config?.authSchemePreference ?? loadConfig2(NODE_AUTH_SCHEME_PREFERENCE_OPTIONS2, loaderConfig),
|
||||||
bodyLengthChecker: config?.bodyLengthChecker ?? calculateBodyLength2,
|
bodyLengthChecker: config?.bodyLengthChecker ?? calculateBodyLength2,
|
||||||
credentialDefaultProvider: config?.credentialDefaultProvider ?? defaultProvider2,
|
credentialDefaultProvider: config?.credentialDefaultProvider ?? defaultProvider,
|
||||||
defaultUserAgentProvider: config?.defaultUserAgentProvider ?? createDefaultUserAgentProvider2({ serviceId: clientSharedValues.serviceId, clientVersion: packageInfo.version }),
|
defaultUserAgentProvider: config?.defaultUserAgentProvider ?? createDefaultUserAgentProvider2({ serviceId: clientSharedValues.serviceId, clientVersion: packageInfo.version }),
|
||||||
httpAuthSchemes: config?.httpAuthSchemes ?? [
|
httpAuthSchemes: config?.httpAuthSchemes ?? [
|
||||||
{
|
{
|
||||||
schemeId: "aws.auth#sigv4",
|
schemeId: "aws.auth#sigv4",
|
||||||
identityProvider: (ipc) => ipc.getIdentityProvider("aws.auth#sigv4") || (async (idProps) => await defaultProvider2(idProps?.__config || {})()),
|
identityProvider: (ipc) => ipc.getIdentityProvider("aws.auth#sigv4") || (async (idProps) => await defaultProvider(idProps?.__config || {})()),
|
||||||
signer: new AwsSdkSigV4Signer2()
|
signer: new AwsSdkSigV4Signer2()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -74785,12 +74785,13 @@ function exportAccountId(identity, maskAccountId) {
|
|||||||
return accountId;
|
return accountId;
|
||||||
}
|
}
|
||||||
function validateAccountId(expectedAccountIds, account) {
|
function validateAccountId(expectedAccountIds, account) {
|
||||||
const allowedAccountIds = expectedAccountIds?.filter((id) => id !== "") ?? [];
|
if (!expectedAccountIds || expectedAccountIds.length === 0 || expectedAccountIds[0] === "") {
|
||||||
if (allowedAccountIds.length === 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!account || !allowedAccountIds.includes(account)) {
|
if (!account || !expectedAccountIds.includes(account)) {
|
||||||
throw new Error("The account ID of the provided credentials does not match any of the allowed account IDs");
|
throw new Error(
|
||||||
|
`The account ID of the provided credentials (${account ?? "unknown"}) does not match any of the expected account IDs: ${expectedAccountIds.join(", ")}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function toCredentialIdentity(creds) {
|
function toCredentialIdentity(creds) {
|
||||||
@@ -74803,24 +74804,6 @@ function toCredentialIdentity(creds) {
|
|||||||
...creds.SessionToken && { sessionToken: creds.SessionToken }
|
...creds.SessionToken && { sessionToken: creds.SessionToken }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function maskProxyCredentials(proxyServer) {
|
|
||||||
setSecret(proxyServer);
|
|
||||||
let url;
|
|
||||||
try {
|
|
||||||
url = new URL(proxyServer);
|
|
||||||
} catch (_) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const part of [url.username, url.password]) {
|
|
||||||
if (!part) continue;
|
|
||||||
setSecret(part);
|
|
||||||
try {
|
|
||||||
const decoded = decodeURIComponent(part);
|
|
||||||
if (decoded !== part) setSecret(decoded);
|
|
||||||
} catch (_) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function sanitizeGitHubVariables(name) {
|
function sanitizeGitHubVariables(name) {
|
||||||
const nameWithoutSpecialCharacters = name.replace(/[^\p{L}\p{Z}\p{N}_.:/=+\-@]/gu, SANITIZATION_CHARACTER);
|
const nameWithoutSpecialCharacters = name.replace(/[^\p{L}\p{Z}\p{N}_.:/=+\-@]/gu, SANITIZATION_CHARACTER);
|
||||||
const nameTruncated = nameWithoutSpecialCharacters.slice(0, MAX_TAG_VALUE_LENGTH);
|
const nameTruncated = nameWithoutSpecialCharacters.slice(0, MAX_TAG_VALUE_LENGTH);
|
||||||
@@ -74862,11 +74845,11 @@ async function retryAndBackoff(fn, isRetryable, maxRetries = 12, retries = 0, ba
|
|||||||
info(
|
info(
|
||||||
`Retry${opName}: attempt ${nextRetry} of ${maxRetries} failed: ${errorMessage(err)}. Retrying after ${Math.floor(delay)}ms.`
|
`Retry${opName}: attempt ${nextRetry} of ${maxRetries} failed: ${errorMessage(err)}. Retrying after ${Math.floor(delay)}ms.`
|
||||||
);
|
);
|
||||||
|
await sleep2(delay);
|
||||||
if (nextRetry >= maxRetries) {
|
if (nextRetry >= maxRetries) {
|
||||||
info(`Retry${opName}: reached max retries (${maxRetries}); giving up.`);
|
info(`Retry${opName}: reached max retries (${maxRetries}); giving up.`);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
await sleep2(delay);
|
|
||||||
return await retryAndBackoff(fn, isRetryable, maxRetries, nextRetry, base, label);
|
return await retryAndBackoff(fn, isRetryable, maxRetries, nextRetry, base, label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74876,6 +74859,18 @@ function errorMessage(error3) {
|
|||||||
function isDefined(i5) {
|
function isDefined(i5) {
|
||||||
return i5 !== void 0 && i5 !== null;
|
return i5 !== void 0 && i5 !== null;
|
||||||
}
|
}
|
||||||
|
async function areCredentialsValid(credentialsClient) {
|
||||||
|
const client = credentialsClient.stsClient;
|
||||||
|
try {
|
||||||
|
const identity = await client.send(new import_client_sts.GetCallerIdentityCommand({}));
|
||||||
|
if (identity.Account) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
function getBooleanInput(name, options) {
|
function getBooleanInput(name, options) {
|
||||||
const trueValue = ["true", "True", "TRUE"];
|
const trueValue = ["true", "True", "TRUE"];
|
||||||
const falseValue = ["false", "False", "FALSE"];
|
const falseValue = ["false", "False", "FALSE"];
|
||||||
@@ -75190,7 +75185,6 @@ async function assumeRole(params) {
|
|||||||
|
|
||||||
// src/CredentialsClient.ts
|
// src/CredentialsClient.ts
|
||||||
var import_client_sts3 = __toESM(require_dist_cjs16());
|
var import_client_sts3 = __toESM(require_dist_cjs16());
|
||||||
var import_credential_provider_node = __toESM(require_dist_cjs15());
|
|
||||||
var import_node_http_handler5 = __toESM(require_dist_cjs6());
|
var import_node_http_handler5 = __toESM(require_dist_cjs6());
|
||||||
|
|
||||||
// node_modules/proxy-agent/dist/index.js
|
// node_modules/proxy-agent/dist/index.js
|
||||||
@@ -76401,7 +76395,6 @@ var CredentialsClient = class {
|
|||||||
}
|
}
|
||||||
if (props.proxyServer) {
|
if (props.proxyServer) {
|
||||||
info("Configuring proxy handler for STS client");
|
info("Configuring proxy handler for STS client");
|
||||||
maskProxyCredentials(props.proxyServer);
|
|
||||||
const proxyOptions = {
|
const proxyOptions = {
|
||||||
httpProxy: props.proxyServer,
|
httpProxy: props.proxyServer,
|
||||||
httpsProxy: props.proxyServer
|
httpsProxy: props.proxyServer
|
||||||
@@ -76467,13 +76460,10 @@ var CredentialsClient = class {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async loadCredentials() {
|
async loadCredentials() {
|
||||||
return (0, import_credential_provider_node.defaultProvider)({
|
const config = {};
|
||||||
clientConfig: {
|
if (this.requestHandler !== void 0) config.requestHandler = this.requestHandler;
|
||||||
...this.region !== void 0 && { region: this.region },
|
const client = new import_client_sts3.STSClient(config);
|
||||||
...this.stsEndpoint !== void 0 && { endpoint: this.stsEndpoint },
|
return client.config.credentials();
|
||||||
...this.requestHandler !== void 0 && { requestHandler: this.requestHandler }
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76517,14 +76507,8 @@ function parseIni2(iniData) {
|
|||||||
function stringifyIni(data2) {
|
function stringifyIni(data2) {
|
||||||
const sections = [];
|
const sections = [];
|
||||||
for (const [sectionName, sectionData] of Object.entries(data2)) {
|
for (const [sectionName, sectionData] of Object.entries(data2)) {
|
||||||
if (/[\r\n]/.test(sectionName)) {
|
|
||||||
throw new Error("INI section names must not contain newline characters");
|
|
||||||
}
|
|
||||||
const lines = [`[${sectionName}]`];
|
const lines = [`[${sectionName}]`];
|
||||||
for (const [key, value] of Object.entries(sectionData)) {
|
for (const [key, value] of Object.entries(sectionData)) {
|
||||||
if (/[\r\n]/.test(key) || /[\r\n]/.test(value)) {
|
|
||||||
throw new Error("INI keys and values must not contain newline characters");
|
|
||||||
}
|
|
||||||
lines.push(`${key} = ${value}`);
|
lines.push(`${key} = ${value}`);
|
||||||
}
|
}
|
||||||
sections.push(lines.join("\n"));
|
sections.push(lines.join("\n"));
|
||||||
@@ -76705,15 +76689,8 @@ async function run() {
|
|||||||
let sourceAccountId;
|
let sourceAccountId;
|
||||||
let webIdentityToken;
|
let webIdentityToken;
|
||||||
if (useExistingCredentials) {
|
if (useExistingCredentials) {
|
||||||
const identity = await (async () => {
|
const validCredentials = await areCredentialsValid(credentialsClient);
|
||||||
try {
|
if (validCredentials) {
|
||||||
return await getCallerIdentity(credentialsClient.stsClient);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
if (identity) {
|
|
||||||
validateAccountId(expectedAccountIds, identity.Account);
|
|
||||||
notice("Pre-existing credentials are valid. No need to generate new ones.");
|
notice("Pre-existing credentials are valid. No need to generate new ones.");
|
||||||
if (timeoutId) clearTimeout(timeoutId);
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Examples
|
||||||
|
|
||||||
|
## [federated-setup](./federated-setup/README.md)
|
||||||
|
|
||||||
|
The directory contains templates for setting up the `configure-aws-credentials`
|
||||||
|
federation between your GitHub Organization/repository and your AWS account.
|
||||||
|
|
||||||
|
## [cfn-deploy-example](./cfn-deploy-example/README.md)
|
||||||
|
|
||||||
|
Repository example uses aws-action `configure-aws-credentials` with OIDC
|
||||||
|
federation template
|
||||||
|
[github-actions-oidc-federation-and-role](./github-actions-oidc-federation-and-role.yml).
|
||||||
|
Example demonstrates a repository that deploys AWS CloudFormation template using
|
||||||
|
cfn-deploy GitHub Action.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
name: 'compliance'
|
||||||
|
## run ci testing on all push events
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
## Guard rule set
|
||||||
|
sast-guard:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- uses: grolston/guard-action@main
|
||||||
|
with:
|
||||||
|
data_directory: './cloudformation/' ## change to your template directory
|
||||||
|
rule_set: 'FedRAMP-Moderate'
|
||||||
|
show_summary: 'all'
|
||||||
|
output_format: 'single-line-summary'
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: deploy
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
env:
|
||||||
|
AWS_DEFAULT_REGION: us-east-1
|
||||||
|
AWS_DEFAULT_OUTPUT: json
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-cfn:
|
||||||
|
name: deploy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# These permissions are needed to interact with GitHub’s OIDC Token endpoint.
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
- name: Configure AWS Credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
|
with:
|
||||||
|
aws-region: us-east-1
|
||||||
|
## the following creates an ARN based on the values entered into github secrets
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_DEPLOY_ROLE }}
|
||||||
|
role-session-name: myGitHubActions
|
||||||
|
- name: Deploy EC2 Bastion
|
||||||
|
uses: aws-actions/aws-cloudformation-github-deploy@v1.3.0
|
||||||
|
with:
|
||||||
|
name: myEC2bastion
|
||||||
|
## change to path to template in your github repo
|
||||||
|
template: cloudformation/ec2-bastion.yml
|
||||||
|
capabilities: CAPABILITY_IAM, CAPABILITY_NAMED_IAM
|
||||||
|
no-fail-on-empty-changeset: "1"
|
||||||
|
## parameter set in github secrets
|
||||||
|
parameter-overrides: "pVpc=${{ secrets.VPC_ID }},pSubnet=${{ secrets.SUBNET_ID }}"
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# cfn-deploy example
|
||||||
|
|
||||||
|
Example uses aws-action `configure-aws-credentials` with OIDC federation. Prior
|
||||||
|
to using this example project, the user needs to deploy the
|
||||||
|
[github-actions-oidc-federation-and-role](../federated-setup/github-actions-oidc-federation-and-role.yml)
|
||||||
|
template in the AWS account they want to deploy the CloudFormation template
|
||||||
|
into. Specify the GitHub Organization name, repository name, and the specific
|
||||||
|
branch you want to deploy on.
|
||||||
|
|
||||||
|
Within the [github/workflows](./.github/workflows/) directory there is a
|
||||||
|
[compliance.yml](./.github/workflows/compliance.yml) and a
|
||||||
|
[deploy.yml](./.github/workflows/deploy.yml). The deploy.yml file leverages the
|
||||||
|
aws-action `configure-aws-credentials` and accesses GitHub Action Secrets for
|
||||||
|
some of the variables. The compliance.yml runs static application security
|
||||||
|
testing using cfn-guard.
|
||||||
|
|
||||||
|
To use the example you will need to set the following GitHub Action Secrets:
|
||||||
|
|
||||||
|
| Secret Key | Used With | Description |
|
||||||
|
| --------------- | -------------------------------- | ---------------------------------------- |
|
||||||
|
| AWS_ACCOUNT_ID | configure-aws-credentials | The AWS account ID |
|
||||||
|
| AWS_DEPLOY_ROLE | configure-aws-credentials | The name of the IAM role |
|
||||||
|
| VPC_ID | aws-cloudformation-github-deploy | VPC ID the EC2 Bastion is deployed to |
|
||||||
|
| SUBNET_ID | aws-cloudformation-github-deploy | Subnet ID the EC2 Bastion is deployed to |
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
---
|
||||||
|
AWSTemplateFormatVersion: "2010-09-09"
|
||||||
|
Description: EC2 bastion for latest AWS Linux 2 EC2 deployment
|
||||||
|
Metadata:
|
||||||
|
AWS::CloudFormation::Interface:
|
||||||
|
ParameterGroups:
|
||||||
|
- Label:
|
||||||
|
default: "EC2 Configuration"
|
||||||
|
Parameters:
|
||||||
|
- pTagNameValue
|
||||||
|
- pOperatingSystem
|
||||||
|
- pInstanceType
|
||||||
|
- pVolumeSize
|
||||||
|
- pEbsDeleteOnTermination
|
||||||
|
- Label:
|
||||||
|
default: "Network Configuration"
|
||||||
|
Parameters:
|
||||||
|
- pVpc
|
||||||
|
- pSubnet
|
||||||
|
ParameterLabels:
|
||||||
|
pOperatingSystem:
|
||||||
|
default: "Operating System"
|
||||||
|
pInstanceType:
|
||||||
|
default: "Instance Type"
|
||||||
|
pTagNameValue:
|
||||||
|
default: "EC2 Name"
|
||||||
|
pVolumeSize:
|
||||||
|
default: "Volume Size"
|
||||||
|
pEbsDeleteOnTermination:
|
||||||
|
default: "Delete EBS Volume on Termination"
|
||||||
|
pSubnet:
|
||||||
|
default: "Subnet"
|
||||||
|
pVpc:
|
||||||
|
default: "VPC"
|
||||||
|
Parameters:
|
||||||
|
pSubnet:
|
||||||
|
Description: The subnet to launch the instance in to. It must be part of the VPC chosen above.
|
||||||
|
Type: AWS::EC2::Subnet::Id
|
||||||
|
pVpc:
|
||||||
|
Description: The VPC to launch the EC2 instance in to.
|
||||||
|
Type: AWS::EC2::VPC::Id
|
||||||
|
pOperatingSystem:
|
||||||
|
Type: "AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>"
|
||||||
|
Default: "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-ebs"
|
||||||
|
pInstanceType:
|
||||||
|
Description: Desired Instance Size
|
||||||
|
Type: String
|
||||||
|
Default: t3.small
|
||||||
|
AllowedValues:
|
||||||
|
- t3.small
|
||||||
|
- t3.medium
|
||||||
|
- t3.nano
|
||||||
|
pTagNameValue:
|
||||||
|
Description: "Required: Enter the tag name you'd like applied to the instance. Tag Name gives the name to the EC2 instance."
|
||||||
|
Type: String
|
||||||
|
MinLength: 1
|
||||||
|
Default: "myBastion"
|
||||||
|
pVolumeSize:
|
||||||
|
Description:
|
||||||
|
Enter the number of GBs you want your volume to be. The minimum value
|
||||||
|
is 8 GBs
|
||||||
|
Type: Number
|
||||||
|
Default: 50
|
||||||
|
MinValue: 8
|
||||||
|
pEbsDeleteOnTermination:
|
||||||
|
Description: "Specify if the EBS volume should be deleted if EC2 is deleted."
|
||||||
|
Type: String
|
||||||
|
Default: true
|
||||||
|
AllowedValues:
|
||||||
|
- true
|
||||||
|
- false
|
||||||
|
Rules:
|
||||||
|
SubnetInVPC:
|
||||||
|
Assertions:
|
||||||
|
- Assert: !EachMemberIn
|
||||||
|
- !ValueOfAll
|
||||||
|
- AWS::EC2::Subnet::Id
|
||||||
|
- VpcId
|
||||||
|
- !RefAll "AWS::EC2::VPC::Id"
|
||||||
|
AssertDescription: All subnets must in the VPC
|
||||||
|
Resources:
|
||||||
|
rSecurityGroupDefault:
|
||||||
|
Type: AWS::EC2::SecurityGroup
|
||||||
|
Properties:
|
||||||
|
GroupDescription: !Sub "Default SG for SC Product ${pTagNameValue} "
|
||||||
|
VpcId: !Ref pVpc
|
||||||
|
SecurityGroupEgress:
|
||||||
|
- Description: Outbound unrestricted traffic
|
||||||
|
IpProtocol: "-1"
|
||||||
|
CidrIp: 0.0.0.0/0
|
||||||
|
Tags:
|
||||||
|
- Key: Name
|
||||||
|
Value: !Ref pTagNameValue
|
||||||
|
rLinuxEc2:
|
||||||
|
Type: AWS::EC2::Instance
|
||||||
|
Metadata:
|
||||||
|
guard:
|
||||||
|
SuppressedRules:
|
||||||
|
- 'EC2_INSTANCE_DETAILED_MONITORING_ENABLED'
|
||||||
|
Properties:
|
||||||
|
ImageId: !Ref pOperatingSystem
|
||||||
|
IamInstanceProfile: !Ref rec2InstanceProfile
|
||||||
|
Monitoring: false
|
||||||
|
InstanceType: !Ref pInstanceType
|
||||||
|
EbsOptimized: true
|
||||||
|
SourceDestCheck: true
|
||||||
|
SubnetId: !Ref pSubnet
|
||||||
|
SecurityGroupIds:
|
||||||
|
- !Ref rSecurityGroupDefault
|
||||||
|
BlockDeviceMappings:
|
||||||
|
- DeviceName: "/dev/xvda"
|
||||||
|
Ebs:
|
||||||
|
VolumeSize: !Ref pVolumeSize
|
||||||
|
DeleteOnTermination: !Ref pEbsDeleteOnTermination
|
||||||
|
Tags:
|
||||||
|
- Key: Name
|
||||||
|
Value: !Ref pTagNameValue
|
||||||
|
UserData:
|
||||||
|
Fn::Base64:
|
||||||
|
yum update -y
|
||||||
|
## Instance Profiles
|
||||||
|
## EC2 IAM Roles
|
||||||
|
rEc2Role:
|
||||||
|
Type: AWS::IAM::Role
|
||||||
|
Properties:
|
||||||
|
RoleName: !Sub "ec2-role-${AWS::StackName}"
|
||||||
|
AssumeRolePolicyDocument:
|
||||||
|
Statement:
|
||||||
|
- Effect: Allow
|
||||||
|
Principal:
|
||||||
|
Service: [ec2.amazonaws.com]
|
||||||
|
Action: ['sts:AssumeRole']
|
||||||
|
Path: /
|
||||||
|
ManagedPolicyArns:
|
||||||
|
- !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore'
|
||||||
|
- !Sub 'arn:${AWS::Partition}:iam::aws:policy/CloudWatchAgentServerPolicy'
|
||||||
|
rec2InstanceProfile:
|
||||||
|
Type: AWS::IAM::InstanceProfile
|
||||||
|
Properties:
|
||||||
|
InstanceProfileName: !Sub "ec2-profile-${AWS::StackName}"
|
||||||
|
Path: /
|
||||||
|
Roles:
|
||||||
|
- !Ref rEc2Role
|
||||||
|
Outputs:
|
||||||
|
oLinuxEc2InstanceId:
|
||||||
|
Description: Resource ID of the newly created EC2 instance
|
||||||
|
Value: !Ref rLinuxEc2
|
||||||
|
oLinuxEc2PrivateIP:
|
||||||
|
Description: Private IP Address for EC2
|
||||||
|
Value: !GetAtt rLinuxEc2.PrivateIp
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# federated-setup
|
||||||
|
|
||||||
|
## [github-action-oidc-federation](./github-actions-oidc-federation.yml)
|
||||||
|
|
||||||
|
Setup of the OIDC federation between your GitHub Organization/repository and
|
||||||
|
your AWS account.
|
||||||
|
|
||||||
|
## [github-actions-oidc-federation-and-role](./github-actions-oidc-federation-and-role.yml)
|
||||||
|
|
||||||
|
Setup of the OIDC federation between your GitHub Organization/repository and
|
||||||
|
your AWS account along with a role that only executes on specific branch.
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
AWSTemplateFormatVersion: "2010-09-09"
|
||||||
|
Description: Github Actions configuration - OIDC IAM IdP and associated role CI/CD
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
|
||||||
|
GitHubOrganization:
|
||||||
|
Type: String
|
||||||
|
Description: This is the root organization or personal account where repos are stored (Case Sensitive)
|
||||||
|
|
||||||
|
RepositoryName:
|
||||||
|
Type: String
|
||||||
|
Description: The repo(s) these roles will have access to. (Use * for all org or personal repos)
|
||||||
|
Default: "*"
|
||||||
|
|
||||||
|
BranchName:
|
||||||
|
Type: String
|
||||||
|
Description: Name of the git branch to to trust. (Use * for all branches)
|
||||||
|
Default: "*"
|
||||||
|
|
||||||
|
RoleName:
|
||||||
|
Type: String
|
||||||
|
Description: Name the Role
|
||||||
|
|
||||||
|
UseExistingProvider:
|
||||||
|
Type: String
|
||||||
|
Description: "Only one GitHub Provider can exists. Choose yes if one is already present in account"
|
||||||
|
Default: "no"
|
||||||
|
AllowedValues:
|
||||||
|
- "yes"
|
||||||
|
- "no"
|
||||||
|
|
||||||
|
Conditions:
|
||||||
|
|
||||||
|
CreateProvider: !Equals ["no", !Ref UseExistingProvider]
|
||||||
|
|
||||||
|
Resources:
|
||||||
|
|
||||||
|
IdpGitHubOidc:
|
||||||
|
Type: AWS::IAM::OIDCProvider
|
||||||
|
Condition: CreateProvider
|
||||||
|
Properties:
|
||||||
|
Url: https://token.actions.githubusercontent.com
|
||||||
|
ClientIdList:
|
||||||
|
- sts.amazonaws.com
|
||||||
|
- !Sub https://github.com/${GitHubOrganization}/${RepositoryName}
|
||||||
|
ThumbprintList:
|
||||||
|
- 6938fd4d98bab03faadb97b34396831e3780aea1
|
||||||
|
Tags:
|
||||||
|
- Key: Name
|
||||||
|
Value: !Sub ${RoleName}-OIDC-Provider
|
||||||
|
|
||||||
|
RoleGithubActions:
|
||||||
|
Type: AWS::IAM::Role
|
||||||
|
Properties:
|
||||||
|
RoleName: !Ref RoleName
|
||||||
|
AssumeRolePolicyDocument:
|
||||||
|
Statement:
|
||||||
|
- Effect: Allow
|
||||||
|
Action: sts:AssumeRoleWithWebIdentity
|
||||||
|
Principal:
|
||||||
|
Federated: !If
|
||||||
|
- CreateProvider
|
||||||
|
- !Ref IdpGitHubOidc
|
||||||
|
- !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
|
||||||
|
Condition:
|
||||||
|
StringLike:
|
||||||
|
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrganization}/${RepositoryName}:ref:refs/heads/${BranchName}
|
||||||
|
ManagedPolicyArns:
|
||||||
|
## edit the managed policy to give least privileges
|
||||||
|
- !Sub arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
|
||||||
|
IdpGitHubOidc:
|
||||||
|
Condition: CreateProvider
|
||||||
|
Description: "ARN of Github OIDC Provider"
|
||||||
|
Value: !GetAtt IdpGitHubOidc.Arn
|
||||||
|
|
||||||
|
RoleGithubActionsARN:
|
||||||
|
Description: "CICD Role for GitHub Actions"
|
||||||
|
Value: !GetAtt RoleGithubActions.Arn
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
AWSTemplateFormatVersion: "2010-09-09"
|
||||||
|
Description: Github Actions configuration - OIDC IAM IdP Federation
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
|
||||||
|
GitHubOrganization:
|
||||||
|
Type: String
|
||||||
|
Description: This is the root organization or personal account where repos are stored (Case Sensitive)
|
||||||
|
Default: ""
|
||||||
|
|
||||||
|
RepositoryName:
|
||||||
|
Type: String
|
||||||
|
Description: The repo(s) these roles will have access to. (Use * for all org or personal repos)
|
||||||
|
Default: "*"
|
||||||
|
|
||||||
|
RoleName:
|
||||||
|
Type: String
|
||||||
|
Description: Name the Role
|
||||||
|
Default: ""
|
||||||
|
|
||||||
|
|
||||||
|
Resources:
|
||||||
|
|
||||||
|
IdpGitHubOidc:
|
||||||
|
Type: AWS::IAM::OIDCProvider
|
||||||
|
Properties:
|
||||||
|
Url: https://token.actions.githubusercontent.com
|
||||||
|
ClientIdList:
|
||||||
|
- sts.amazonaws.com
|
||||||
|
- !Sub https://github.com/${GitHubOrganization}/${RepositoryName}
|
||||||
|
ThumbprintList:
|
||||||
|
- 6938fd4d98bab03faadb97b34396831e3780aea1
|
||||||
|
Tags:
|
||||||
|
- Key: Name
|
||||||
|
Value: !Sub ${RoleName}-OIDC-Provider
|
||||||
|
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
|
||||||
|
IdpGitHubOidc:
|
||||||
|
Description: "ARN of Github OIDC Provider"
|
||||||
|
Value: !GetAtt IdpGitHubOidc.Arn
|
||||||
Generated
+8
-9
@@ -1,17 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "configure-aws-credentials",
|
"name": "configure-aws-credentials",
|
||||||
"version": "6.2.4",
|
"version": "6.2.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "configure-aws-credentials",
|
"name": "configure-aws-credentials",
|
||||||
"version": "6.2.4",
|
"version": "6.2.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^3.0.1",
|
"@actions/core": "^3.0.1",
|
||||||
"@aws-sdk/client-sts": "^3.1116.0",
|
"@aws-sdk/client-sts": "^3.1116.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.972.63",
|
|
||||||
"@smithy/node-http-handler": "^4.11.3",
|
"@smithy/node-http-handler": "^4.11.3",
|
||||||
"proxy-agent": "^8.0.2"
|
"proxy-agent": "^8.0.2"
|
||||||
},
|
},
|
||||||
@@ -5179,9 +5178,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.3.2",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
|
||||||
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
|
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -5919,9 +5918,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/markdownlint-cli/node_modules/js-yaml": {
|
"node_modules/markdownlint-cli/node_modules/js-yaml": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.1.tgz",
|
||||||
"integrity": "sha512-n+mUVyUX5bVv7G/G2zyIHOhdxfuU1dY2NOFzTQUWiMUbFss8b57NFlgCCaggU78wSw5KVS9cllzeLyzyR+n5nw==",
|
"integrity": "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "configure-aws-credentials",
|
"name": "configure-aws-credentials",
|
||||||
"description": "A GitHub Action to configure AWS credentials",
|
"description": "A GitHub Action to configure AWS credentials",
|
||||||
"version": "6.2.4",
|
"version": "6.2.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"lint": "biome check --error-on-warnings ./src ./test && markdownlint -i node_modules -i CHANGELOG.md '**/*.md'",
|
"lint": "biome check --error-on-warnings ./src ./test && markdownlint -i node_modules -i CHANGELOG.md '**/*.md'",
|
||||||
@@ -35,7 +35,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^3.0.1",
|
"@actions/core": "^3.0.1",
|
||||||
"@aws-sdk/client-sts": "^3.1116.0",
|
"@aws-sdk/client-sts": "^3.1116.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.972.63",
|
|
||||||
"@smithy/node-http-handler": "^4.11.3",
|
"@smithy/node-http-handler": "^4.11.3",
|
||||||
"proxy-agent": "^8.0.2"
|
"proxy-agent": "^8.0.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { info } from '@actions/core';
|
import { info } from '@actions/core';
|
||||||
import { STSClient } from '@aws-sdk/client-sts';
|
import { STSClient } from '@aws-sdk/client-sts';
|
||||||
import { defaultProvider } from '@aws-sdk/credential-provider-node';
|
|
||||||
import type { AwsCredentialIdentity } from '@aws-sdk/types';
|
import type { AwsCredentialIdentity } from '@aws-sdk/types';
|
||||||
import { NodeHttpHandler } from '@smithy/node-http-handler';
|
import { NodeHttpHandler } from '@smithy/node-http-handler';
|
||||||
import { ProxyAgent } from 'proxy-agent';
|
import { ProxyAgent } from 'proxy-agent';
|
||||||
import { buildCustomUserAgent, errorMessage, getCallerIdentity, maskProxyCredentials } from './helpers';
|
import { buildCustomUserAgent, errorMessage, getCallerIdentity } from './helpers';
|
||||||
import { ProxyResolver } from './ProxyResolver';
|
import { ProxyResolver } from './ProxyResolver';
|
||||||
|
|
||||||
if (!process.env.AWS_EXECUTION_ENV) {
|
if (!process.env.AWS_EXECUTION_ENV) {
|
||||||
@@ -32,7 +31,6 @@ export class CredentialsClient {
|
|||||||
}
|
}
|
||||||
if (props.proxyServer) {
|
if (props.proxyServer) {
|
||||||
info('Configuring proxy handler for STS client');
|
info('Configuring proxy handler for STS client');
|
||||||
maskProxyCredentials(props.proxyServer);
|
|
||||||
const proxyOptions: { httpProxy: string; httpsProxy: string; noProxy?: string } = {
|
const proxyOptions: { httpProxy: string; httpsProxy: string; noProxy?: string } = {
|
||||||
httpProxy: props.proxyServer,
|
httpProxy: props.proxyServer,
|
||||||
httpsProxy: props.proxyServer,
|
httpsProxy: props.proxyServer,
|
||||||
@@ -107,15 +105,9 @@ export class CredentialsClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async loadCredentials() {
|
private async loadCredentials() {
|
||||||
// Previously we constructed a new client, but that picks up the default provider chain including the endpoint.
|
const config = {} as { requestHandler?: NodeHttpHandler };
|
||||||
// Explicitly calling the default provider chain allows us to pass in the endpoint and region as well as the
|
if (this.requestHandler !== undefined) config.requestHandler = this.requestHandler;
|
||||||
// proxy config.
|
const client = new STSClient(config);
|
||||||
return defaultProvider({
|
return client.config.credentials();
|
||||||
clientConfig: {
|
|
||||||
...(this.region !== undefined && { region: this.region }),
|
|
||||||
...(this.stsEndpoint !== undefined && { endpoint: this.stsEndpoint }),
|
|
||||||
...(this.requestHandler !== undefined && { requestHandler: this.requestHandler }),
|
|
||||||
},
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-30
@@ -5,6 +5,7 @@ import type { Credentials, STSClient } from '@aws-sdk/client-sts';
|
|||||||
import { GetCallerIdentityCommand } from '@aws-sdk/client-sts';
|
import { GetCallerIdentityCommand } from '@aws-sdk/client-sts';
|
||||||
import type { AwsCredentialIdentity } from '@aws-sdk/types';
|
import type { AwsCredentialIdentity } from '@aws-sdk/types';
|
||||||
import type { UserAgent } from '@smithy/types';
|
import type { UserAgent } from '@smithy/types';
|
||||||
|
import type { CredentialsClient } from './CredentialsClient';
|
||||||
|
|
||||||
const MAX_TAG_VALUE_LENGTH = 256;
|
const MAX_TAG_VALUE_LENGTH = 256;
|
||||||
const SANITIZATION_CHARACTER = '_';
|
const SANITIZATION_CHARACTER = '_';
|
||||||
@@ -166,13 +167,15 @@ export function exportAccountId(identity: { Account: string; Arn: string }, mask
|
|||||||
// Validates that the account of the already-resolved caller identity is in the allow-list provided via the
|
// Validates that the account of the already-resolved caller identity is in the allow-list provided via the
|
||||||
// `allowed-account-ids` input.
|
// `allowed-account-ids` input.
|
||||||
export function validateAccountId(expectedAccountIds: string[] | undefined, account: string | undefined): void {
|
export function validateAccountId(expectedAccountIds: string[] | undefined, account: string | undefined): void {
|
||||||
const allowedAccountIds = expectedAccountIds?.filter((id) => id !== '') ?? [];
|
if (!expectedAccountIds || expectedAccountIds.length === 0 || expectedAccountIds[0] === '') {
|
||||||
if (allowedAccountIds.length === 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!account || !allowedAccountIds.includes(account)) {
|
if (!account || !expectedAccountIds.includes(account)) {
|
||||||
// Account IDs are deliberately omitted: this error reaches the job log before any mask exists.
|
throw new Error(
|
||||||
throw new Error('The account ID of the provided credentials does not match any of the allowed account IDs');
|
`The account ID of the provided credentials (${
|
||||||
|
account ?? 'unknown'
|
||||||
|
}) does not match any of the expected account IDs: ${expectedAccountIds.join(', ')}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,29 +193,6 @@ export function toCredentialIdentity(creds?: Partial<Credentials>): AwsCredentia
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registers any userinfo embedded in a proxy URL as secrets so it is masked in job logs.
|
|
||||||
// First the literal proxy string, then any username/password components if parseable.
|
|
||||||
// If the username/password is percent-encoded, the decoded form is also masked.
|
|
||||||
export function maskProxyCredentials(proxyServer: string): void {
|
|
||||||
core.setSecret(proxyServer);
|
|
||||||
let url: URL;
|
|
||||||
try {
|
|
||||||
url = new URL(proxyServer);
|
|
||||||
} catch (_) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const part of [url.username, url.password]) {
|
|
||||||
if (!part) continue;
|
|
||||||
core.setSecret(part);
|
|
||||||
try {
|
|
||||||
const decoded = decodeURIComponent(part);
|
|
||||||
if (decoded !== part) core.setSecret(decoded);
|
|
||||||
} catch (_) {
|
|
||||||
// malformed percent-encoding; the raw form is already masked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tags have a more restrictive set of acceptable characters than GitHub environment variables can.
|
// Tags have a more restrictive set of acceptable characters than GitHub environment variables can.
|
||||||
// This replaces anything not conforming to the tag restrictions by inverting the regular expression.
|
// This replaces anything not conforming to the tag restrictions by inverting the regular expression.
|
||||||
// See the AWS documentation for constraint specifics https://docs.aws.amazon.com/STS/latest/APIReference/API_Tag.html.
|
// See the AWS documentation for constraint specifics https://docs.aws.amazon.com/STS/latest/APIReference/API_Tag.html.
|
||||||
@@ -280,13 +260,13 @@ export async function retryAndBackoff<T>(
|
|||||||
`Retrying after ${Math.floor(delay)}ms.`,
|
`Retrying after ${Math.floor(delay)}ms.`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await sleep(delay);
|
||||||
|
|
||||||
if (nextRetry >= maxRetries) {
|
if (nextRetry >= maxRetries) {
|
||||||
core.info(`Retry${opName}: reached max retries (${maxRetries}); giving up.`);
|
core.info(`Retry${opName}: reached max retries (${maxRetries}); giving up.`);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
await sleep(delay);
|
|
||||||
|
|
||||||
return await retryAndBackoff(fn, isRetryable, maxRetries, nextRetry, base, label);
|
return await retryAndBackoff(fn, isRetryable, maxRetries, nextRetry, base, label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,6 +281,19 @@ export function isDefined<T>(i: T | undefined | null): i is T {
|
|||||||
}
|
}
|
||||||
/* c8 ignore stop */
|
/* c8 ignore stop */
|
||||||
|
|
||||||
|
export async function areCredentialsValid(credentialsClient: CredentialsClient) {
|
||||||
|
const client = credentialsClient.stsClient;
|
||||||
|
try {
|
||||||
|
const identity = await client.send(new GetCallerIdentityCommand({}));
|
||||||
|
if (identity.Account) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Like core.getBooleanInput, but respects the required option.
|
* Like core.getBooleanInput, but respects the required option.
|
||||||
*
|
*
|
||||||
|
|||||||
+5
-13
@@ -3,12 +3,12 @@ import type { AssumeRoleCommandOutput } from '@aws-sdk/client-sts';
|
|||||||
import { assumeRole } from './assumeRole';
|
import { assumeRole } from './assumeRole';
|
||||||
import { CredentialsClient } from './CredentialsClient';
|
import { CredentialsClient } from './CredentialsClient';
|
||||||
import {
|
import {
|
||||||
|
areCredentialsValid,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
exportAccountId,
|
exportAccountId,
|
||||||
exportCredentials,
|
exportCredentials,
|
||||||
exportRegion,
|
exportRegion,
|
||||||
getBooleanInput,
|
getBooleanInput,
|
||||||
getCallerIdentity,
|
|
||||||
retryAndBackoff,
|
retryAndBackoff,
|
||||||
toCredentialIdentity,
|
toCredentialIdentity,
|
||||||
translateEnvVariables,
|
translateEnvVariables,
|
||||||
@@ -53,8 +53,8 @@ export async function run() {
|
|||||||
});
|
});
|
||||||
const roleChaining = getBooleanInput('role-chaining', { required: false });
|
const roleChaining = getBooleanInput('role-chaining', { required: false });
|
||||||
const outputCredentials = getBooleanInput('output-credentials', { required: false });
|
const outputCredentials = getBooleanInput('output-credentials', { required: false });
|
||||||
// Default to always outputting environment credentials unless profile is specified. If profile is specified,
|
// Default to always outputting environment credentials unless profile is specified. If profile is specified, default
|
||||||
// default to no environment credentials (but still output them if the user specifically requests it).
|
// to no environment credentials (but still output them if the user specifically requests it).
|
||||||
const outputEnvCredentials = getBooleanInput('output-env-credentials', { required: false, default: !awsProfile });
|
const outputEnvCredentials = getBooleanInput('output-env-credentials', { required: false, default: !awsProfile });
|
||||||
const unsetCurrentCredentials = getBooleanInput('unset-current-credentials', { required: false });
|
const unsetCurrentCredentials = getBooleanInput('unset-current-credentials', { required: false });
|
||||||
let disableRetry = getBooleanInput('disable-retry', { required: false });
|
let disableRetry = getBooleanInput('disable-retry', { required: false });
|
||||||
@@ -165,16 +165,8 @@ export async function run() {
|
|||||||
|
|
||||||
//if the user wants to attempt to use existing credentials, check if we have some already
|
//if the user wants to attempt to use existing credentials, check if we have some already
|
||||||
if (useExistingCredentials) {
|
if (useExistingCredentials) {
|
||||||
const identity = await (async () => {
|
const validCredentials = await areCredentialsValid(credentialsClient);
|
||||||
try {
|
if (validCredentials) {
|
||||||
return await getCallerIdentity(credentialsClient.stsClient);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
if (identity) {
|
|
||||||
// The allowed-account-ids guardrail applies to reused credentials too.
|
|
||||||
validateAccountId(expectedAccountIds, identity.Account);
|
|
||||||
core.notice('Pre-existing credentials are valid. No need to generate new ones.');
|
core.notice('Pre-existing credentials are valid. No need to generate new ones.');
|
||||||
if (timeoutId) clearTimeout(timeoutId);
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -53,15 +53,8 @@ export function parseIni(iniData: string): Record<string, Record<string, string>
|
|||||||
export function stringifyIni(data: Record<string, Record<string, string>>): string {
|
export function stringifyIni(data: Record<string, Record<string, string>>): string {
|
||||||
const sections: string[] = [];
|
const sections: string[] = [];
|
||||||
for (const [sectionName, sectionData] of Object.entries(data)) {
|
for (const [sectionName, sectionData] of Object.entries(data)) {
|
||||||
if (/[\r\n]/.test(sectionName)) {
|
|
||||||
throw new Error('INI section names must not contain newline characters');
|
|
||||||
}
|
|
||||||
const lines: string[] = [`[${sectionName}]`];
|
const lines: string[] = [`[${sectionName}]`];
|
||||||
for (const [key, value] of Object.entries(sectionData)) {
|
for (const [key, value] of Object.entries(sectionData)) {
|
||||||
// A newline in a key or value would inject arbitrary INI lines (e.g. credential_process).
|
|
||||||
if (/[\r\n]/.test(key) || /[\r\n]/.test(value)) {
|
|
||||||
throw new Error('INI keys and values must not contain newline characters');
|
|
||||||
}
|
|
||||||
lines.push(`${key} = ${value}`);
|
lines.push(`${key} = ${value}`);
|
||||||
}
|
}
|
||||||
sections.push(lines.join('\n'));
|
sections.push(lines.join('\n'));
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
|
||||||
|
|
||||||
vi.mock('@aws-sdk/credential-provider-node', () => ({
|
|
||||||
defaultProvider: vi.fn(() => async () => ({ accessKeyId: 'AKIA', secretAccessKey: 'secret' })),
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { defaultProvider } from '@aws-sdk/credential-provider-node';
|
|
||||||
import { CredentialsClient } from '../src/CredentialsClient';
|
|
||||||
|
|
||||||
describe('CredentialsClient', {}, () => {
|
|
||||||
it('pins ambient credential resolution to the configured region and STS endpoint', {}, async () => {
|
|
||||||
const client = new CredentialsClient({
|
|
||||||
region: 'eu-west-1',
|
|
||||||
stsEndpoint: 'https://sts.example.com',
|
|
||||||
roleChaining: false,
|
|
||||||
});
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: any required to call private method
|
|
||||||
await (client as any).loadCredentials();
|
|
||||||
expect(defaultProvider).toHaveBeenCalledWith({
|
|
||||||
clientConfig: expect.objectContaining({ region: 'eu-west-1', endpoint: 'https://sts.example.com' }),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('omits unset client config values from ambient credential resolution', {}, async () => {
|
|
||||||
const client = new CredentialsClient({ region: 'eu-west-1', roleChaining: false });
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: any required to call private method
|
|
||||||
await (client as any).loadCredentials();
|
|
||||||
expect(defaultProvider).toHaveBeenLastCalledWith({ clientConfig: { region: 'eu-west-1' } });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -126,45 +126,6 @@ describe('Configure AWS Credentials helpers', {}, () => {
|
|||||||
expect(core.exportVariable).toHaveBeenCalledWith('AWS_SESSION_TOKEN', '');
|
expect(core.exportVariable).toHaveBeenCalledWith('AWS_SESSION_TOKEN', '');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('maskProxyCredentials', {}, () => {
|
|
||||||
it('masks username and password embedded in a proxy URL', {}, () => {
|
|
||||||
helpers.maskProxyCredentials('http://user:secretpass@proxy.example.com:8080');
|
|
||||||
expect(core.setSecret).toHaveBeenCalledWith('user');
|
|
||||||
expect(core.setSecret).toHaveBeenCalledWith('secretpass');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('masks both encoded and decoded forms of the credentials', {}, () => {
|
|
||||||
helpers.maskProxyCredentials('http://user:p%40ss@proxy.example.com:8080');
|
|
||||||
expect(core.setSecret).toHaveBeenCalledWith('p%40ss');
|
|
||||||
expect(core.setSecret).toHaveBeenCalledWith('p@ss');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('masks the whole value even without embedded credentials or when unparseable', {}, () => {
|
|
||||||
helpers.maskProxyCredentials('http://proxy.example.com:8080');
|
|
||||||
expect(core.setSecret).toHaveBeenCalledWith('http://proxy.example.com:8080');
|
|
||||||
helpers.maskProxyCredentials('not a url');
|
|
||||||
expect(core.setSecret).toHaveBeenCalledWith('not a url');
|
|
||||||
// no username/password parts, so exactly one mask per call
|
|
||||||
expect(core.setSecret).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('validateAccountId', {}, () => {
|
|
||||||
it('enforces the allow-list even when the first element is empty', {}, () => {
|
|
||||||
expect(() => helpers.validateAccountId(['', '999999999999'], '111111111111')).toThrow(/does not match/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('passes an allowed account despite empty entries in the list', {}, () => {
|
|
||||||
expect(() => helpers.validateAccountId(['', '111111111111'], '111111111111')).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('skips validation only when no non-empty entries exist', {}, () => {
|
|
||||||
expect(() => helpers.validateAccountId(undefined, '111111111111')).not.toThrow();
|
|
||||||
expect(() => helpers.validateAccountId([], '111111111111')).not.toThrow();
|
|
||||||
expect(() => helpers.validateAccountId([''], '111111111111')).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('filesystem helpers', {}, () => {
|
describe('filesystem helpers', {}, () => {
|
||||||
describe('isSymlink', {}, () => {
|
describe('isSymlink', {}, () => {
|
||||||
it('returns true for a symlink', {}, () => {
|
it('returns true for a symlink', {}, () => {
|
||||||
|
|||||||
+5
-47
@@ -841,7 +841,7 @@ describe('Configure AWS Credentials', {}, () => {
|
|||||||
|
|
||||||
await run();
|
await run();
|
||||||
expect(core.setFailed).toHaveBeenCalledWith(
|
expect(core.setFailed).toHaveBeenCalledWith(
|
||||||
'The account ID of the provided credentials does not match any of the allowed account IDs',
|
'The account ID of the provided credentials (111111111111) does not match any of the expected account IDs: 999999999999',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -861,7 +861,7 @@ describe('Configure AWS Credentials', {}, () => {
|
|||||||
|
|
||||||
await run();
|
await run();
|
||||||
expect(core.setFailed).toHaveBeenCalledWith(
|
expect(core.setFailed).toHaveBeenCalledWith(
|
||||||
'The account ID of the provided credentials does not match any of the allowed account IDs',
|
'The account ID of the provided credentials (111111111111) does not match any of the expected account IDs: 999999999999, 888888888888',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -917,7 +917,7 @@ describe('Configure AWS Credentials', {}, () => {
|
|||||||
|
|
||||||
await run();
|
await run();
|
||||||
expect(core.setFailed).toHaveBeenCalledWith(
|
expect(core.setFailed).toHaveBeenCalledWith(
|
||||||
'The account ID of the provided credentials does not match any of the allowed account IDs',
|
'The account ID of the provided credentials (111111111111) does not match any of the expected account IDs: 999999999999',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -936,7 +936,7 @@ describe('Configure AWS Credentials', {}, () => {
|
|||||||
|
|
||||||
await run();
|
await run();
|
||||||
expect(core.setFailed).toHaveBeenCalledWith(
|
expect(core.setFailed).toHaveBeenCalledWith(
|
||||||
'The account ID of the provided credentials does not match any of the allowed account IDs',
|
'The account ID of the provided credentials (111111111111) does not match any of the expected account IDs: 999999999999',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -956,7 +956,7 @@ describe('Configure AWS Credentials', {}, () => {
|
|||||||
|
|
||||||
await run();
|
await run();
|
||||||
expect(core.setFailed).toHaveBeenCalledWith(
|
expect(core.setFailed).toHaveBeenCalledWith(
|
||||||
'The account ID of the provided credentials does not match any of the allowed account IDs',
|
'The account ID of the provided credentials (111111111111) does not match any of the expected account IDs: 999999999999',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1015,33 +1015,6 @@ describe('Configure AWS Credentials', {}, () => {
|
|||||||
await run();
|
await run();
|
||||||
expect(core.setFailed).not.toHaveBeenCalled();
|
expect(core.setFailed).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails on the use-existing-credentials path when the account is not allowed', async () => {
|
|
||||||
vi.mocked(core.getInput).mockImplementation(
|
|
||||||
mocks.getInput({
|
|
||||||
...mocks.USE_EXISTING_CREDENTIALS_INPUTS,
|
|
||||||
'allowed-account-ids': '999999999999',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
|
|
||||||
|
|
||||||
await run();
|
|
||||||
expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('does not match'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('reuses existing credentials when their account is allowed', async () => {
|
|
||||||
vi.mocked(core.getInput).mockImplementation(
|
|
||||||
mocks.getInput({
|
|
||||||
...mocks.USE_EXISTING_CREDENTIALS_INPUTS,
|
|
||||||
'allowed-account-ids': '111111111111',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
|
|
||||||
|
|
||||||
await run();
|
|
||||||
expect(core.notice).toHaveBeenCalledWith('Pre-existing credentials are valid. No need to generate new ones.');
|
|
||||||
expect(core.setFailed).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Global Timeout Configuration', {}, () => {
|
describe('Global Timeout Configuration', {}, () => {
|
||||||
@@ -1267,21 +1240,6 @@ describe('Configure AWS Credentials', {}, () => {
|
|||||||
|
|
||||||
expect(core.setFailed).not.toHaveBeenCalled();
|
expect(core.setFailed).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('masks credentials embedded in the proxy URL', async () => {
|
|
||||||
vi.mocked(core.getInput).mockImplementation(
|
|
||||||
mocks.getInput({
|
|
||||||
...mocks.GH_OIDC_INPUTS,
|
|
||||||
'http-proxy': 'http://user:secretpass@proxy.example.com:8080',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(core.setSecret).toHaveBeenCalledWith('user');
|
|
||||||
expect(core.setSecret).toHaveBeenCalledWith('secretpass');
|
|
||||||
expect(core.setFailed).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('AWS Profile Support', {}, () => {
|
describe('AWS Profile Support', {}, () => {
|
||||||
|
|||||||
@@ -114,22 +114,6 @@ describe('Profile Manager', {}, () => {
|
|||||||
const result = stringifyIni({ dev: {} });
|
const result = stringifyIni({ dev: {} });
|
||||||
expect(result).toBe('[dev]\n');
|
expect(result).toBe('[dev]\n');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects values containing newlines', {}, () => {
|
|
||||||
expect(() =>
|
|
||||||
stringifyIni({ dev: { aws_session_token: 'token\n[injected]\ncredential_process = evil' } }),
|
|
||||||
).toThrow('must not contain newline characters');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects keys containing newlines', {}, () => {
|
|
||||||
expect(() => stringifyIni({ dev: { 'key\ninjected': 'val' } })).toThrow('must not contain newline characters');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects section names containing newlines', {}, () => {
|
|
||||||
expect(() => stringifyIni({ 'dev\r\n[injected]': { key: 'val' } })).toThrow(
|
|
||||||
'must not contain newline characters',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('validateProfileName', {}, () => {
|
describe('validateProfileName', {}, () => {
|
||||||
@@ -439,24 +423,6 @@ describe('Profile Manager', {}, () => {
|
|||||||
expect(configParsed['profile dev'].region).toBe('us-east-1');
|
expect(configParsed['profile dev'].region).toBe('us-east-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('refuses to write credentials containing newlines instead of injecting profiles', {}, () => {
|
|
||||||
expect(() =>
|
|
||||||
writeProfileFiles(
|
|
||||||
'dev',
|
|
||||||
{
|
|
||||||
AccessKeyId: 'AKIAIOSFODNN7EXAMPLE',
|
|
||||||
SecretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
|
||||||
SessionToken: 'token\n[injected]\ncredential_process = evil-command',
|
|
||||||
},
|
|
||||||
'us-east-1',
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
).toThrow('must not contain newline characters');
|
|
||||||
|
|
||||||
const credsPath = getProfileFilePaths().credentials;
|
|
||||||
expect(fs.existsSync(credsPath)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses correct section naming for default profile', {}, () => {
|
it('uses correct section naming for default profile', {}, () => {
|
||||||
writeProfileFiles(
|
writeProfileFiles(
|
||||||
'default',
|
'default',
|
||||||
|
|||||||
Reference in New Issue
Block a user