OIDC in Buildkite Package Registries

Open ID Connect (OIDC) is an authentication protocol based on the OAuth 2.0 framework. With OIDC, one system or service issues an OIDC token, which is a signed JSON Web Token (JWT) containing metadata (or claims) about a user or object. This token can be consumed by another service (which may be offered by a third-party or by the same organization) to authenticate the user or object. An OIDC policy configured on this other service defines which OIDC tokens, based on their claims (also known as asserted claims) are permitted to perform the actions. If the OIDC token's asserted claims comply with those of the OIDC policy configured in the other service, the token is authenticated and the service issuing the token is permitted to perform its actions on the other service.

You can configure Buildkite registries with OIDC policies that allow access using OIDC tokens issued by Buildkite Agents and other OIDC identity providers. This is similar to how third-party products and services can be configured with OIDC policies to consume Buildkite Agent OIDC tokens for specific pipeline jobs, for deployment, or access management and security purposes.

A Buildkite Agent's OIDC tokens assert claims about the slugs of the pipeline it is building and organization that contains this pipeline, the ID of the job that created the token, as well as other claims, such as the name of the branch used in the build, the SHA of the commit that triggered the build, and the agent ID. If the token's claims do not comply with the registry's OIDC policy, the OIDC token is rejected, and any actions attempted with that token will fail. If the claims do comply, however, the OIDC token will have read and write access to packages in the registry.

The Buildkite Agent's oidc command allows you to request an OIDC token from Buildkite containing claims about the pipeline's current job. These tokens can then be used by a Buildkite registry to determine (through its OIDC policy) if the organization, pipeline and any other metadata associated with the pipeline and its job are permitted to publish/upload packages to this registry.

OIDC token requirements

All Buildkite registries defined with an OIDC policy, require the following claims from an OIDC token (unless indicated as optional), regardless of the OIDC identity provider that issued the token.

Claim Value
iat (issued at) Must be a UNIX timestamp in the past.
nbf (not before) (Optional) If present, must be a UNIX timestamp in the past.
exp (expiration time) Must be a UNIX timestamp in the future. The OIDC token's lifespan—that is, the exp minus the iat timestamp values—cannot be greater than 5 minutes.
aud (audience) Must be equal to the registry's canonical URL, which has the format https://packages.buildkite.com/{org.slug}/{registry.slug}.

When generating an OIDC token from:

  • A Buildkite Agent, the --audience option must explicitly be specified with the required value, whereas iat, nbf and exp claims will automatically be included in the token.

  • Another OIDC identity provider, ensure that its OIDC tokens contain these required claims. This should be the case by default, but if not, consult the relevant documentation for your OIDC identity provider on how to include these claims in the OIDC tokens it issues.

Define an OIDC policy for a registry

You can specify an OIDC policy for your Buildkite registry, which defines the criteria for which OIDC tokens, from the Buildkite Agent or another OIDC identity provider, will be accepted by your registry and authenticate a package publication/upload action from that system.

To define an OIDC policy for one or more Buildkite pipeline jobs in a registry:

  1. Select Packages in the global navigation to access the Registries page.

  2. Select the registry whose OIDC policy needs defining.

  3. Select Settings > OIDC Policy to access the registry's OIDC Policy page.

  4. In the Policy field, specify this using the following Basic OIDC policy format, or one based on a more complex example.

Learn more about how an OIDC policy for a registry is constructed in Policy structure and behavior.

Basic OIDC policy format

The basic format for a Buildkite registry's OIDC policy, to handle OIDC tokens issued by a Buildkite Agent is:

- iss: https://agent.buildkite.com
  claims:
    organization_slug: organization-slug
    pipeline_slug: pipeline-slug
    build_branch: main

where:

  • iss (the issuer) must be https://agent.buildkite.com, representing the Buildkite Agent.
  • organization-slug can be obtained from the end of your Buildkite URL, after accessing Packages or Pipelines in the global navigation of your organization in Buildkite.
  • pipeline-slug can be obtained from the end of your Buildkite URL, after accessing Pipelines in the global navigation of your organization in Buildkite.
  • main or whichever branch of the repository you want to restrict package publication/uploads from pipeline builds.

However, more complex OIDC policies can be created.

Complex OIDC policy example

The following OIDC policy for a Buildkite registry contains two statements—one for a registry in Package Registries and another for GitHub Actions.

- iss: https://agent.buildkite.com
  claims:
    organization_slug:
      equals: your-org
    pipeline_slug:
      in:
        - one-pipeline
        - another-pipeline
    build_branch:
      matches:
        - main
        - feature/*
      not_equals: feature/not-this-one

- iss: https://token.actions.githubusercontent.com
  claims:
    repository:
      matches: your-org/*
    actor:
      in:
        - deploy-bot
        - revert-bot

The first statement allows OIDC tokens representing a pipeline's job being built by a Buildkite Agent, but only when all of the following is true for the tokens' claims:

The second statement allows OIDC tokens representing a GitHub Actions workflow, but only when all of the following is true for the tokens' claims:

  • The repositories match your-org/*
  • The actor is either deploy-bot or revert-bot

Policy structure and behavior

OIDC policy statements in Buildkite Package Registries are defined as a YAML- or JSON-formatted list, each of which includes a token issuer from an OIDC identity provider, along with a map of claim rules.

If an OIDC token's claims match both the token issuer and all claim rules defined by any statement within a registry's OIDC policy, then the token is accepted and the OIDC identity provider that issued the token is granted access to the registry. If no statements of the OIDC policy match, the token is rejected, and no registry access is granted.

When using YAML to define an OIDC policy, only simple YAML syntax is accepted—that is, YAML containing only scalar values, maps, and lists. Complex YAML syntax and features, such as anchors, aliases, and tagged values are not supported.

Statements

A statement defines a list of claim rules for a particular token issuer within an OIDC policy, where a token issuer is typically determined by an OIDC identity provider.

Each statement in the policy must contain contain a token issuer (iss) field, whose value is determined by the OIDC identity provider, and permits OIDC tokens from that token issuer. While multiple statements are typically used to allow access from multiple token issuers (that is, one statement per issuer), more than one statement can also be defined for a single issuer or OIDC identity provider to handle more complex claim rule scenarios.

A statement must also contain a claims field, which is a map of claim rules.

Currently, only OIDC tokens from the following token issuers are supported.

Token issuer name The token issuer (iss) value Relevant documentation link
Buildkite https://agent.buildkite.com Buildkite Agent oidc command
GitHub Actions https://token.actions.githubusercontent.com GitHub Actions OIDC Tokens
CircleCI https://oidc.circleci.com/org/$ORG where $ORG is your organization name CircleCI OIDC Tokens

If you'd like to use OIDC tokens from a different token issuer or OIDC identity provider with Buildkite Package Registries, please contact support.

Claim rules

A statement contains a claims field, which in turn contains a map of claim rules, where the rule's key is the name of the claim being verified, and the rule's value is the actual rule used to verify this claim. Each rule is a map of matchers, which are used to match a claim value in an OIDC token.

If at least one claim rule defined within an OIDC policy's statement is missing from an OIDC token and no other statements in that policy have complete matches with the token's claims, then the token is rejected. When a claim rule contains multiple matchers—such as the build_branch claim rule in the complex example above—all of the rule's matchers must match a claim in the token for it to be granted registry access. In the build_branch example above, this means that the token must have a build_branch claim whose value is either main or begins with feature/, but whose value is not feature/not-this-one.

Be aware that this means some combinations of matchers used in a claim rule may never match an OIDC token's claims. For example, the following OIDC policy statement will always reject a token, since the token's build_branch claim cannot be both equal to main and not equal to main at the same time:

- iss: https://agent.buildkite.com
  claims:
    build_branch:
      equals: main
      not_equals: main

Claim rule matchers

The following matchers can be used within a claim rule.

Matcher Argument type Description
equals Scalar The claim value must be exactly equal to the argument.
not_equals Scalar The claim value must not be exactly equal to the argument.
in List of scalars The claim value must be in the list of arguments.
not_in List of scalars The claim value must not be in the list of arguments.
matches List of glob strings OR a single glob string The claim value must match at least one of the globs provided. Note that this matcher is only applied when the claim value is a string, and is ignored otherwise.

Argument type details:

  • A scalar is a single value, which must be a String, Number (float or integer), Boolean, or Null.

  • A glob string is a string that may contain wildcards, such as * or ?, which match zero or more characters, or a single character respectively. Glob strings are not regular expressions, and do not support the full range of features that regular expressions do.

As a special case, if a claim rule in its entirety is a scalar, it is treated as if it were a rule with the equals matcher. This means that the following two claim rules are equivalent:

organization_slug: your-org
# is equivalent to
organization_slug:
  equals: your-org

Configure a Buildkite pipeline to authenticate to a registry

Configuring a Buildkite pipeline command step to request an OIDC token from Buildkite to interact with your Buildkite registry configured with an OIDC policy, is a two-part process.

Part 1: Request an OIDC token from Buildkite

To do this, use the following buildkite-agent oidc command:

buildkite-agent oidc request-token --audience "https://packages.buildkite.com/{org.slug}/{registry.slug}" --lifetime 300

where:

  • --audience is the target system that consumes this OIDC token. For Buildkite Package Registries, this value must be based on the URL https://packages.buildkite.com/{org.slug}/{registry.slug}.
  • {org.slug} can be obtained from the end of your Buildkite URL, after accessing Packages or Pipelines in the global navigation of your organization in Buildkite.
  • {registry.slug} is the slug of your registry, which is the kebab-case version of your registry name, and can be obtained after accessing Packages in the global navigation > your registry from the Registries page.

  • --lifetime is the time (in seconds) that the OIDC token is valid for. By default, this value must be less than 300.

Part 2: Authenticate the registry with the OIDC token

To do this (using Docker as an example), authenticate the registry with the OIDC token obtained in part 1 by piping the output through to the docker login command:

docker login packages.buildkite.com/{org.slug}/{registry.slug} --username buildkite --password-stdin

where:

Therefore, the full command step would look like:

buildkite-agent oidc request-token --audience "https://packages.buildkite.com/{org.slug}/{registry.slug}" --lifetime 300 | docker login packages.buildkite.com/{org.slug}/{registry.slug} --username buildkite --password-stdin

Assuming a Buildkite organization with slug my-organization and a pipeline slug my-pipeline, this full command would look like:

buildkite-agent oidc request-token --audience "https://packages.buildkite.com/my-organization/my-pipeline" --lifetime 300 | docker login packages.buildkite.com/my-organization/my-pipeline --username buildkite --password-stdin

Example pipeline

The following example Buildkite pipeline YAML snippet demonstrates how to push Docker images to a Buildkite registry using OIDC token authentication:

pipeline.yml
steps:
- key: "docker-build" # Build the Docker image
  label: ":docker: Build"
  command: docker build --tag packages.buildkite.com/my-organization/my-pipeline/my-image:latest .

- key: "docker-login" # Authenticate the Buildkite Agent to the Buildkite registry using an OIDC token
  label: ":docker: Login"
  command: buildkite-agent oidc request-token --audience "https://packages.buildkite.com/my-organization/my-pipeline" --lifetime 300 | docker login packages.buildkite.com/my-organization/my-pipeline --username buildkite --password-stdin
  depends_on: "docker-build"

- key: "docker-push" # Now authenticated, push the Docker image to the registry
  label: ":docker: Push"
  command: docker push packages.buildkite.com/my-organization/my-pipeline/my-pipeline/my-image:latest
  depends_on: "docker-login"