Chapter 5. RBAC for Least Privileges

Where should “RBAC Least Privilege” live?

  • Keep RBAC as its own short chapter (Ops hardening) with hands-on Role/RoleBinding/SA steps. It’s a foundational Kubernetes control and applies whether or not Kyverno is present.

  • Reference Kyverno as an optional guardrail at the end of that chapter (e.g., “disallow wildcard RBAC”), since you already use Kyverno earlier. That keeps the learning path incremental:

    1. grant least privilege (RBAC files)
    2. verify with kubectl auth can-i
    3. optionally enforce via Kyverno policy

Minimal Makefile additions (fit your style)

These targets don’t change any existing flow; they’re opt-in helpers.

# ---- RBAC (namespace-scoped least privilege) ----
k8s-rbac:
	@echo "== Apply app ServiceAccount + Role + RoleBinding =="
	@kubectl apply -f $(K8S_DIR)/rbac/sa.yaml
	@kubectl apply -f $(K8S_DIR)/rbac/role.yaml
	@kubectl apply -f $(K8S_DIR)/rbac/binding.yaml

rbac-verify:
	@echo "== RBAC can-i checks (evidence) =="
	@mkdir -p artifacts
	@{ \
	  date; \
	  echo "# RBAC verify (serviceaccount $(NAMESPACE):app-sa)"; \
	  kubectl -n $(NAMESPACE) auth can-i get pods     --as=system:serviceaccount:$(NAMESPACE):app-sa; \
	  kubectl -n $(NAMESPACE) auth can-i list pods    --as=system:serviceaccount:$(NAMESPACE):app-sa; \
	  kubectl -n $(NAMESPACE) auth can-i watch pods   --as=system:serviceaccount:$(NAMESPACE):app-sa; \
	  kubectl -n $(NAMESPACE) auth can-i create pods  --as=system:serviceaccount:$(NAMESPACE):app-sa; \
	  kubectl -n $(NAMESPACE) auth can-i get secrets  --as=system:serviceaccount:$(NAMESPACE):app-sa; \
	  kubectl -n kube-system auth can-i list pods     --as=system:serviceaccount:$(NAMESPACE):app-sa; \
	} | tee artifacts/rbac-verify.txt
	@echo "wrote artifacts/rbac-verify.txt"

RBAC files (namespace $(NAMESPACE))

infra/k8s/rbac/sa.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  namespace: ship
automountServiceAccountToken: true

infra/k8s/rbac/role.yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: app-read
  namespace: ship
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["configmaps"]
    verbs: ["get"]

infra/k8s/rbac/binding.yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-read-binding
  namespace: ship
subjects:
  - kind: ServiceAccount
    name: app-sa
    namespace: ship
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: app-read

And add to your Deployment (already present file):

spec:
  template:
    spec:
      serviceAccountName: app-sa

Optional: Kyverno guardrail for RBAC (put in your existing policies/)

Since you already install/apply Kyverno, add a simple policy to block wildcard RBAC. This belongs in the Kyverno section and you can link it from the RBAC chapter as “turn on this safety net”.

policies/disallow-rbac-wildcards.yaml

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-rbac-wildcards
spec:
  validationFailureAction: enforce
  background: true
  rules:
    - name: no-wildcard-role
      match:
        any:
          - resources:
              kinds: ["Role","ClusterRole"]
      validate:
        message: "Avoid wildcard RBAC; specify explicit verbs/resources."
        pattern:
          spec:
            rules:
              - verbs: "!*=*"
                resources: "!*=*"

Wire it with your existing target (no new targets needed):

# just run:
make kyverno-apply