Skip to content

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-namespace resource 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/britive 3.0.5 or later. The examples use Terraform; the same objects can be created in the console.
  • oc access 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.

rbac.yaml
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.

rbac.yaml
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.

namespace-optin.yaml
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-system
oc apply -f rbac.yaml -f namespace-optin.yaml
oc label namespace <app>-dev owner=<repo> env=dev

Write 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.

checkout.sh
#!/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.

checkin.sh
#!/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-found

Create the labels and the resource type

main.tf
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.

main.tf
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

main.tf
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.

main.tf
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.

main.tf
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.

main.tf
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

SymptomCauseFix
Checkout fails; broker log shows errorDetail="missing CLUSTER_NAME"Variable not declared on the permission or not mapped on the profile permissionAdd it to variables and map it (resource.cluster_name)
E1003: Key part of policy resource labels is not part profileassociationsA policy filters on a label key the profile does not associateAdd the key to the profile’s associations
Attribute "code_language" cannot be specified when "checkout_code_file" is specifiedBoth set on the permissionRemove code_language
forbidden: ... cannot bind ClusterRole during checkoutcluster_role differs from the broker role’s resourceNamesChange the variable and the bind rule together
Token works for a few seconds after check-inAPI server authentication cache (about 10 s)Expected; the service account is already deleted

Next Steps

Last updated on