Skip to content

CI Pipelines

Overview

A pipeline job presents its CI platform’s OIDC token to Britive, checks out the deployer profile on its namespace, uses the returned token with oc, and checks in when the job ends, also on failure or cancellation. Each repository and environment pair is its own , so a dev pipeline can never check out a prod namespace.

What you’ll accomplish:

  • Federate the CI platform to Britive and create one service identity per pipeline environment
  • Scope each identity to its own namespaces with label-based profile policies
  • Add checkout, check-in and a token check to the pipeline
  • Prove isolation with a negative test

Before You Begin

  • The aro-namespace resource type, resources, deployer profile and policies from Namespace Resource Type
  • A runner that can reach the cluster API. For private APIs, a self-hosted runner in the cluster’s network.
  • pybritive, oc and jq on the runner

Choose the Identity

Use the CI platform’s OIDC token, matched on its subject claim. The platform’s notion of a deployment environment must appear in that claim; job variables do not.

PlatformPyBritive federation providerSubject to matchGuide
GitHub Actions-P github-<audience>sub, including :environment:<env>GitHub Actions
GitLab-P gitlabGitLab sub format per project, ref and environmentGitLab Pipelines
Bitbucket Pipelines-P bitbucketBitbucket OIDC subject per repository and deployment environmentBitbucket Pipelines

The rest of this guide uses GitHub Actions.

Steps

Create the OIDC identity provider

Create an OIDC identity provider for the CI platform with an exact sub match and an allowed audience. For GitHub: issuer https://token.actions.githubusercontent.com, audience britive. Details: GitHub Actions.

Create one service identity per environment

Check the repository’s subject format, then create gha-<repo>-dev and gha-<repo>-prod with the exact sub values:

gh api repos/<org>/<repo>/actions/oidc/customization/sub
Service identitysub (immutable subject enabled)
gha-<repo>-devrepo:<org>@<owner-id>/<repo>@<repo-id>:environment:dev
gha-<repo>-prodrepo:<org>@<owner-id>/<repo>@<repo-id>:environment:prod

Scope each identity with a profile policy

One policy per service identity on the deployer profile, filtered to the identity’s owner and env labels (policy example). The dev identity then sees only resources labeled env=dev.

Protect the production environment

In GitHub, limit the prod environment to the main branch (deployment branch policy). GitHub issues a token with environment:prod only after the environment’s rules pass, so this is the control that keeps other branches out of production. Add required reviewers where the GitHub plan supports them.

Store the checkout paths as variables

The checkout path is resources/<resource>/<profile>. Set it per environment so the workflow stays the same for both:

gh variable set BRITIVE_TENANT     --body "<tenant>"
gh variable set BRITIVE_FEDERATION --body "github-britive"
gh variable set BRITIVE_PROFILE --env dev  --body "resources/<cluster>-<app>-dev/deployer"
gh variable set BRITIVE_PROFILE --env prod --body "resources/<cluster>-<app>-prod/deployer"

Add the checkout step

--silent keeps status lines off stdout. The step finds the object with token and server in the response, masks the token, logs in, and sets the namespace on the context (oc project needs the Projects API, which a namespace-scoped role does not grant).

.github/workflows/deploy.yml
jobs:
  deploy:
    runs-on: [self-hosted, linux]
    environment: ${{ inputs.environment }}
    permissions:
      id-token: write
      contents: read
    env:
      BRITIVE_TENANT: ${{ vars.BRITIVE_TENANT }}
      BRITIVE_FEDERATION: ${{ vars.BRITIVE_FEDERATION }}
      BRITIVE_PROFILE: ${{ vars.BRITIVE_PROFILE }}
    steps:
      - name: Check out OpenShift access
        id: britive
        run: |
          set -euo pipefail
          export KUBECONFIG="$RUNNER_TEMP/kubeconfig"
          echo "KUBECONFIG=$KUBECONFIG" >> "$GITHUB_ENV"
          RAW=$(pybritive checkout "$BRITIVE_PROFILE" -t "$BRITIVE_TENANT" -P "$BRITIVE_FEDERATION" -m json --silent)
          OUT=$(sed -n '/^[[:space:]]*[{[]/,$p' <<<"$RAW" | jq -c 'first(.. | (if type == "string" then (fromjson? // empty) else . end) | objects | select(has("token") and has("server")))')
          TOKEN=$(jq -r .token <<<"$OUT"); echo "::add-mask::$TOKEN"
          NS=$(jq -r .namespace <<<"$OUT")
          CA=$(jq -r '.ca // ""' <<<"$OUT")
          if [ -n "$CA" ]; then
            echo "$CA" | base64 -d > "$RUNNER_TEMP/ca.crt"
            oc login --server="$(jq -r .server <<<"$OUT")" --token="$TOKEN" --certificate-authority="$RUNNER_TEMP/ca.crt" >/dev/null
          else
            oc login --server="$(jq -r .server <<<"$OUT")" --token="$TOKEN" >/dev/null
          fi
          oc config set-context --current --namespace="$NS" >/dev/null
          echo "Checked out $(jq -r .service_account <<<"$OUT") in $NS"

      - name: Deploy
        run: oc get pods,deployments,services

Add the check-in step

Check-in must run even when the job fails or is cancelled:

.github/workflows/deploy.yml
      - name: Check in OpenShift access
        id: checkin
        if: always()
        run: pybritive checkin "$BRITIVE_PROFILE" -t "$BRITIVE_TENANT" -P "$BRITIVE_FEDERATION" --silent

Add the token check (optional)

Proves the credential is gone. The API server caches successful token authentications for about 10 seconds, so poll:

.github/workflows/deploy.yml
      - name: Token rejected after check-in
        if: always() && steps.checkin.outcome == 'success'
        run: |
          for i in $(seq 0 5 60); do
            if ! oc whoami >/dev/null 2>&1; then echo "PASS: token rejected (within ${i}s)"; exit 0; fi
            sleep 5
          done
          echo "FAIL: token still accepted 60s after check-in"; exit 1

Verify

Run each case and confirm the expected result.

TestHowExpected
Pipeline reads its own namespaceRun the workflow with environment=devChecked out gha-<txn> in <app>-dev; oc whoami shows system:serviceaccount:<app>-dev:gha-<txn>
GuardrailsIn the job: oc auth can-i delete pods; oc get pods -n <app>-prodno; Forbidden
Cross-environment denialA dev job checks out resources/<cluster>-<app>-prod/deployerFails with resource and profile combination not found
CancellationCancel a run during a long stepCheck-in still runs; no gha-* service account remains
Token after check-inThe token check stepPASS: token rejected (within 10s)
Nothing left overoc get sa,rolebinding -A -l app.kubernetes.io/managed-by=britiveNo resources

Troubleshoot

SymptomCauseFix
Authenticated to tenant ... via federation provider authentication then could not login after 3 attemptsThe token’s sub matches no service identityCompare the repository’s subject format with the service identity; use Test matching
britive.exceptions.TransactionNotFoundThe broker’s checkout script failedBroker log: response.errorDetail
jq: parse error: Invalid numeric literalPyBritive status lines before the JSONAdd --silent
error: You are not a member of project "<ns>"oc project uses the Projects APIoc config set-context --current --namespace="$NS"
ValueError: no checked out profile found at check-inCheckout never completedFix the checkout failure; nothing to clean up

Next Steps

Last updated on