Continuing the series on GitHub custom images, I want to build on the previous example. In that post, I showed how a pre-job script can validate which workflows get to use a runner. Now let’s take the next step: use OpenID Connect (OIDC) authentication before the job starts.
One example of where this can come in handy is when your Actions workflow relies on job containers. Job containers let you specify an OCI image, and GitHub uses that image to create a container that will host your job steps.
If your image lives in a private registry, you need to authenticate to the registry before GitHub can pull it. Normally, that means a username and password. Of course, since you’re security conscious, you prefer to use passwordless authentication with OIDC tokens rather than long-lived credentials. It helps that most cloud providers support using OIDC tokens for registry authentication. Unfortunately, a few constraints get in the way:
- The workflow syntax does not support OIDC authentication for container registries; it just allows username/password authentication.
- You can’t use a previous job to do the OIDC authentication. Federated credentials are tied to the lifecycle of the job where they are requested. You also can’t pass secrets between jobs (for good reasons).
- The authentication needs to happen before the job starts, since the runner isn’t able to execute any steps until the container is created.
We can work around this issue with a custom image. Instead of authenticating inside the job, you can configure the runner to authenticate when the job starts, before any steps run.
Getting the GitHub token
For this example, assume your goal is to provide access to a private container registry that supports OIDC authentication.
To make this work, each workflow needs to include id-token: write in the job’s permissions. That lets the job request an OIDC token from GitHub’s identity provider. Once you set this permission, the pre-job script in your custom image can request an OIDC token for the current job context.
1permissions:
2 id-token: writeAs I mentioned in Understanding OIDC and Identity Federation, Actions exposes environment variables you can use to request a token. Your workflow can use those during image generation to access protected resources. For this scenario, you will use a pre-job script to get a token for the workflow job that is assigned to the runner.
You can use a command like this to retrieve a token from the GitHub identity provider:
1curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL" -H "Accept: application/json; api-version=2.0" -H "Content-Type: application/json" -d "{}" | jq -r '.value'The next step is to do a token exchange with your resource provider to get an access token.
Getting a JFrog access token
If you’re using JFrog Artifactory as your private container registry, you can exchange the GitHub OIDC token for a JFrog access token. Here’s an example of how to make that request (based on their documentation):
1PAYLOAD=$(jq -n --arg jwt "$JWT" \
2 '{
3 "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
4 "subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
5 "subject_token": $jwt,
6 "provider_name": "github-oidc"
7 }')
8
9curl -X POST -H "Content-type: application/json" \
10 "https://${JFROG_URL}/access/api/v1/oidc/token" \
11 -d "$PAYLOAD"You can then use the returned access token as the password for a docker login command.
Getting an Azure Container Registry access token
Azure is a bit more complex. I covered how to do a token exchange for an Entra ID access token in Understanding OIDC and Identity Federation. Once you have an Entra ID access token, you need to exchange it for an Azure Container Registry (ACR) refresh token. Then, the refresh token is used to get an access token for the registry.
The REST calls required to do this are documented on Microsoft Learn, and Microsoft provides a complete walkthrough of getting the credentials programmatically. Of course, there’s an easier way if you have access to the Azure CLI. The CLI can handle the entire token exchange process for you. Here’s what that can look like in a pre-job script:
1az login --service-principal --username "${CLIENT_ID}" --tenant "${TENANT_ID}" --federated-token "${JWT}"
2az account set --subscription "${SUBSCRIPTION_ID}"
3az acr login --name "${ACR_REGISTRY}"The last command will perform the Entra ID token exchange, retrieve the access token, and call docker login (passing 00000000-0000-0000-0000-000000000000 as the username and using the token as the password).
What about the job container?
To use a job container, simply specify the image name in the job definition:
1test:
2 runs-on: larger-runner-acr
3 container: myregistry.azurecr.io/my-image:v1Under the covers, the runner will call the Docker CLI to start your job container. Since the pre-job script has already authenticated to the registry, the image pull will succeed without needing any further authentication:
1▼ Create local container network
2 /usr/bin/docker network create --label 4c177b github_network_2bf28bb641f84bd28e4188548d6451b9
3 87ccf0d831801224b1ad4379afb62648c06fd61ecf5a8043ff1495aa56adc9bd
4▼ Starting job container
5 /usr/bin/docker pull myregistry.azurecr.io/my-image:v1
6 Digest: sha256:9dc159af07be6f78d586ba6f3ab67fc8ff115a76710427d0699be30068671103Now jobs on this runner can access container images from a private registry based on claims specific to the workflow. In addition to not needing a static credential, each job is forced to request just-in-time access, giving you an extra layer of security.
Don’t forget the cleanup
When the job is done, call docker logout to remove cached credential details from the runner. If your provider supports it, you should also revoke the access token. Both of these can be handled by your post-job script.
Why bother doing this if the runner is ephemeral? If the token becomes compromised during the job, this kind of cleanup limits how long the token remains useful.
Wrapping up
Hopefully this gives you a few ideas for using OIDC tokens to avoid long-lived credentials. You get the security benefits of short-lived tokens while still working within the constraints of GitHub Actions’ job container authentication. This pattern works whether you’re using JFrog Artifactory, Azure Container Registry, or another OIDC-capable provider.
When you combine pre-job scripts for authentication with post-job scripts for cleanup, you get a clean lifecycle for runner credentials. Pair that with the workflow validation techniques from the previous post and you have a solid foundation for managing access to your runners and the resources they use (in the cloud or on premises!).
As you implement this in your own environment, remember that the security of this approach depends on careful configuration of your OIDC claims and permissions. Take the time to understand what your provider validates and ensure those validations align with your security requirements. With that foundation in place, you’ll have a flexible and secure way to apply zero-trust principles when accessing resources in your GitHub Actions workflows.
