December 21, 2023

How to Use OpenPubkey with GitHub Actions Workloads

Ethan Heilman, CTO, BastionZero

James Carnegie, Principal Sofware Engineer, Docker, Inc.

OpenPubkey is the web's new technology for adding public keys to standard SSO interactions with Identity Providers (IdPs) that speak OpenID Connect (OIDC). OpenPubkey works by essentially turning an IdP into a Certificate Authority (CA). A CA is a trusted entity that issues certificates that cryptographically bind an identity with a cryptographic public key. With OpenPubkey, any OIDC-speaking Identity Provider can bind public keys to identities today.

OpenPubkey is newly open-sourced through a collaboration of BastionZero, Docker and the Linux Foundation. We’d love for you to try it out, contribute, and build your own use cases on it.  Check out the repository here!

In this post, our goal is to show you how to use OpenPubkey to bind public keys to workload identities. We’ll concentrate on Github Actions workloads, because this is what is currently supported by the OpenPubkey open source project.  We’ll also briefly cover how Docker is using OpenPubkey with Github Actions to sign in-toto attestations on Docker Official Images and improve supply chain security.

What’s an ID Token?

Before we start, let’s review the OpenID Connect protocol. Identity Providers that speak OpenID Connect (OIDC) are usually called OpenID Providers, so we will just call them OPs in the rest of this post.

OIDC has an important artifact called an ID Token.  A user obtains an ID Token after she completes her Single Sign On (SSO) to her OP.  She can then present the ID Token to a third-party service to prove that she has properly been authenticated by her OP.  

The ID Token includes the user’s identity (like her email address) and is cryptographically signed by the OP. The third-party service can validate the ID Token by querying the OP’s JWKS endpoint, obtaining the OP’s public key, and then using the OP’s public key to validate the signature on the ID Token.  The OP’s public key is available by querying an JSON Web Key Set (JWKS) endpoint hosted by the OP.


How do Github Actions obtain ID tokens?

So far we’ve been talking about human identities (e.g. email addresses) and how they are used with ID Tokens.   But our focus in this blog is on workload identities.

It turns out that Github Actions has a nice way to assign ID Tokens to GitHub Actions.  

Here’s how it works. Github runs an OpenID Provider (OP).  When a new GitHub Action is spun up, GitHub first assigns it a fresh API key and secret.   The Github Action can then use its API key and secret to authenticate to Github’s OP.    GitHub’s OP can validate this API key and secret (because it knows that it was assigned to the new GitHub Action) and then provides the GitHub Action with an OIDC ID Token.  This Github Action can now use this ID Token to identify itself to third party services.

When interacting with Github’s OP,  Docker uses the job_workflow_ref claim in the ID Token as the workflow’s “identity”.  This claim identifies the location of the file that the Github Action is built from, so it allows the verifier to identify the file that generated the workflow and thus also understand and check the validity of the workflow itself.  Here’s an example of how the claim could be set:  job_workflow_ref = octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main

There are other claims in the ID Tokens issued by Github’s OP that could be useful in other use cases. For instance, there is a field called Actor or ActorID is the identity of the person who kicked off the Github Action.  This could be useful for checking that workload was kicked off by a specific person. (But less useful when the workload is kicked off by an automated process.)

Githubs OP supports many other useful fields in the ID Token.  You can learn more about these fields in the Github OIDC documentation.

Creating a PK Token for workloads.

Now that we’ve seen how to identify workloads using Github’s OP, we will see how to bind that workload identity to its public key with OpenPubkey.  OpenPubKey does this with a cryptographic object called the PK Token.

To understand how this works, let’s go back and look at how Github’s OP implements the OpenID Connect protocol (OIDC). The ID Tokens generated by Github’s OP have a field called audience. Importantly, unlike user identity in worload identity the audience field is chosen by the OIDC client that requests the ID Token. When Github’s OP creates the ID Token, it includes the audience along with the other various fields (like job_workflow_ref and actor) that the OP signs when it creates the ID Token.

So, in OpenPubkey, the Github Action workload runs an OpenPubkey client that first generates a new public-private key pair. Then, when the workload authenticates to Github’s OP with OIDC, it sets the audience field equal to the cryptographic hash of the workload’s public key along with some random noise.

Now the ID Token contains the Github OP’s signature on the workload’s identity (the job_workflow_ref field and other relevant fields) AND on the hash of the workload’s public key.  This is most of what we need in order to have Github’s OP bind the workload’s identity and public key!

In fact, the PK Token is a JSON Web Signature (JWS) which roughly consists of:


☑️ the ID Token, including the audience field, which contains a hash of the workload’s public key.
☑️ The workload’s public key.
☑️ The random noise used to compute the hash of the workload’s public key.
☑️ A signature, under the workload’s public key, of all the information in the PK Token. (This signature acts as a cryptographic proof that the user has access to the user-held secret signing key that is certified in the PK token.)

The PK token can then be presented to any OpenPubkey verifier, which uses OIDC to obtain the Github OP’s public key from its JWKS URI. The verifier then verifies the ID Token using the Github OP public key, and then verifies the rest of the other fields in the PK token using the workload’s public key. Now the verifier knows the public key of the workload (as identified by its job_workflow_ref or other fields in the ID Token), and can use this public key for whatever cryptography it wants to do.


Can you use ephemeral keys with OpenPubkey?

Yes! An ephemeral key is a key that is only used for a short period of time. Ephemeral keys are nice because there is no need for long term management of the private key, it can simply be deleted when it is no longer needed, this improves security and reduces operational overhead.

Here’s how to do this with OpenPubkey. You simply choose a public-private key pair, then authenticate to the OP to obtain a PK Token for the public key, then sign your object using the private key, and finally throw away the private key.  

One-time-use PK Token. In fact, we can take this one step further and ensure the PK Token may only be associated with a single signed object. Here’s how it works. First, we take a hash of the object to be signed. Then, when the workload authenticates to Github’s OP, sets the audience claim to equal to the cryptographic hash of the following items:

☑️ The public key

☑️ The hash of the object to be signed

☑️ Some random noise

Finally, OpenPubkey verifier obtains the signed object and its one-time-use PK Token, and then validates the PK Token by additionally checking that the hash of the signed object is included in the audience claim. Now you have a one-time-use PK Token!  Learn more about this feature of OpenPubkey here.

How will Docker use OpenPubkey to sign Docker Official Images?

Docker will be using OpenPubkey with Github Actions workloads to sign in-toto attestations on Docker Official Images (DOI).  Docker Official Images will be created using a Github Action workload. The workload creates a fresh ephemeral public-private key pair, obtains the PK Token for the public key via OpenPubkey, and finally signs attestations on the image using the private key.  

The private key is then deleted, and the image, its signature, and the PK Token will be made available in the Docker Hub container registry.   This approach is nice because it doesn’t require the signer to maintain or store the private key.

Docker’s container signing use case also relies heavily on a The Updated Framework (TUF), another Linux Foundation open source project. Check out this blog post for more details on how this all works.

What else can you do with OpenPubkey and Github Actions workloads?


Here are a few ideas.

Signing private artifacts with a one-time key: Consider signing artifacts that will be stored in a private repository.   You can use OpenPubkey if you want to have a Github Action cryptographically sign an artifact using a one-time-use key.  One nice thing about this approach is that it doesn’t require you to expose information in a public repository or transparency log. Instead, you just need to post the artifact, its signature, and its PK Token in the private repository.   This is useful for private code repositories or internal build systems where you don’t want to reveal to the world what is being built, by who, when, or how frequently.

If relevant, you could also consider using the actor and actor-ID claim to bind the human who builds a particular artifact to the signed artifact itself.

Authenticating workload-to-workload communication. Suppose you want one workload (call it Bob) to process an artifact created by another workload (call it Alice).  If the Alice workload is a Github Action, the artifact it creates could be signed using OpenPubkey and passed on to the Bob workload, which uses an OpenPubkey verifier to verify it using the Github OP’s public key (which it would obtain from the Github OP’s JWKS url).  This approach might be useful in a multi-stage CICD process.

And other things too! But these are just strawman ideas. The whole point of this post is for you to try out OpenPubkey, contribute, and build your own usecases on it.

How do I get started?

Here’s the OpenPubkey open-source repository on Github, please go take a look!  

We also have community meetings that you can attend to discuss your usecases and requirements, or join us on the OpenPubkey channel on the OpenSSF slack.

Other technical issues we need to think about.

Before we wrap up, there are a few technical questions we need to discuss.

Aren’t ID Tokens supposed to remain private?

You might worry about applications of OpenPubkey where the ID Token is broadly exposed to the public inside the PK Token. For example, in Docker Official Image signing use case, the PK Tokens are made available to the public in the Docker Hub container registry. If the ID Token is broadly exposed to the public, there is a risk that the ID Token could be replayed and used for unauthorized access to other services.  

This is why we have a slightly different PK Token for applications where the PK Token is made broadly available to the public.

For those applications, OpenPubkey strips the OP’s signature from the ID Token before including it the PK token. The OP’s signature is replaced with an Guillou-Quisquater (GQ) non-interactive proof-of-knowledge for an RSA signature (which, for short, is also known as a “GQ signature”). Now, the ID Token cannot be replayed against other services, because the OP’s signature is removed but the security of the OP’s RSA signature is maintained by the GQ Signature.   

So, in applications where the PK Token must be broadly exposed to the public, the PK Token is a JSON Web Signature (JWS) which consists of:

☑️ The ID Token excluding the OP’s signature

☑️ A GQ signature on the ID Token

☑️ The user’s public key

☑️ The random noise used to compute the hash of the user’s public key

☑️ A signature, under the user’s public key, of all the information in the PK token

The GQ signature allows the client to prove that the ID Token was validly signed by the OP, without revealing the OP’s signature. The OpenPubkey client generates the GQ signature to cryptographically prove that the client knows the OP’s signature on the ID Token, while still keeping the OP’s signature secret. GQ signatures only work with RSA, but this is fine because every OpenID Connect provider is required to support RSA.

Because GQ signatures are larger and slower than regular signatures, we recommend using them only for use cases where the PK Token must be made broadly available to the public. BastionZero’s infrastructure access use case does not use GQ signatures because it does not require the PK Token to be made public. Instead, the user only exposes their PK Token to the target (e.g. server, container, cluster, database) that they want to access; this is the same way an ID Token is usually exposed with OpenID Connect.   GQ signatures might not be necessary when authenticating workload-to-workload communications; if the Alice workload is passing the signed artifact and its PK Token to the Bob workload only, there is less of a concern as the PK Token is broadly available to the public.

What happens when the OP rotates its OpenID Connect key?

OPs have OpenID Connect signing keys that change over time (e.g. every two weeks). What happens if we need to use a PK Token after the OP rotates OpenID Connect key that signed the PK Token?

For some use cases, the lifetime of a PK Token is short. With BastionZero’s infrastructure access use case, for instance, a PK Token will not be used for longer than 24 hours. In this use case, these timing problems are solved by (1) having user re-authenticate to the IdP and create a new PK Token whenever the IdP rotates it’s key, and (2) having the OpenPubkey verifier check that the client also has a valid OIDC Refresh Token along with the PK Token whenever the ID Token expires.

For some use cases, the PK Token has a long life, so we do need to worry about OP rotating their OpenID Connect keys. With Docker’s attestation signing use case, this problem is solved by having the TUF additionally store a historical log of the OP’s signing key. Anyone can keep an historical log of the OP public keys for use after they expire.  

In fact, we envision a future where OP’s might keep this historical log themselves.

That’s it for now! Check out the OpenPubkey repo here. We’d love for you to join the project, contribute, and identify other use cases where OpenPubkey might be useful.

Connect with our OpenPubkey experts!

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
How to Use OpenPubkey with GitHub Actions Workloads

See BastionZero in Action

BastionZero connects teams to resources and requires no additional infrastructure to deploy or manage. It is the first—and only—cloud-native solution for trustless access providing multi-root authentication while maintaining zero entitlements to your systems.

With BastionZero, you can reclaim your architecture from over-privileged third parties and ensure that the right people have access to the right resources at just the right time—every time.

Schedule a demo now to see how you can trust less and access more with BastionZero.

Sign up for the BastionZero newsletter

We talk about zero trust, remote access, threat intel, and more!

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Future-proof your cloud security strategy

Try BastionZero for free today and see why fast-growing companies trust us over any other identity provider.