Write Your Own Keycard – Custom RBAC in Action

Duration: ~20 minutes

Overview

By the end you’ll be able to design and apply a narrowly scoped Role that grants only the handful of permissions an application maintainer truly needs, verify what works, prove what is denied, and explain how that shrinks incident blast radius.

Create a minimal custom Role and RoleBinding, prove allowed versus denied verbs, and state the least-privilege intent in plain language.

Why it matters

Imagine an office where every employee’s badge opens every room—including finance, HR, and the server closet.

Nobody intends abuse, but one lost badge means a long night of worrying what was accessed. Built‑in broad roles (edit, admin) are those master badges. Custom RBAC is like cutting a key that only opens the supply cabinet your role requires. When (not if) an account is phished or a token is leaked, tightly scoped permissions turn a potential headline into a routine ticket. Auditors also smile when intent is obvious: “This identity may only adjust configuration.”

What does it solve

Most real security incidents are ordinary credentials used in the wrong places. Over-broad roles quietly enable:

  • Unapproved pod deployments (persistence footholds)

  • Reading secrets far outside an app’s scope (data leakage)

  • Creating new RoleBindings to escalate (privilege stacking)

  • Difficult audit narratives (“Why did this CI user delete a Secret?”)

Crafting a custom Role removes silent “just in case” powers. It converts vague responsibility (“Dev team rights”) into precise intent (“May adjust non-sensitive app configuration values and rotate one secret”). That precision speeds investigations and compliance reviews.

Your Mission

In 101-03 you saw what namespace admin (and even edit) actually unlocks: Secret theft, a self-forged RoleBinding, and a foothold Deployment. Built-in roles are the right starting ladder—they are still broader than many day-to-day jobs. An app maintainer who only needs to flip a ConfigMap flag and rotate one Secret does not need to create pods or mint more RoleBindings.

Your job is to cut a custom Role for that job and prove it is not a master badge:

  • Allow only get/list/update/patch on ConfigMaps and Secrets—enough to read and rotate, not to wipe the Secret.

  • Bind that Role to a human (devuser) and show the allowed path works (oc auth can-i plus a real oc patch).

  • Show the dangerous verbs stay denied: create pods, create RoleBindings, delete the Secret. Try the creates/deletes so a can-i of no is not the only evidence.

  • Repeat the allow check for a dedicated ServiceAccount (how CI and app pods should authenticate), then turn off default token automount so a pod that does not call the API does not carry a token.

When you finish, you should be able to say in one sentence what this identity may do—and what an attacker with that leaked token still cannot do.

Click each step only if you need a hint.

Create project 201-01-c-rbac-lab

Stay in this project for every later command.

oc new-project 201-01-c-rbac-lab
Now using project "201-01-c-rbac-lab".
Create Role config-secret-updater

Grant get, list, update, and patch on ConfigMaps and Secrets. Include patch so oc patch and modern controllers work.

oc create role config-secret-updater \
  --verb=get --verb=list --verb=update --verb=patch \
  --resource=configmaps,secrets
role.rbac.authorization.k8s.io/config-secret-updater created.
Create RoleBinding updater-binding for user devuser

Bind the Role to the demo user. Do not bind edit or admin.

oc create rolebinding updater-binding \
  --role=config-secret-updater --user=devuser
rolebinding.rbac.authorization.k8s.io/updater-binding created.
Create ConfigMap app-config

Seed a ConfigMap so you have something allowed to patch.

oc create configmap app-config --from-literal=flag=on
configmap/app-config created.
Create Secret db-creds

Seed a Secret you will rotate (update/patch) but must not be able to delete.

oc create secret generic db-creds --from-literal=password=Initial123!
secret/db-creds created.
Check whether devuser can list ConfigMaps

This verb is on the Role. Impersonate devuser.

oc auth can-i list configmaps --as devuser
Result is yes.
Check whether devuser can update secret/db-creds

Rotation is an update, not a delete.

oc auth can-i update secret/db-creds --as devuser
Result is yes.
Patch ConfigMap app-config as devuser

Prove the allowed path with a real write, not only can-i.

oc patch configmap app-config -p '{"data":{"flag":"off"}}' --type=merge --as devuser
configmap/app-config patched.
Patch Secret db-creds as devuser

Rotate the password in place. stringData avoids hand-rolled base64.

oc patch secret db-creds -p '{"stringData":{"password":"Rotated456!"}}' --type=merge --as devuser
secret/db-creds patched.
Check whether devuser can create pods

Pod create is how an attacker plants a foothold. This Role must not allow it.

oc auth can-i create pods --as devuser
Result is no.
Try oc run as devuser

Confirm admission/RBAC actually blocks the create, not just can-i.

oc run test --image=registry.access.redhat.com/ubi9/ubi --as devuser || echo DENIED
The create fails and DENIED prints.
Check whether devuser can create RoleBindings

RoleBinding create is the persistence path from 101-03.

oc auth can-i create rolebinding --as devuser
Result is no.
Try creating RoleBinding bogus as devuser

Prove they cannot mint a second binding to view (or anything else).

oc create rolebinding bogus --clusterrole=view --user=devuser --as devuser || echo DENIED
The create fails and DENIED prints.
Check whether devuser can delete secret/db-creds

Rotation needs update/patch. Delete is extra blast radius—leave it off the Role.

oc auth can-i delete secret/db-creds --as devuser
Result is no.
Try deleting Secret db-creds as devuser

Confirm the Secret survives.

oc delete secret db-creds --as devuser || echo DENIED
Delete fails, DENIED prints, and db-creds still exists.
Create ServiceAccount app-sa

Automation should use a dedicated SA, not the default SA and not a human user.

oc create sa app-sa
serviceaccount/app-sa created.
Bind config-secret-updater to app-sa

Same Role, different subject. Scope the binding to this project’s SA.

oc create rolebinding sa-updater \
  --role=config-secret-updater \
  --serviceaccount=201-01-c-rbac-lab:app-sa
rolebinding.rbac.authorization.k8s.io/sa-updater created.
Check whether app-sa can patch ConfigMap app-config

Impersonate the SA the same way a compromised pod would.

oc auth can-i patch configmap/app-config \
  --as system:serviceaccount:201-01-c-rbac-lab:app-sa
Result is yes.
Disable automountServiceAccountToken on app-sa

If a pod does not need the API, do not mount a token by default.

oc patch sa app-sa -p '{"automountServiceAccountToken":false}'
serviceaccount/app-sa patched. Pods that need the API must opt in.

Debrief

Custom Roles shrink blast radius: losing a closet key hurts less than losing a master key—precise verbs beat broad edit/admin for sensitive paths.

What breaks without this:

  • Broad namespaced roles → pod create persistence, Secret sprawl, RBAC self-escalation

  • ClusterRole for namespace objects → accidental cluster reach

minimal Roles, separated ServiceAccounts, disable unused automount, oc auth can-i matrices, and RHACS/wildcard reviews.
include patch when updates need it; avoid “just in case” delete; prefer Role over ClusterRole for namespace-scoped work.
giphy

Cleanup

Before moving to the next module, run the lab cleanup script to reset transient resources from this module.

cd ~/ocp5-rhacs-showroom
bash setup/lab-cleanup.sh --module 201-01