The Setup
One topic came up over and over on my recent AWS Solutions Architect exam: IAM Users vs. IAM Roles. They recommend IAM Users for real people, and then those people, depending on what they are doing, can assume an appropriate IAM Role to provide the required level of access needed.
As an example, suppose we have a separate AWS account for each of our two environments: Dev
and Production
. Further, we have a separate AWS account for our users (three accounts total). We can set up the IAM Users in the User account to have only the ability to assume certain roles, the roles that let them act upon one of the four deployment environments. A developer logs into his account and then assumes an IAM role that lets her work within the Dev
account. Later, she can switch roles and then work on the Production
account. These roles have independently-controlled permissions (she can do most anything in Dev
, less so in Production
). One user; multiple roles.
For years, I had been configuring my GitHub Actions AWS permissions with an IAM User dedicated to deploying my private repository actions (note that I've already deviated from recommended practice by creating an IAM User for a non-person). This "user" belonged to a group with permissions to handle whatever the deployment pipeline needed: build, test, deploy, validate. I then added this user's AWS Access Key/Secret to GitHub's Action Secrets for the repository. This works, but everyone is doing a lot of work to keep those long-lived keys from being exposed. There is a better way to do this: OpenID Connect.
OpenID Connect, or OIDC, is a protocol built on top of OAuth 2.0. In short, it answers the question "What is the identity of the person currently using this app?" It allows the authentication (AuthN) to reside with an identity provider (GitHub) and allows the resource owner (AWS), who has a trust relationship with the identity provider, to provide access to resources for that user. For AWS, this means using short-lived credentials from its Security Token Service (STS) that reduce the risk of leaking those credentials - they are temporary and disposable.
Since late 2021, GitHub Actions has supported OpenID Connect (OIDC) for obtaining credentials. Last month, I updated my repositories to use OIDC for their interactions with AWS. This post outlines the steps I took to get that set up. I hope this helps you improve the security of your pipelines in GitHub.
Steps
Identity Provider
The first step is to set up GitHub Actions as a recognized identity provider in my AWS account. This is also called an "OIDC Trust" relationship. In AWS IAM, create an Identity Provider with GitHub's provider URL and Audience. I am using the open-source action configure-aws-credentials
(link) which means I want to use an Audience
value of sts.amazonaws.com
. Be sure to click the "Get Thumbprint" button to save a copy of the x.509 certificate used by GitHub into the AWS identity provider.
Master Role Configuration
I want to set up access for my pipeline that is triggered off of my "master" branch. Since this is always kicked off by a merge from a trusted author, we can give it a broad set of permissions. For Dependabot or other Pull-Request builds, you will want a narrower set of permissions (see below).
For my "master" branch build, I will create a role that the authenticated GitHub Action will assume. The role is where I link authentication (AuthN) with authorization (AuthZ), where the OIDC provider does the AuthN while the IAM Policies attached to the role do the AuthZ. To bring these two together I need two chunks of JSON: one for the trust relationship and one for the policies.
The Trust Relationship is where we say "When a particular provider, with a particular audience, for a particular subject, asks for a role, give him this one." In our case, the provider is the oidc-provider
(the ARN) we set up in step 1, the audience is sts.amazon.aws
, and the subject is one of the repositories (or organizations) that you want to authorize. This last part is critical. This is where I lock down the request to GitHub Actions coming from only the repositories I want to use the role (and skipping this step leaves you open to exploitation).
The next part of setting up this role is to provide it with permissions (the AuthZ). In this case, I have an existing IAM Policy for my "GitHub Deployer", so I can attach it to this role. It already has the permissions I need to access the services I routinely use to deploy serverless apps while denying access to services that I don't use (but hackers love) like ec2:*
, iam:CreateUser
, etc. I'll go into this particular section on Deployer Policy Guidelines in a future article.
Update GitHub Actions
We are almost there. The only thing left to do is to add a GitHub Actions step to have the OIDC user assume the role and set temporary credentials. As I mentioned above, I use the open-source Action script called configure-aws-credentials and we only need to pass it the region and the role we want it to assume, like so:
This will set short-term (1 hour by default) credentials in environment variables named AWS_ACCESS_KEY_ID
and AWS_SECRET_ACCESS_KEY
where my aws-sdk will pick them up for testing, deployment, etc.
Dependabot Role Configuration
Why do we need a separate configuration for Dependabot (or for outside-contributor pull requests)? In short, safety. These PRs often include code you have yet to review. Someone could write code to scrape/steal all your secrets in AWS Secrets Manager, spin up new IAM Users with Admin access, or launch bitcoin-mining EC2s in a faraway region. For all this and more we need tighter permissions for PRs.
One approach to tightening permissions on PRs is not to run any tests that need resources, e.g., only unit tests. This may be acceptable in your situation. However, if you want to drive your integration tests, the ones that hit your DynamoDB or S3 or similar, you'll need to give GitHub Actions permissions to do so. Here, we want to firmly stick with the principle of "least privilege" and tightly lock down the role's policies. This means a separate IAM Role from the one we created for "master".
In this role, we use a similar trust relationship, but we can narrow the subject condition to only allow a user to assume this role if he is acting upon a specific organization with a specific repository and it is a pull request, like so:
Meanwhile, we want to create an IAM Policy that reflects only the actions necessary to run your integration test suite. In this repository's case, it needs only to Put, Delete, and Create an item plus the ability to scan the table. I can create a simple policy that allows only these four actions and only against this one particular table (my test table for the repository):
Final Thoughts
If you don't want to put your IAM Role ARN, which includes your AWS account number, in plain text (although this is now not considered sensitive information), you can add the Role ARN as a secret and pull it in, like so:
Summary
AWS and GitHub have done the legwork to make this setup simple. You can go from "I should look into this. 🤔" to "I'm done! 🥂" in an hour. Getting long-lived secrets for non-people IAM Users out of GitHub Actions is reasonably straightforward. So much so, that I would go so far as to say you ought to do it. So, do it. Have fun.
References
PingIdentity: OpenID Connect (OIDC)
GitHub Docs: About Security Hardening with OpenID Connect
GitHub Docs: Configuring OpenID Connect in AWS
GitHub Actions Script: configure-aws-credentials
Last Week in AWS: Are AWS Account IDs Sensitive Information?
Daniel Grzelak: Hacking Github AWS Integrations Again