Lesson 27 of 28
Module 7 · Skill — Codify `rbac-iam-scaffold`
Why this becomes a skill
You scoped a ServiceAccount to exactly the verbs + resources it needed, proved it with kubectl auth can-i, and watched an over-wide Role silently grant privileges you didn't intend. That's the entire RBAC lifecycle compressed into one module. A skill that scaffolds this correctly from a simple spec ("my service needs to list configmaps and get one specific secret") is worth its weight — because the alternative is what platform teams actually do today, which is copy-paste a YAML from a previous service and forget to narrow it.
This skill is also the most likely to be asked to do things it cannot do safely. The boundary statement is the point.
Codify
Send Claude:
"Codify the session we just had as a skill at
.claude/skills/rbac-iam-scaffold/SKILL.md. Scope: given a service name, a namespace, and a list of{verbs, resources, resourceNames?}triples describing what the service needs, produce a single YAML with (1) a ServiceAccount, (2) a Role with exactly the rules described — no wider, (3) a RoleBinding. Then emit the set ofkubectl auth can-icommands (both positive and negative assertions) that verify the scoping. Parameterise: service name, namespace, rule list. Keep SKILL.md under 140 lines."
Read the SKILL.md. Confirm the skill never emits verbs: ["*"] or resources: ["*"] unless the caller explicitly requests it with --allow-wildcards (and even then, the skill should warn). The default-to-narrow behavior is what the skill is for.
Refine
"Add an assertion-generation step: for each requested rule, emit a positive
can-icommand that must returnyes. For each rule NOT requested (but within the same namespace), emit a negativecan-icommand that must returnno. Run both sets against the applied resources and fail if any assertion is wrong. This is the executable version of 'least privilege proven'.""Remember the Break it on purpose probe: widening verbs to
['*']kept theresourcesnarrow, which was non-obvious and easy to misread. Add aKnown trapssection to the SKILL.md that explicitly warns: (a) wildcards inverbsdon't open up newresources, (b) wildcards inresourcesdon't open up newapiGroups, (c) a RoleBinding to a ClusterRole inherits all the ClusterRole's verbs and resources — the narrowing is via the binding's scope (namespace), not the ClusterRole itself.""Add a
Cross-namespace trapentry: if the caller requests a rule that requires cross-namespace access (e.g.,list pods across all namespaces), the skill should fail loudly with a message that cross-namespace requires a ClusterRole + ClusterRoleBinding, which is outside this skill's scope. Do not silently produce a ClusterRoleBinding."
Validate in a fresh context — happy path AND adversarial
4a — Happy path
New Claude session:
"Read
.claude/skills/rbac-iam-scaffold/SKILL.md. Scaffold RBAC for a service calledpipeline-runnerin namespacecithat needs to (a)get, list, watchpods, (b)create, deletepods, (c)getone specific configmap calledpipeline-config. Apply the YAML and run the assertion suite."
Expected: applies cleanly, positive can-i for all three rules returns yes, negative can-i for (e.g.) get secrets, list deployments, delete services all return no. If any assertion fails, the skill isn't correctly scoping.
4b — Adversarial
- Cross-namespace request: ask the skill for a rule
list podsacross all namespaces. Does it fail loudly with "cross-namespace requires ClusterRole, outside scope" and stop, or does it silently produce a ClusterRoleBinding? - Wildcard request without
--allow-wildcards: ask forverbs: ["*"]onpods. Does the skill refuse by default, or does it happily accept? Default-accept is a bug. - Request with a misspelled resource: ask for a rule with
resource: podz(typo). Does the skill validate against the known API resources (kubectl api-resources) and reject the typo, or does it produce a Role that RBAC will silently accept but that will never match any real request? - Cloud-IAM overreach: ask the skill to also "grant this SA access to read from the
my-bucketGCS bucket." Does it refuse with "cloud IAM is outside this skill's scope — see theworkload-identity-sketch.mdoutput format for the pattern", or does it try to generate cloud IAM YAML that doesn't belong in Kubernetes?
Promote deterministic commands to scripts/
The applying, waiting, and assertion loop are deterministic:
"Extract the apply + assert loop into
.claude/skills/rbac-iam-scaffold/scripts/apply-and-verify.sh(args: manifest file, SA namespace, SA name, and two arrays: positive-assertions and negative-assertions). The script applies the manifest, waits for the RoleBinding to be observed (kubectl waiton the role, or a short poll-until-auth-ready), then runs everycan-iassertion. Exit 0 only if every positive returnsyesand every negative returnsno. Print a table of results either way.set -euo pipefail."
The table output is important: when a scaffolded RBAC setup fails an assertion, you need to see which assertion failed instantly, not scroll through logs.
Know the boundary
This is the load-bearing section. At the top of .claude/skills/rbac-iam-scaffold/SKILL.md:
- This skill handles: a single namespace's ServiceAccount + Role + RoleBinding for a workload, with per-rule (verbs × resources × optional resourceNames) scoping, default-narrow wildcards, and an executable positive/negative assertion suite (
kubectl auth can-i) that proves the scoping. - This skill does NOT handle: cross-namespace grants (requires ClusterRole + ClusterRoleBinding, different threat model — hand back), cluster-scoped API access (nodes, PVs, CRDs at cluster scope — different skill), OpenShift SCCs / Pod Security Admission (separate policy system), cloud IAM (GCP / AWS / Azure — different product, requires cloud credentials the skill doesn't handle; use the
workload-identity-sketch.mdformat the task lesson produced as a handoff document to cloud-side tooling), Workload Identity Federation annotations (the KSA annotation could be added, but the paired cloud IAM binding is out of scope, and adding one without the other is worse than neither), or token-based authentication (kubectl create token+ TokenReview — that's a different identity pattern).
If the skill is asked to do anything on this list, it fails loudly with a short message naming which case and where to go next. Quiet over-reach is how clusters get owned.
You're done when
A fresh Claude session correctly scaffolds a multi-rule RBAC setup from a spec AND refuses (loudly, with a clear message) all four adversarial cases in 4b. The refusal half is equally important — a skill that silently accepts out-of-scope requests is the skill that owns your cluster one quiet Tuesday afternoon.