Security is top of mind for us all in software development. My colleague, Mark Paulsen, recently shared a number of examples to mitigate OWASP vulnerabilities while maintaining your developer experience and productivity.
The security of the applications we’re building is important. But we also need to consider the security of the hosting environments that we’re deploying to. When deploying somewhere, you typically need to provide several pieces of information to enable the deployment to take place. Think of your cloud provider. You may need to be authenticated using a service principal, authorized using role-based access control, and the name of a project or an ID to a subscription, or some additional resource metadata.
And here lies the challenge. You will typically have tens, possibly hundreds of service principals depending on the size of your application environment (also assuming you’ve adopted the principle of least privilege). Each of those service principals would have its own password or certificate, which would then be used to authenticate to the cloud provider.
Cryptographic failures is in position #2 of the OWASP 2021 Top 10 list (which would encompass scenarios like secret or certificate leaks, weak passwords, and hardcoded passwords). IBM’s Cost of a data breach 2022 report explains that stolen/lost credentials were the most common cause of a data breach and also took the longest to identify.
So, what happens if one of your passwords or certificates leaks? GitHub Advanced Security’s secret scanning can identify exposed secrets in your codebase. With push protection, you can be proactive and prevent secrets from being committed in the first place. In case you missed it, push protection now also covers custom patterns which you have defined!
But what about the best practice of regularly rotating the passwords/certificates associated with each of those service principals? How do you keep references of those secret materials up to date in your CI/CD tool? There is a fair amount of operational complexity involved in just maintaining secrets and access between your tools of choice.
From a Site Reliability Engineering (SRE) perspective, this overhead could be considered as toil based on the characteristics defined in Google’s Site Reliability engineering book. Toil is unavoidable. But in the context of this blog post, we have the opportunity for optimization. While there are tools like Hashicorp Vault, which can help you organize, automate and maintain your secrets, wouldn’t it be better if you could avoid the problem overall. What if you didn’t have to use secrets to deploy to your preferred cloud provider?
Fortunately, that’s something we’ve been working on at GitHub. Back in 2021, we announced that GitHub enables you to deploy to your cloud provider using OpenID Connect. In this post, I’ll provide you with a deeper overview of this functionality and how it can reduce operational complexities by removing the need for passwords.
What is OpenID Connect (OIDC)?
Let’s start by making sure we’re all on the same page. Open ID Connect is an authentication protocol, built on top of the OAuth 2.0 framework (an authorization protocol). An ID token is usually returned from an authorization endpoint by using a sign-on flow.
This ID token is served in the JSON Web Token (JWT) standard, and typically digitally signed. As a result, this token can be used to verify the identity of the caller, and retrieve additional claims (think of these as additional properties, or statements about the entity) as well.
You can find an example of an ID token returned from GitHub Actions below:
{
"typ": "JWT",
"alg": "RS256",
"x5t": "example-thumbprint",
"kid": "example-key-id"
}
{
"jti": "example-id",
"sub": "repo:octo-org/octo-repo:environment:prod",
"environment": "prod",
"aud": "https://github.com/octo-org",
"ref": "refs/heads/main",
"sha": "example-sha",
"repository": "octo-org/octo-repo",
"repository_owner": "octo-org",
"actor_id": "12",
"repository_visibility": private,
"repository_id": "74",
"repository_owner_id": "65",
"run_id": "example-run-id",
"run_number": "10",
"run_attempt": "2",
"actor": "octocat",
"workflow": "example-workflow",
"head_ref": "",
"base_ref": "",
"event_name": "workflow_dispatch",
"ref_type": "branch",
"job_workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main",
"iss": "https://token.actions.githubusercontent.com",
"nbf": 1632492967,
"exp": 1632493867,
"iat": 1632493567
}
In the context of this blog post, we can use OpenID Connect in GitHub Actions to generate an ID token for us. This token is signed by GitHub and provides claims on the context of the workflow being executed (for example, the repository details, run number, actor that called the workflow, etc.).
As a result, a cloud provider can then use this ID token to verify the authenticity of a request, allowing a ‘trade’ of the GitHub ID token for a short-lived access token.
Without OpenID Connect, you would typically have to pass in some credentials to your CI/CD tool, so that it can authenticate to your cloud provider.
GitHub Actions uses OpenID Connect to enable a workflow to authenticate against the cloud provider directly, without needing to use a password or a certificate. Instead, the access token from the cloud provider can be used.
Throughout this process, you are effectively establishing a ‘trust’ between GitHub and a service principal in your cloud provider. In AWS, you would add an OIDC provider to IAM, in Azure a ‘Federated Identity Credential’ and then ‘Workload Identity Federation’ in GCP.
Setting up your GitHub Action Workflow for OIDC
Whenever you execute a GitHub Action workflow run, a GitHub Token is created. You may have already referenced this token in your existing workflows using the ${{ secrets.GITHUB_TOKEN }}
expression. The GITHUB_TOKEN
is typically used to gain access to the needed parts of GitHub for your automation’s needs.
For example, if your workflow is publishing a new package, then you may need write permissions to GitHub Packages. If you’re adding a comment to a GitHub Issue, then you would need write permissions to issues. Check out the GitHub docs for full details on permissions for the GITHUB_TOKEN.
To generate a GitHub OIDC ID token within your workflow, you’ll need to explicitly give the GITHUB_TOKEN
permission to do this. This is done by setting the permissions for the id-token to write, as demonstrated in the snippet below.
permissions:
id-token: write # This is required for requesting the JWT
This permission can be set either at the overall workflow level, or an individual job level. This will depend on where you need to use the token in your workflow (that is, across multiple jobs, or just one job— remember, principle of least privilege—only give the GITHUB_TOKEN
the access it needs!).
Once this step is complete, your GitHub Action workflow will be capable of requesting the OIDC ID token, as outlined in the next section.
Authenticating to the cloud provider using the GitHub OIDC token
If you already use GitHub Actions to deploy to the cloud, then you may be aware that there are several GitHub Actions that you can use to authenticate to your cloud provider:
Note: while several cloud providers have GitHub Actions that support OIDC authentication, it’s possible to create a custom action for those providers which do not have an official GitHub action that supports this approach. You can find out more about the process in the GitHub docs. |
GitHub Actions typically have multiple properties that can be set, so you need to consider the appropriate configuration for your cloud provider’s action. When configured to use OpenID Connect authentication, the GitHub Action will generate the GitHub ID token, and send that to the cloud provider to be exchanged for the access token to the cloud provider). This can then be used in the later steps of your workflow (for example, additional GitHub Actions or your own scripts), to perform authenticated steps against the cloud provider.
See an example below of logging in to Azure using OIDC:
name: Login to Azure and execute the Azure CLI
on: [push]
permissions:
id-token: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: 'Login to Azure using OIDC'
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: 'List the Azure Resource Groups'
run: |
az group list
There are a few points to note about the above example:
- The id-token permission is set to write at the workflow level. If additional jobs were added, then they would also be able to retrieve a GitHub ID token. This could have been configured at the job level, beneath deploy instead. If the id-token permission was not explicitly set to write, then the login step would fail, as the workflow would be unable to retrieve the GitHub ID token.
- The Azure/login step is used to authenticate to Azure. In this configuration, a Client ID, Tenant ID and Subscription ID are properties set on the action. Notice that a password/certificate is not provided.
- Some may consider the ID of the service principal, Azure Active Directory tenant, and Azure Subscription as sensitive information. They are passed in using GitHub Secrets, which would then mask those values if they are outputted in the workflow logs.
- The Azure/login step retrieves the GitHub ID token. It then sends the GitHub ID token to Azure, along with the Service Principal Client ID, Tenant ID and Subscription ID. Azure then validates whether this specific workflow is ‘allowed’ access. If allowed, then the access token will be sent back to the workflow. Otherwise, the login step will fail, and the workflow will fail.
- The command line is then used to list the Azure Resource Groups in the subscription that the above service principal has access to.
Note: the configurable properties for each GitHub Action are set by the owner of the action. While client-id, tenant-id and subscription-id are used for the Azure/login step, these are not the same for actions from the other cloud providers. Make sure to familiarize yourself with the appropriate action for your cloud provider, and the recommended configuration. |
Now, let’s take stock. At this point, you have a GitHub Actions workflow which is capable of generating a GitHub ID token. You can then use a GitHub Action from one of the cloud providers to take the ID token, and exchange it for a short-lived access token. This access token can then be used (based on the role-based access control permissions you have configured on the cloud provider) to execute your workflow steps.
We aren’t using passwords! No longer do we need to worry about rotating certificates, or passwords. Instead, we rely upon the OpenID Connect protocol, and the trust between GitHub and our cloud provider to provide a short-lived access token for use in the workflow. This takes us one step closer to a passwordless world, being able to deploy to our cloud provider without passing a password or certificate!
Written by
Chris is a passionate developer advocate and senior program manager in GitHub’s Developer Relations team. He works with execs, engineering leads, and teams from the smallest of startups, established enterprises, open source communities and individual developers, helping them ❤️ GitHub and unlock their software engineering potential.