Reusable workflow is good ... Until you realize your identity is also reusable by anyone (2)
- How OIDC authentication works on the cloud
- Customizing subject claim
- What can go wrong?
- Real-world misconfiguration examples
- Wrap up
In my previous blog post, we discussed how a GitHub reusable workflow can be used by others to sign their software artifact.
This is interesting but not exciting enough. So, this time, I’m going to show you how a similar misconfiguration can unexpectedly open the door to the cloud environment.
How OIDC authentication works on the cloud
First, let’s revisit how we usually use GitHub OIDC tokens to authenticate into cloud environments.
Let’s use AWS as an example.
- The GitHub Actions workflow presents the OIDC token to the AWS STS endpoint and requests authentication as an IAM role.
- AWS STS validates the token and checks if the IAM role trust policy matches the OIDC token claims.
- If all the checks are passed, AWS STS returns a session token to the GitHub Actions workflow.
- The workflow uses the session token to access resources in the AWS account.
The most commonly used OIDC claim to identify the calling workflow is the sub
(i.e. subject), which includes the GitHub organization, repository, branch, and the GitHub Actions environment being used.
The processes in other public cloud providers are similar: validating the token authenticity and then checking the token identity using the sub
claim.
Customizing subject claim
We can see that the subject claim of a GitHub Actions OIDC token only contains limited information about the workflow. If we want to get more information about the workflow (e.g., who triggers it), we need to use other claims (e.g. actor
).
Unfortunately, most of the cloud providers don’t allow us to use those non-standard claims for authentication purposes, which means we can’t write our IAM role trust policy to verify if the authenticating workflow is triggered by richardfan
, as follows:
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:actor": "richardfan"
}
}
Luckily, in GitHub, we can customize the OIDC subject claim to include other claims.
We can use API call or GitHub CLI to configure what information to include in the subject claim of our GitHub Actions OIDC tokens.
After that, the subject claim of OIDC tokens will look like this:
E.g., If we include repo
and actor
in the subject
{
...
"sub": "repo:octo-org/octo-repo:actor:richardfan"
...
}
And now, we can achieve the access control in the previous example by the following IAM role trust policy:
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:sub": "repo:octo-org/octo-repo:actor:richardfan"
}
}
What can go wrong?
As we can see, the subject claim of the OIDC token can be customized to include and not include specific claims.
What if someone customizes their OIDC token to include only generic information and uses it as the cloud access control?
E.g., If someone includes only ref
and environment
in the OIDC subject claim and uses the following trust policy in the IAM role:
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:sub": "ref:refs/heads/main:environment:prod"
}
}
Anyone can now create their own GitHub Actions environment called prod
, a GitHub Actions workflow to assume this IAM role, and push it to the main
branch.
Then, we can log in to that AWS account.
Real-world misconfiguration examples
In the previous blog post, we talked about utilizing others’ GitHub reusable workflow to sign our artifact.
I was wondering if the same misconception on the job_workflow_ref
claim can be found on other tasks, and the answer is yes. (If you want to learn more, read my previous blog post)
Just like an access control that relies only on ref
and environment
claims can be abused, an access control that relies only on job_workflow_ref
claims can be abused, too.
After some Google search, I found a few blog posts and tutorials guiding people to use job_workflow_ref
as the condition to authenticate a GitHub Actions workflow to the cloud environment.
Here are some examples:
- Using OIDC with Reusable Workflows to Securely Access Cloud Resources
- How to setup GitHub Actions authentication with AWS using OIDC
- Azure Deployment Using GitHub Actions Reusable Workflows From Central Repo With OIDC
Using reusable workflow to enforce CI/CD governance
These 3 examples have a similar reason for their design: to enforce CI/CD governance.
In an organization having multiple teams building their own workloads, using the same standard to deploy resources is critical.
Using job_workflow_ref
as one of the authentication conditions can force all teams to use the same reusable workflow to access cloud environments.
The DevOps team can then implement best practices in the centralized reusable workflow.
Confused deputy problem
However, using only the job_workflow_ref
as the access control condition causes a confused deputy situation.
job_workflow_ref
indicates the location of the GitHub Actions workflow. In a reusable workflow scenario, this would be the location of the reusable workflow.
In the previous example, the OIDC token may look like this:
{
...
"sub": "repo:octo-org/app-team2-repo:...",
"job_workflow_ref": "octo-org/devops-repo/.github/workflows/deployment-workflow.yml@refs/heads/main"
...
}
If the repository owner customized the subject claim to include job_workflow_ref
, it may look like this:
{
...
"sub": "job_workflow_ref:octo-org/devops-repo/.github/workflows/deployment-workflow.yml@refs/heads/main",
"job_workflow_ref": "octo-org/devops-repo/.github/workflows/deployment-workflow.yml@refs/heads/main"
...
Now, the cloud authentication system (i.e., AWS STS) can only see which workflow requests a session token but can’t see where it is initially triggered.
Is it app-team1-repo
? Is it app-team2-repo
? Or is it something else?
Imagine the reusable workflow is called someone else (which can be anyone if the reusable workflow is publicly readable); that person can also authenticate to the cloud environment!
Try to … hack
To prove my theory, I followed one of the blog posts I found and tried to access its demo environment.
In that blog post, the author created a reusable workflow to log in to a Microsoft Azure account using OIDC tokens.
In his demo Azure account, he used job_workflow_ref
as the only Subject identifier condition and put the location of the reusable workflow hosted in his GitHub repository.
I created a simple GitHub Actions workflow just to call his reusable workflow.
From the screenshot, we can see that my GitHub Actions run can successfully log in to his demo account.
Luckily, this workflow only lists the available secrets in the Azure account and does nothing else. So, even though I can log in to the account, I can’t do any damage to it.
However, I found another blog post suggests using reusable workflow to log in to Azure account and do webapps deployment.
This workflow takes the webapp name and package path as inputs. So, in theory, everyone can put their package in their own repository and use this reusable workflow to deploy it to the owner’s Azure account.
I can’t verify if that’s the case because the demo repository of this blog post has been deleted already.
But whoever follows this blog post … good luck!
Wrap up
GitHub Actions reusable workflow is a great way to centralize and standardize CI/CD pipelines.
Having authentication between CI/CD pipelines and cloud environments aware of which reusable workflow is being used helps enforce the standards.
But we need to be careful, using job_workflow_ref
as the only authentication condition is dangerous.
At the end of the day, we want to know who is trying to login, not just how they login.
If you are implementing GitHub Actions authentication workflow on cloud, make sure the condition on sub
claim is restricted as specific as who you want to access your cloud account.
In most cases, you would always want to check the repo
claim. So keep it as your access control condition unless you know what you are doing.