Secure Your CI/CD: GitHub Actions Workflows with OIDC for AWS and GCP
OpenID Connect (OIDC) has become the gold standard for securing system-to-system interactions in modern cloud workflows. In this post, I’ll guide you through setting up OIDC authentication for CI/CD pipelines using three of the most widely adopted tools in the industry: GitHub Actions, AWS, and GCP. By the end, you’ll have a robust, secure setup for deploying your applications seamlessly to both AWS and GCP resources.
In a previous post, I did a deep dive into the differences between two of today’s newest and more prominent technologies for deploying software using workflows: GitHub Actions and GitLab CI/CD pipelines. You can use either of these platforms to deploy your workloads to public cloud providers such as AWS and GCP.
I will focus this post around GitHub Actions as it has more market share. However, if you have read that post, you will see the case I make for the viability of GitLab’s CI/CD Pipelines. I have also chosen the two most prominent cloud providers and will discuss the nuances of implementing OIDC for both AWS and GCP. The idea is to expand the stack’s scope so most readers will be able to take these concrete steps and apply them to their own situation.
What is OIDC and why should you use it?
OIDC is a protocol for authenticating users to access systems via HTTPS securely. In our case leveraging endpoints to interact with public cloud services and resources that reside within those cloud providers. We can shift this process left in the software development lifecycle by having our CI/CD workflows handle this authentication and authorization. There is no shortage of material on the internet that you can find to get more intricate details of how the OIDC protocol works. I will give a brief description of the pieces needed when they come up in our stack. However, going any deeper is out of scope for this article.
Now, let’s dive into the practical implementation. We will walk through creating a GitHub Actions workflow that leverages the GitHub Actions OIDC provider for both AWS and GCP. For AWS, after successfully authenticating with OIDC, we will push an image to AWS Elastic Container Registry (ECR). We will then repeat this process in GCP, pushing an image to the Artifact Registry. This hands-on approach will demonstrate the real-world application of OIDC in CI/CD pipelines.
Setting up OIDC Provider with AWS
Each of our cloud providers handles identity and access management (IAM) slightly differently, but the concepts are the same for both AWS and GCP. We will start with AWS. For this walk through I use the AWS CLI for its ease of use as well as the descriptive nature of the commands I find spells out exactly what is going on.
First, we need to create an AWS IAM entity that describes an OIDC provider, aka identity provider (IdP). This is the mechanism that will verify and authenticate the client system with OIDC. It will also issue a JSON web token (JWT) upon successful authentication to be used when we interact with our AWS resources via GitHub Actions. In our use case, the client is AWS and the provider is GitHub.
aws iam create-open-id-connect-provider \
- url https://token.actions.githubusercontent.com \
- client-id-list sts.amazonaws.com \
- thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
The URL is the URL of the OIDC IdP to trust. The client-id, aka audience, is used to identify the client application to authenticate with the OIDC IdP. The thumbprint is a publicly available server certificate that the IdP uses, in this case it is Github’s. You can find a more detailed synopsis of this command at the AWS docs. Once you run this command you can verify that the provider was created in the AWS console by navigating to the IAM page and under Identity Providers you will now see the token.actions.githubusercontent.com provider.
Next, we will create an AWS IAM Role such that it will have a Trust Relationship set up with this IdP.
aws iam create-role \
--role-name GithubActionsRole \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::<AWS_ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:<GITHUB_ORGANIZATION>/<GITHUB_REPOSITORY>:*"
}
}
}
]
}'
Note, when you run this command to replace the following AWS_ACCOUNT_ID with your ID. Replace GITHUB_ORGANIZATION and GITHUB_REPOSITORY to match the organization and repository your GitHub Actions workflow will reside.
Finally, we will attach some policies to this role. The first policy is a best practice to reduce any potential security risks with the GitHub Action user assuming any other role and the second defines the permissions needed to interact with ECR. This Action will push an image in the repository to our private ECR repository.
Set deny AssumeRole on all resources:
aws iam put-role-policy \
--role-name GithubActionsRole \
--policy-name OidcSafetyPolicy \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "OidcSafeties",
"Effect": "Deny",
"Action": [
"sts:AssumeRole"
],
"Resource": "*"
}
]
}'
ECR permissions:
aws iam put-role-policy \
--role-name GithubActionsRole \
--policy-name GithubActionsDeployPolicy \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage"
],
"Resource": "arn:aws:ecr:us-east-1:<AWS_ACCOUNT_ID>:repository/<ECR_REPOSITORY_NAME>"
}
]
}'
You can adjust this second policy to match your needs, however I would recommend always setting up the STS deny policy. We now have all the IAM pieces in place to run our workflow for the AWS side of the house.
Setting up OIDC Provider with GCP
Next, we will set up the OIDC provider in GCP. As I mentioned above, GCP and AWS approach IAM differently. In GCP, we create a service account and attach roles to it. We’ll use the GCP CLI gcloud for these operations.
First, create a service account:
gcloud iam service-accounts create github-actions\
--description="Service Account for GitHub Actions" \
--display-name="GitHub Actions SA"
Next, create a Workload Identity Pool to manage external providers like GitHub:
gcloud iam workload-identity-pools create POOL_ID --location="global" --display-name="My workload pool"
You can view your new service account and identity pool in the GCP IAM console under the Service Accounts and Workload Identity Federation tabs respectively.
Now, create the OIDC IdP for GitHub in the pool:
gcloud iam workload-identity-pools providers create-oidc PROVIDER_ID \
--location="global" \
--workload-identity-pool="POOL_ID" \
--display-name="GitHub Actions Provider" \
--attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \
--issuer-uri="https://token.actions.githubusercontent.com" \
--allowed-audiences="https://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID" \
--attribute-condition="assertion.repository_owner == \"<GITHUB_ORGANIZATION\""
In GCP, we create attribute mappings and conditions directly in the CLI, serving the same function as the Trust Relationship in AWS. Note that we use the Google API endpoint as the audience, unlike in AWS where we set it to the STS service.
To allow the workload identity provider to impersonate the service account, grant it the roles/iam.workloadIdentityUser role:
gcloud iam service-accounts add-iam-policy-binding github-actions@PROJECT_ID.iam.gserviceaccount.com \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/attribute.repository/GITHUB_ORG/GITHUB_REPO"
Verify this in the GCP IAM console under the Workload Identity Federation tab. Click on your workload identity pool, then “CONNECTED SERVICE ACCOUNT” in the right-hand pane. You should see your github-actions service account with the attribute repo_org=”GITHUB_ORG/GITHUB_REPO” set.
Finally, set the appropriate permissions for your service account to push images to Artifact Registry:
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:github-actions@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/storage.admin"
With all AWS and GCP components in place, we can now build our GitHub Actions workflow to tie everything together.
Putting it all together with GitHub Actions
Reusable workflows allow organizations to standardize deployment processes across multiple repositories, reducing duplication and ensuring consistent configuration. In your GitHub repository where your Actions workflow will live, create your workflow YAML file in the `.github/workflows/` directory. Below, I’ll decompose the workflow.yml file into its constituent parts for easier digestion.
Here I have set my workflow to be callable, making it a reusable workflow that can be referenced throughout your organization using the `uses` clause.
I’ve set up secrets and inputs, which are managed in the Settings tab of your repository.
To retrieve the WORKLOAD_IDENTITY_PROVIDER, run the following gcloud command:
gcloud iam workload-identity-pools providers describe "github" \
--project="PROJECT_ID" \
--location="global" \
--workload-identity-pool="POOL_ID" \
--format="value(name)"
name: Deploy to ECR and connect to GCP
on:
workflow_call:
secrets:
AWS_ACCOUNT_ID:
required: true
GCP_SERVICE_ACCOUNT:
required: true
GCP_PROJECT_ID:
required: true
WORKLOAD_IDENTITY_PROVIDER:
required: true
inputs:
aws-region:
required: true
type: string
ecr-repository:
required: true
type: string
Our first job will configure the GitHub Action to authenticate with your AWS OIDC IdP Role, log in to ECR build and push our image to the registry.
jobs:
deploy-to-ecr:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GithubActionsRole
aws-region: ${{ inputs.aws-region }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, and push image to Amazon ECR
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: ${{ inputs.ecr-repository }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
Our second job will authenticate with GCP using our OIDC Workload IdP, configure Docker for Artifact Registry, build and push our image to the Registry.
deploy-to-gcp:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Authenticate to GCP
uses: 'google-github-actions/auth@v2'
with:
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}
- name: Configure Docker for GCR
run: gcloud auth configure-docker
- name: Build and push to GCR
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: gcr.io/${{ secrets.GCP_PROJECT_ID }}/your-image-name:${{ github.sha }}
As I mentioned above this is a reusable workflow and as such it can be called from other workflows making this a streamlined process throughout your organization. Here is what an example caller workflow would look like.
name: Caller Workflow
on:
workflow_dispatch:
jobs:
call-reusable-workflow:
permissions:
id-token: write
contents: read
uses: ./.github/workflows/reusable-workflow.yml
with:
aws-region: "us-east-1"
ecr-repository: "repo/image"
secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
GCP_SERVICE_ACCOUNT: ${{ secrets.GCP_SERVICE_ACCOUNT }}
GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}
Wrap-Up
There you have it! You can now streamline security by implementing OIDC, the industry standard for professional development. We’ve walked through the step-by-step process for both AWS and GCP, demonstrating the versatility of this approach across major cloud platforms. By automating this process with GitHub Actions, you can enhance efficiency and reduce manual errors. Moreover, by creating a reusable workflow, we’ve ensured consistency and scalability across projects, preventing future issues and promoting best practices. This approach not only improves security but also simplifies authentication processes, making your CI/CD pipelines more robust and manageable. I hope you’ve gained valuable insights that you can implement in your projects today!