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-namespaceresource type, resources,deployerprofile 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,ocandjqon 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.
| Platform | PyBritive federation provider | Subject to match | Guide |
|---|---|---|---|
| GitHub Actions | -P github-<audience> | sub, including :environment:<env> | GitHub Actions |
| GitLab | -P gitlab | GitLab sub format per project, ref and environment | GitLab Pipelines |
| Bitbucket Pipelines | -P bitbucket | Bitbucket OIDC subject per repository and deployment environment | Bitbucket 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 identity | sub (immutable subject enabled) |
|---|---|
gha-<repo>-dev | repo:<org>@<owner-id>/<repo>@<repo-id>:environment:dev |
gha-<repo>-prod | repo:<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).
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,servicesAdd the check-in step
Check-in must run even when the job fails or is cancelled:
- name: Check in OpenShift access
id: checkin
if: always()
run: pybritive checkin "$BRITIVE_PROFILE" -t "$BRITIVE_TENANT" -P "$BRITIVE_FEDERATION" --silentAdd the token check (optional)
Proves the credential is gone. The API server caches successful token authentications for about 10 seconds, so poll:
- 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 1Verify
Run each case and confirm the expected result.
| Test | How | Expected |
|---|---|---|
| Pipeline reads its own namespace | Run the workflow with environment=dev | Checked out gha-<txn> in <app>-dev; oc whoami shows system:serviceaccount:<app>-dev:gha-<txn> |
| Guardrails | In the job: oc auth can-i delete pods; oc get pods -n <app>-prod | no; Forbidden |
| Cross-environment denial | A dev job checks out resources/<cluster>-<app>-prod/deployer | Fails with resource and profile combination not found |
| Cancellation | Cancel a run during a long step | Check-in still runs; no gha-* service account remains |
| Token after check-in | The token check step | PASS: token rejected (within 10s) |
| Nothing left over | oc get sa,rolebinding -A -l app.kubernetes.io/managed-by=britive | No resources |
Troubleshoot
| Symptom | Cause | Fix |
|---|---|---|
Authenticated to tenant ... via federation provider authentication then could not login after 3 attempts | The token’s sub matches no service identity | Compare the repository’s subject format with the service identity; use Test matching |
britive.exceptions.TransactionNotFound | The broker’s checkout script failed | Broker log: response.errorDetail |
jq: parse error: Invalid numeric literal | PyBritive status lines before the JSON | Add --silent |
error: You are not a member of project "<ns>" | oc project uses the Projects API | oc config set-context --current --namespace="$NS" |
ValueError: no checked out profile found at check-in | Checkout never completed | Fix the checkout failure; nothing to clean up |