mirror of
https://github.com/aws-actions/configure-aws-credentials.git
synced 2026-09-02 05:55:10 +09:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a1829d0ba1 | |||
| 4b8b5e37b4 | |||
| a05dfa82bd | |||
| 378a941623 | |||
| 82408b69eb | |||
| 1f2d3ed486 | |||
| ed5da29eb9 | |||
| 7fdbbb8968 | |||
| 3852440c21 | |||
| c16f89bdf4 | |||
| 9c362eeba7 | |||
| d5f8da8822 | |||
| 2db24970cf | |||
| 58c3b6e457 | |||
| 2630a7c30d | |||
| 9cb796df96 | |||
| 9def7f603e | |||
| f5c3a50aab | |||
| 64cf66c882 | |||
| af68a47c78 | |||
| bcd4089080 | |||
| 721b15f0ee | |||
| 0e771c8069 | |||
| 6e36582a90 | |||
| 7a26c6cbd2 | |||
| 83c855008c | |||
| e2c213d5f6 | |||
| fd3f78a69a | |||
| ab089d1087 | |||
| 0ea2ab0d82 | |||
| 12d377b18d | |||
| 3f6acccbef | |||
| 9f178b3b31 | |||
| 4c029bab3d | |||
| 15313a702c | |||
| c52a2eac0d | |||
| 87f905da14 | |||
| ba7d3485af | |||
| 0802eece3e |
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Stale issue job
|
name: Stale issue job
|
||||||
steps:
|
steps:
|
||||||
- uses: aws-actions/stale-issue-cleanup@v6
|
- uses: aws-actions/stale-issue-cleanup@v7
|
||||||
with:
|
with:
|
||||||
# Setting messages to an empty string will cause the automation to skip
|
# Setting messages to an empty string will cause the automation to skip
|
||||||
# that category
|
# that category
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ these are incredibly useful:
|
|||||||
- Any modifications you've made relevant to the bug
|
- Any modifications you've made relevant to the bug
|
||||||
- Anything unusual about your environment or deployment
|
- Anything unusual about your environment or deployment
|
||||||
|
|
||||||
|
We also ask that you refrain from opening issues via the `gh` CLI or GitHub
|
||||||
|
API. These methods bypass our issue templates and therefore don't apply the
|
||||||
|
proper labels or workflows that we use. Note that AI agents typically do not
|
||||||
|
properly use issue templates. Issues that don't have the proper labels
|
||||||
|
applied may be closed without comment.
|
||||||
|
|
||||||
## Contributing via Pull Requests
|
## Contributing via Pull Requests
|
||||||
|
|
||||||
Contributions via pull requests are much appreciated. Before sending us a pull
|
Contributions via pull requests are much appreciated. Before sending us a pull
|
||||||
|
|||||||
@@ -600,8 +600,9 @@ claims ([1][gh-blog-oidc], [2][sub-claim-custom]).
|
|||||||
|
|
||||||
#### Immutable subject claims
|
#### Immutable subject claims
|
||||||
|
|
||||||
Repositories created on github.com on or after 15 July 2026, and older
|
Repositories created on github.com on or after 15 July 2026, older
|
||||||
repositories that have opted in, emit an [immutable `sub` claim][immutable-sub].
|
repositories that have opted in, and older repositories that have been renamed
|
||||||
|
since 15 July 2026, emit an [immutable `sub` claim][immutable-sub].
|
||||||
This claim appends the permanent numeric ID of the organization and of the
|
This claim appends the permanent numeric ID of the organization and of the
|
||||||
repository after each name, separated by `@`, so that a recycled org or
|
repository after each name, separated by `@`, so that a recycled org or
|
||||||
repository name cannot be used to mint tokens matching a stale trust policy.
|
repository name cannot be used to mint tokens matching a stale trust policy.
|
||||||
|
|||||||
+20
-20
@@ -222,7 +222,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||||||
|
|
||||||
The following npm package may be included in this product:
|
The following npm package may be included in this product:
|
||||||
|
|
||||||
- @aws-sdk/client-sts@3.1101.0
|
- @aws-sdk/client-sts@3.1116.0
|
||||||
|
|
||||||
This package contains the following license:
|
This package contains the following license:
|
||||||
|
|
||||||
@@ -432,9 +432,9 @@ Apache License
|
|||||||
|
|
||||||
The following npm packages may be included in this product:
|
The following npm packages may be included in this product:
|
||||||
|
|
||||||
- @aws-sdk/signature-v4-multi-region@3.996.43
|
- @aws-sdk/signature-v4-multi-region@3.996.46
|
||||||
- @smithy/core@3.31.1
|
- @smithy/core@3.33.3
|
||||||
- @smithy/types@4.16.1
|
- @smithy/types@4.17.2
|
||||||
|
|
||||||
These packages each contain the following license:
|
These packages each contain the following license:
|
||||||
|
|
||||||
@@ -832,7 +832,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|||||||
|
|
||||||
The following npm package may be included in this product:
|
The following npm package may be included in this product:
|
||||||
|
|
||||||
- @aws-sdk/core@3.977.5
|
- @aws-sdk/core@3.977.9
|
||||||
|
|
||||||
This package contains the following license:
|
This package contains the following license:
|
||||||
|
|
||||||
@@ -1042,15 +1042,15 @@ Apache License
|
|||||||
|
|
||||||
The following npm packages may be included in this product:
|
The following npm packages may be included in this product:
|
||||||
|
|
||||||
- @aws-sdk/credential-provider-env@3.972.66
|
- @aws-sdk/credential-provider-env@3.972.70
|
||||||
- @aws-sdk/credential-provider-ini@3.973.11
|
- @aws-sdk/credential-provider-ini@3.973.15
|
||||||
- @aws-sdk/credential-provider-node@3.972.77
|
- @aws-sdk/credential-provider-node@3.972.81
|
||||||
- @aws-sdk/token-providers@3.1102.0
|
- @aws-sdk/token-providers@3.1116.0
|
||||||
- @aws-sdk/types@3.974.2
|
- @aws-sdk/types@3.974.5
|
||||||
- @aws-sdk/xml-builder@3.972.37
|
- @aws-sdk/xml-builder@3.972.40
|
||||||
- @smithy/credential-provider-imds@4.4.16
|
- @smithy/credential-provider-imds@4.5.2
|
||||||
- @smithy/fetch-http-handler@5.6.13
|
- @smithy/fetch-http-handler@5.7.2
|
||||||
- @smithy/node-http-handler@4.9.13
|
- @smithy/node-http-handler@4.11.3
|
||||||
- @smithy/signature-v4@5.6.12
|
- @smithy/signature-v4@5.6.12
|
||||||
|
|
||||||
These packages each contain the following license:
|
These packages each contain the following license:
|
||||||
@@ -1261,9 +1261,9 @@ Apache License
|
|||||||
|
|
||||||
The following npm packages may be included in this product:
|
The following npm packages may be included in this product:
|
||||||
|
|
||||||
- @aws-sdk/credential-provider-process@3.972.66
|
- @aws-sdk/credential-provider-process@3.972.70
|
||||||
- @aws-sdk/credential-provider-sso@3.973.10
|
- @aws-sdk/credential-provider-sso@3.973.14
|
||||||
- @aws-sdk/credential-provider-web-identity@3.972.72
|
- @aws-sdk/credential-provider-web-identity@3.972.76
|
||||||
|
|
||||||
These packages each contain the following license:
|
These packages each contain the following license:
|
||||||
|
|
||||||
@@ -1473,9 +1473,9 @@ Apache License
|
|||||||
|
|
||||||
The following npm packages may be included in this product:
|
The following npm packages may be included in this product:
|
||||||
|
|
||||||
- @aws-sdk/credential-provider-http@3.972.68
|
- @aws-sdk/credential-provider-http@3.972.72
|
||||||
- @aws-sdk/credential-provider-login@3.972.73
|
- @aws-sdk/credential-provider-login@3.972.77
|
||||||
- @aws-sdk/nested-clients@3.997.40
|
- @aws-sdk/nested-clients@3.997.44
|
||||||
|
|
||||||
These packages each contain the following license:
|
These packages each contain the following license:
|
||||||
|
|
||||||
|
|||||||
+328
-126
File diff suppressed because it is too large
Load Diff
@@ -1,14 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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'
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
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 }}"
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# 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 |
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
---
|
|
||||||
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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
---
|
|
||||||
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
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
---
|
|
||||||
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
+361
-363
File diff suppressed because it is too large
Load Diff
+9
-8
@@ -17,25 +17,26 @@
|
|||||||
"organization": true
|
"organization": true
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-sdk/credential-provider-env": "^3.972.65",
|
"@aws-sdk/credential-provider-env": "^3.972.70",
|
||||||
"@biomejs/biome": "2.5.6",
|
"@biomejs/biome": "2.5.10",
|
||||||
"@smithy/property-provider": "^4.4.16",
|
"@smithy/property-provider": "^4.5.2",
|
||||||
"@types/node": "^26.1.2",
|
"@types/node": "^26.2.0",
|
||||||
"@vitest/coverage-v8": "4.1.10",
|
"@vitest/coverage-v8": "4.1.10",
|
||||||
"aws-sdk-client-mock": "^4.1.0",
|
"aws-sdk-client-mock": "^4.1.0",
|
||||||
"esbuild": "^0.28.1",
|
"esbuild": "^0.28.2",
|
||||||
"generate-license-file": "^4.2.1",
|
"generate-license-file": "^4.2.1",
|
||||||
"json-schema": "^0.4.0",
|
"json-schema": "^0.4.0",
|
||||||
"markdownlint-cli": "^0.49.1",
|
"markdownlint-cli": "^0.49.1",
|
||||||
"memfs": "^4.64.0",
|
"memfs": "^4.68.1",
|
||||||
"standard-version": "^9.5.0",
|
"standard-version": "^9.5.0",
|
||||||
"typescript": "^7.0.2",
|
"typescript": "^7.0.2",
|
||||||
"vitest": "4.1.10"
|
"vitest": "4.1.10"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^3.0.1",
|
"@actions/core": "^3.0.1",
|
||||||
"@aws-sdk/client-sts": "^3.1101.0",
|
"@aws-sdk/client-sts": "^3.1116.0",
|
||||||
"@smithy/node-http-handler": "^4.9.13",
|
"@aws-sdk/credential-provider-node": "^3.972.63",
|
||||||
|
"@smithy/node-http-handler": "^4.11.3",
|
||||||
"proxy-agent": "^8.0.2"
|
"proxy-agent": "^8.0.2"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
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 } from './helpers';
|
import { buildCustomUserAgent, errorMessage, getCallerIdentity, maskProxyCredentials } from './helpers';
|
||||||
import { ProxyResolver } from './ProxyResolver';
|
import { ProxyResolver } from './ProxyResolver';
|
||||||
|
|
||||||
if (!process.env.AWS_EXECUTION_ENV) {
|
if (!process.env.AWS_EXECUTION_ENV) {
|
||||||
@@ -31,6 +32,7 @@ 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,
|
||||||
@@ -105,9 +107,15 @@ export class CredentialsClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async loadCredentials() {
|
private async loadCredentials() {
|
||||||
const config = {} as { requestHandler?: NodeHttpHandler };
|
// Previously we constructed a new client, but that picks up the default provider chain including the endpoint.
|
||||||
if (this.requestHandler !== undefined) config.requestHandler = this.requestHandler;
|
// Explicitly calling the default provider chain allows us to pass in the endpoint and region as well as the
|
||||||
const client = new STSClient(config);
|
// proxy config.
|
||||||
return client.config.credentials();
|
return defaultProvider({
|
||||||
|
clientConfig: {
|
||||||
|
...(this.region !== undefined && { region: this.region }),
|
||||||
|
...(this.stsEndpoint !== undefined && { endpoint: this.stsEndpoint }),
|
||||||
|
...(this.requestHandler !== undefined && { requestHandler: this.requestHandler }),
|
||||||
|
},
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-23
@@ -5,7 +5,6 @@ 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 = '_';
|
||||||
@@ -167,15 +166,13 @@ 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 {
|
||||||
if (!expectedAccountIds || expectedAccountIds.length === 0 || expectedAccountIds[0] === '') {
|
const allowedAccountIds = expectedAccountIds?.filter((id) => id !== '') ?? [];
|
||||||
|
if (allowedAccountIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!account || !expectedAccountIds.includes(account)) {
|
if (!account || !allowedAccountIds.includes(account)) {
|
||||||
throw new Error(
|
// Account IDs are deliberately omitted: this error reaches the job log before any mask exists.
|
||||||
`The account ID of the provided credentials (${
|
throw new Error('The account ID of the provided credentials does not match any of the allowed account IDs');
|
||||||
account ?? 'unknown'
|
|
||||||
}) does not match any of the expected account IDs: ${expectedAccountIds.join(', ')}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +190,29 @@ 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.
|
||||||
@@ -260,13 +280,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,19 +301,6 @@ 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.
|
||||||
*
|
*
|
||||||
|
|||||||
+13
-5
@@ -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
|
// Default to always outputting environment credentials unless profile is specified. If profile is specified,
|
||||||
// to no environment credentials (but still output them if the user specifically requests it).
|
// default 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,8 +165,16 @@ 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 validCredentials = await areCredentialsValid(credentialsClient);
|
const identity = await (async () => {
|
||||||
if (validCredentials) {
|
try {
|
||||||
|
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,8 +53,15 @@ 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'));
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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,6 +126,45 @@ 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', {}, () => {
|
||||||
|
|||||||
+47
-5
@@ -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 (111111111111) does not match any of the expected account IDs: 999999999999',
|
'The account ID of the provided credentials does not match any of the allowed account IDs',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -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 (111111111111) does not match any of the expected account IDs: 999999999999, 888888888888',
|
'The account ID of the provided credentials does not match any of the allowed account IDs',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -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 (111111111111) does not match any of the expected account IDs: 999999999999',
|
'The account ID of the provided credentials does not match any of the allowed account IDs',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -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 (111111111111) does not match any of the expected account IDs: 999999999999',
|
'The account ID of the provided credentials does not match any of the allowed account IDs',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -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 (111111111111) does not match any of the expected account IDs: 999999999999',
|
'The account ID of the provided credentials does not match any of the allowed account IDs',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1015,6 +1015,33 @@ 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', {}, () => {
|
||||||
@@ -1240,6 +1267,21 @@ 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,6 +114,22 @@ 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', {}, () => {
|
||||||
@@ -423,6 +439,24 @@ 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