Namespace Resource Type
Overview
Model each OpenShift namespace as a Resource Manager . A checkout creates service account gha-<transaction> with a RoleBinding to one ClusterRole and returns a token for the session; check-in deletes the service account, which invalidates the token. One profile serves every pipeline; labels decide which namespaces each pipeline identity may check out.
What you’ll accomplish:
- Grant the broker the narrow RBAC it needs in opted-in namespaces
- Create the
aro-namespaceresource type with checkout and check-in scripts - Map resource fields and transaction values to script variables
- Create one resource per namespace, a profile, and label-scoped profile policies
Before You Begin
- The Access Broker running with a kubeconfig for the cluster, in a broker pool. See Deploy the Access Broker on OpenShift.
- Britive administrator access, and the Britive Terraform provider
britive/britive3.0.5 or later. The examples use Terraform; the same objects can be created in the console. ocaccess as a cluster administrator to apply RBAC.
Steps
Create the pipeline ClusterRole
The ClusterRole every pipeline service account is bound to. This example is read-only; widen it to what your pipelines deploy.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: gha-reader
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "services", "configmaps", "events"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["route.openshift.io"]
resources: ["routes"]
verbs: ["get", "list", "watch"]Create the broker’s namespace role
The broker can manage service accounts and RoleBindings, mint tokens, and bind only the pipeline ClusterRole (resourceNames). It cannot grant itself or anyone else broader access.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: britive-broker-ns
rules:
- apiGroups: [""]
resources: ["serviceaccounts"]
verbs: ["get", "list", "create", "delete", "patch"]
- apiGroups: [""]
resources: ["serviceaccounts/token"]
verbs: ["create"]
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["rolebindings"]
verbs: ["get", "list", "create", "delete", "patch"]
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["clusterroles"]
verbs: ["bind"]
resourceNames: ["gha-reader"]Opt a namespace in
Bind the broker’s role in each namespace pipelines may use, and label the namespace.
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: britive-broker
namespace: <app>-dev
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: britive-broker-ns
subjects:
- kind: ServiceAccount
name: britive-broker
namespace: britive-systemoc apply -f rbac.yaml -f namespace-optin.yaml
oc label namespace <app>-dev owner=<repo> env=devWrite the checkout script
The script reads its inputs from variables Britive passes (next steps), creates the service account and RoleBinding, requests a token for the session, and prints one JSON object. It rolls back on any error.
#!/usr/bin/env bash
set -euo pipefail
pick() { local n; for n in "$@"; do [[ -n "${!n:-}" ]] && { printf '%s' "${!n}"; return 0; }; done; return 1; }
san() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9.-]+/-/g' \
| cut -c1-63 | sed -E 's/^[^a-z0-9]+//; s/[^a-z0-9]+$//'; }
CLUSTER_NAME=$(pick CLUSTER_NAME) || { echo "missing CLUSTER_NAME" >&2; exit 2; }
API_URL=$(pick API_URL) || { echo "missing API_URL" >&2; exit 2; }
NAMESPACE=$(pick NAMESPACE) || { echo "missing NAMESPACE" >&2; exit 2; }
TXN_ID=$(pick TRX) || { echo "missing TRX" >&2; exit 2; }
CLUSTER_ROLE=$(pick cluster_role || echo gha-reader)
TTL=$(pick EXPIRATION || echo 1800)
(( TTL > 86400 )) && TTL=$(( TTL / 1000 )) # tolerate milliseconds
(( TTL < 600 )) && TTL=600 # TokenRequest minimum
KC="/etc/britive/kubeconfigs/${CLUSTER_NAME}.kubeconfig"
oc_() { oc --kubeconfig "$KC" -n "$NAMESPACE" "$@"; }
TXN_L=$(san "$TXN_ID"); SA=$(san "gha-${TXN_ID}")
EXPIRES_AT=$(date -u -d "+${TTL} seconds" +%Y-%m-%dT%H:%M:%SZ)
cleanup() { oc_ delete sa,rolebinding -l "britive.com/transaction-id=${TXN_L}" --ignore-not-found >/dev/null 2>&1 || true; }
trap cleanup ERR
oc_ apply -f - >/dev/null <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: ${SA}
labels: {app.kubernetes.io/managed-by: britive, britive.com/transaction-id: "${TXN_L}"}
annotations: {britive.com/expires-at: "${EXPIRES_AT}"}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ${SA}
labels: {app.kubernetes.io/managed-by: britive, britive.com/transaction-id: "${TXN_L}"}
roleRef: {apiGroup: rbac.authorization.k8s.io, kind: ClusterRole, name: ${CLUSTER_ROLE}}
subjects: [{kind: ServiceAccount, name: ${SA}, namespace: ${NAMESPACE}}]
EOF
TOKEN=$(oc_ create token "$SA" --duration="${TTL}s")
CA=$(oc --kubeconfig "$KC" config view --minify --raw \
-o jsonpath='{.clusters[0].cluster.certificate-authority-data}')
jq -n --arg server "$API_URL" --arg token "$TOKEN" --arg namespace "$NAMESPACE" \
--arg ca "$CA" --arg expires_at "$EXPIRES_AT" --arg service_account "$SA" \
'{server: $server, token: $token, namespace: $namespace, ca: $ca,
expires_at: $expires_at, service_account: $service_account}'Write the check-in script
Deleting the service account invalidates every token minted for it. Deleting by transaction label makes the script idempotent.
#!/usr/bin/env bash
set -euo pipefail
san() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9.-]+/-/g' \
| cut -c1-63 | sed -E 's/^[^a-z0-9]+//; s/[^a-z0-9]+$//'; }
: "${CLUSTER_NAME:?}" "${NAMESPACE:?}" "${TRX:?}"
oc --kubeconfig "/etc/britive/kubeconfigs/${CLUSTER_NAME}.kubeconfig" -n "$NAMESPACE" \
delete sa,rolebinding \
-l "britive.com/transaction-id=$(san "$TRX"),app.kubernetes.io/managed-by=britive" \
--ignore-not-foundCreate the labels and the resource type
resource "britive_resource_manager_resource_label" "owner" {
name = "owner"
values { name = "<repo>" }
}
resource "britive_resource_manager_resource_label" "env" {
name = "env"
values { name = "dev" }
values { name = "prod" }
}
resource "britive_resource_manager_resource_label" "managed" {
name = "britive-managed"
values { name = "true" }
values { name = "false" }
}
resource "britive_resource_manager_resource_type" "aro_namespace" {
name = "aro-namespace"
parameters {
param_name = "cluster_name"
param_type = "string"
is_mandatory = true
}
parameters {
param_name = "api_url"
param_type = "string"
is_mandatory = true
}
parameters {
param_name = "namespace"
param_type = "string"
is_mandatory = true
}
}Create the permission and declare its variables
The broker passes a script only the variables declared here and mapped on the profile permission (later step). Resource fields and transaction values are not passed implicitly.
resource "britive_resource_manager_response_template" "token" {
name = "aro-token"
show_on_ui = false
template_data = "{\"server\":\"{{server}}\",\"token\":\"{{token}}\",\"namespace\":\"{{namespace}}\",\"ca\":\"{{ca}}\",\"expires_at\":\"{{expires_at}}\",\"service_account\":\"{{service_account}}\"}"
}
resource "britive_resource_manager_resource_type_permission" "deployer" {
name = "deployer"
resource_type_id = britive_resource_manager_resource_type.aro_namespace.id
variables = ["CLUSTER_NAME", "API_URL", "NAMESPACE", "TRX", "EXPIRATION", "cluster_role"]
checkout_code_file = "${path.module}/scripts/checkout.sh"
checkin_code_file = "${path.module}/scripts/checkin.sh"
checkout_time_limit = 120
checkin_time_limit = 120
response_templates = [britive_resource_manager_response_template.token.name]
}Do not set code_language together with checkout_code_file or checkin_code_file; the provider takes the language from the file extension and rejects both together.
Create one resource per namespace
resource "britive_resource_manager_resource" "dev" {
name = "<cluster>-<app>-dev"
resource_type = britive_resource_manager_resource_type.aro_namespace.name
parameter_values = {
cluster_name = "<cluster>"
api_url = "https://api.<cluster-domain>:6443"
namespace = "<app>-dev"
}
resource_labels = {
owner = "<repo>"
env = "dev"
"britive-managed" = "true"
}
}
resource "britive_resource_manager_resource_broker_pools" "dev" {
resource_id = britive_resource_manager_resource.dev.id
broker_pools = ["<broker-pool>"]
}Use hyphens in resource names. / separates path segments in pybritive checkout resources/<resource>/<profile>.
Create the profile
Every label key a profile policy filters on must also be a profile association, so associate britive-managed, owner and env with all allowed values. expiration_duration is in milliseconds.
resource "britive_resource_manager_profile" "deployer" {
name = "deployer"
expiration_duration = 1800000 # 30 minutes
associations {
label_key = "britive-managed"
values = ["true"]
}
associations {
label_key = "owner"
values = ["<repo>"]
}
associations {
label_key = "env"
values = ["dev", "prod"]
}
}Map the script variables on the profile permission
System values use is_system_defined = true: resource.<field> for resource fields, profile.transactionId for the transaction, profile.timeout for the session duration.
resource "britive_resource_manager_profile_permission" "deployer" {
profile_id = britive_resource_manager_profile.deployer.id
name = britive_resource_manager_resource_type_permission.deployer.name
version = "latest"
variables {
name = "CLUSTER_NAME"
value = "resource.cluster_name"
is_system_defined = true
}
variables {
name = "API_URL"
value = "resource.api_url"
is_system_defined = true
}
variables {
name = "NAMESPACE"
value = "resource.namespace"
is_system_defined = true
}
variables {
name = "TRX"
value = "profile.transactionId"
is_system_defined = true
}
variables {
name = "EXPIRATION"
value = "profile.timeout"
is_system_defined = true
}
variables {
name = "cluster_role"
value = "gha-reader"
is_system_defined = false
}
}Add one policy per pipeline identity
Each policy admits one service identity and narrows the profile to that identity’s owner and env.
resource "britive_resource_manager_profile_policy" "dev" {
profile_id = britive_resource_manager_profile.deployer.id
policy_name = "gha-<repo>-dev"
access_type = "Allow"
members = jsonencode({ serviceIdentities = [{ name = "gha-<repo>-dev" }] })
resource_labels {
label_key = "owner"
values = ["<repo>"]
}
resource_labels {
label_key = "env"
values = ["dev"]
}
resource_labels {
label_key = "britive-managed"
values = ["true"]
}
}Verify
Check out as an administrator with access to the profile, or run a pipeline (CI pipelines guide), then look in the namespace:
oc get sa,rolebinding -A -l app.kubernetes.io/managed-by=britive \
-o custom-columns='NS:.metadata.namespace,KIND:.kind,NAME:.metadata.name,EXPIRES:.metadata.annotations.britive\.com/expires-at'During the session: one service account and one RoleBinding named gha-<transaction>. After check-in: none.
Troubleshoot
| Symptom | Cause | Fix |
|---|---|---|
Checkout fails; broker log shows errorDetail="missing CLUSTER_NAME" | Variable not declared on the permission or not mapped on the profile permission | Add it to variables and map it (resource.cluster_name) |
E1003: Key part of policy resource labels is not part profileassociations | A policy filters on a label key the profile does not associate | Add the key to the profile’s associations |
Attribute "code_language" cannot be specified when "checkout_code_file" is specified | Both set on the permission | Remove code_language |
forbidden: ... cannot bind ClusterRole during checkout | cluster_role differs from the broker role’s resourceNames | Change the variable and the bind rule together |
| Token works for a few seconds after check-in | API server authentication cache (about 10 s) | Expected; the service account is already deleted |