Leveraging AWS Roles for Enhanced Security in GitHub Workflows


Moving Away From AWS Access Keys

For a considerable period, I, like many others, relied on Terraform to provision my resources within AWS. Terraform, right out of the box, offers multiple authentication methods to access, provision, modify, or destroy resources, not to mention its support for remote state.

In the beginning, I opted for the straightforward approach, using AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY for a pipeline user to handle the authentication. However, I soon encountered a significant challenge. I wanted to retain exclusive approval rights for changes to AWS via a GitHub environment, while my collaborators needed access to the AWS state to perform plans and ensure the proposed changes would function as expected.

One solution could have been to create another AWS account with access only to the states. However, this would lead to an increase in the number of users and, consequently, more credentials at risk of exposure and compromise.

To avoid this logistical conundrum, I discovered a more secure and efficient way to authorize and perform actions against AWS within my repository - AWS roles for my agents!

In the following sections, we will delve into how to use AWS roles within GitHub workflows as an alternative to using access and secret keys. This method not only enhances security but also aligns with the best practices for AWS access management.

The Power of AWS Roles: What They Are and Why They Matter

AWS roles are a potent tool in your AWS arsenal. They offer a secure method to delegate permissions to trusted entities, eliminating the need for long-term credentials. Instead of relying on permanent access keys, AWS roles utilize temporary security credentials. This approach significantly minimizes the risk of credential exposure, thereby bolstering the security of your AWS operations.

But the power of AWS roles doesn’t stop there. They offer a level of flexibility and control that goes beyond users. You can assign roles to specific repositories, branches, and even workflows, providing a granular level of access control.

Moreover, you can define a policy that precisely outlines the scope of what the role can do within AWS. This way, you can ensure that the role grants permissions only for the necessary resources and actions, further enhancing the security of your AWS operations. This approach aligns with the principle of least privilege, a widely recognized best practice in access management.

Integrating AWS Roles with GitHub Workflows

To integrate AWS roles with GitHub workflows:

  1. Install the configure-aws-credentials action from the GitHub marketplace.
  2. In your workflow file, use the configure-aws-credentials action to set up AWS credentials.
  3. Use the role-to-assume parameter to specify the ARN of the role to assume.

Create AWS Policy

The IAM policy grants Terraform the necessary permissions to manage a specific DynamoDB table and S3 bucket in AWS that we will later assign to a role. These resources are used by Terraform for state management, with the DynamoDB table for state locking and the S3 bucket for storing state files.

Follow the steps below to create a new policy in AWS IAM and attach it to an IAM role. Make sure to replace <region>, <account-id>, <dynamodb-table-name>, and <bucket-name> with your specific values.

  1. Create a new policy in AWS IAM

    • Navigate to the AWS Management Console and open the IAM service.
    • In the left navigation pane, choose “Policies”, then “Create policy”.
    • Choose the “JSON” tab and paste the following policy:
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "dynamodb:*",
                    "s3:*"
                ],
                "Resource": [
                    "arn:aws:dynamodb:<region>:<account-id>:table/<dynamodb-table-name>",
                    "arn:aws:s3:::<bucket-name>",
                    "arn:aws:s3:::<bucket-name>/*"
                ]
            }
        ]
    }
    
  2. Attach the newly created policy example-policy to an IAM role

    • In the IAM service, choose “Roles” in the left navigation pane, then “Create role”.
    • Select Custom trust policy option and paste the following policy
    {
        "Version": "2008-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {
                    "Federated": "arn:aws:iam::<account-id>:oidc-provider/token.actions.GitHubusercontent.com"
                },
                "Action": "sts:AssumeRoleWithWebIdentity",
                "Condition": {
                    "StringLike": {
                        "token.actions.GitHubusercontent.com:sub": "repo:<GitHub Account>/<Repo Name>:environment:AWS-Dev"
                    }
                }
            }
        ]
    }
    

    This policy specifies that the role is exclusively applicable to your AWS account, and only within your repository in a particular GitHub environment, namely AWS-Dev. For further details on the conditions you can set, please refer to the official GitHub documentation. Ensure you provide the appropriate <GitHub Account> and <Repo Name> corresponding to your GitHub information.

  3. Attach the policy from step one to the new trust policy and create with a unique name.

    • Make sure you note down the ARN for the newly created role as that’s how GitHub will reference the role it attempts to assume.

Authorizing GitHub Workflow To Utilize The Assumed Role

  1. Securely store your Amazon Resource Name (ARN). This can be done either within your GitHub repository or as an environment secret.

    • GitHub Repository: If you choose to store it in your repository, make sure it is not in a publicly accessible file or location. It is recommended to use a .env file which is added to your .gitignore file to prevent it from being committed to your repository.
    • Environment Secrets: Alternatively, you can store your ARN as a secret in your GitHub repository settings. Go to your repository settings, then to the ‘Secrets’ section. Here, you can add a new secret with your ARN as the value. This secret can then be accessed in your GitHub workflows.
  2. Update your workflow to setup the assumed role that it will then utilize for access to AWS resources.

    Here’s an example:

    name: Terraform Deploy Workflow
    on: 
        push: 
            branches: [main]
    permissions: 
        id-token: write
        contents: read
        pull-requests: write
    jobs:
    deploy:
        runs-on: ubuntu-latest
        environment:
            name: 'AWS-Dev'
        steps:
        - name: Checkout code
        uses: actions/checkout@v2
        - name: Set up AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
            aws-region: us-west-2
            role-to-assume: {{ secrets.ARN_TERRAFORM_BACKEND }}
        - name: Setup Terraform
        uses: hashicorp/[email protected]
        with:
            terraform_version: 1.8.0
        ...
    

    In this example, the workflow assumes the specified AWS role and uses its permissions to read from the terraform backend in S3 and DynamoDB. Note that the ARN again is being pulled directly from a GitHub secret and the value will not be displayed directly in the run.

    For more about aws-actions/configure-aws-credentials@v4 action, you can read the AWS documentation in GitHub.

  3. Upon running your workflow, it should now seamlessly fetch and utilize the assigned role for authenticating with AWS!

Final Thoughts

Using AWS roles in GitHub workflows instead of access and secret keys is a more secure and efficient way to manage AWS permissions. It not only reduces the risk of credential exposure but also simplifies the process of updating permissions.