Kubernetes
RBAC
CronJob
ServiceAccount
Forbidden Error

CronJob Pod with RBAC Role via Serviceaccount Keeps Throwing Forbidden Error

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

A Kubernetes CronJob that keeps failing with a Forbidden error almost always has an identity problem rather than a scheduling problem. The pod is reaching the API server, but the service account attached to that pod does not have the verbs, resources, or namespace scope needed for the request it is making.

Attach the Service Account to the Pod Template

The most important detail is where serviceAccountName belongs. It must be set on the pod template under the job template, not only on the CronJob metadata.

yaml
1apiVersion: v1
2kind: ServiceAccount
3metadata:
4  name: cleanup-sa
5  namespace: ops
6---
7apiVersion: rbac.authorization.k8s.io/v1
8kind: Role
9metadata:
10  name: pod-reader
11  namespace: ops
12rules:
13  - apiGroups: [""]
14    resources: ["pods"]
15    verbs: ["get", "list", "delete"]
16---
17apiVersion: rbac.authorization.k8s.io/v1
18kind: RoleBinding
19metadata:
20  name: cleanup-bind
21  namespace: ops
22subjects:
23  - kind: ServiceAccount
24    name: cleanup-sa
25    namespace: ops
26roleRef:
27  apiGroup: rbac.authorization.k8s.io
28  kind: Role
29  name: pod-reader
30---
31apiVersion: batch/v1
32kind: CronJob
33metadata:
34  name: cleanup-job
35  namespace: ops
36spec:
37  schedule: "*/15 * * * *"
38  jobTemplate:
39    spec:
40      template:
41        spec:
42          serviceAccountName: cleanup-sa
43          restartPolicy: OnFailure
44          containers:
45            - name: kubectl
46              image: bitnami/kubectl:latest
47              command: ["/bin/sh", "-c"]
48              args:
49                - kubectl get pods -n ops

If serviceAccountName is omitted there, the pod uses the default service account, and your carefully created RoleBinding does nothing for the actual workload.

Match Namespace Scope Correctly

A Role and RoleBinding are namespaced resources. If the CronJob runs in namespace ops, but the RoleBinding lives in default, the permissions will not apply the way you expect.

The alignment should be explicit:

  • CronJob namespace
  • ServiceAccount namespace
  • Role namespace
  • RoleBinding namespace

All four must match for a namespaced role setup.

If the job needs to read or change resources across multiple namespaces, a Role is not enough. That is when you move to ClusterRole and ClusterRoleBinding.

Verify the Exact Permission With kubectl auth can-i

The quickest way to test the service account is to ask the API server whether that identity can perform the operation.

bash
kubectl auth can-i list pods \
  --as=system:serviceaccount:ops:cleanup-sa \
  -n ops

You can also test the exact verb and resource that the container uses, such as patch deployments, get configmaps, or create jobs. This is far faster than repeatedly editing YAML and waiting for the CronJob to run again.

If the command says no, the RBAC rule is wrong. If it says yes, then the next suspect is the pod not actually using the expected service account.

Read the Forbidden Error Literally

The API server error message usually tells you which identity was denied and which resource it tried to access. For example, a message like this is extremely informative:

text
User "system:serviceaccount:ops:cleanup-sa" cannot list resource "pods" in API group "" in the namespace "ops"

That line tells you:

  • the caller identity
  • the verb
  • the resource
  • the API group
  • the namespace

Use that exact combination to write or fix the rule. Guessing from memory is slower and usually less accurate.

Watch for API Group Mistakes

RBAC rules fail often because the API group is wrong. Core resources such as pods, services, and configmaps use the empty API group. Deployments belong to the apps API group. Batch jobs use batch.

If your CronJob scales a deployment, for example, the rule must refer to deployments under apps, not under the empty group.

yaml
1rules:
2  - apiGroups: ["apps"]
3    resources: ["deployments"]
4    verbs: ["get", "patch"]

A correct resource name with the wrong API group still produces Forbidden.

Check the Running Pod, Not Just the Manifest

It is possible to apply the right YAML and still run an older job pod created from an earlier spec. Inspect the actual pod or job created by the CronJob.

bash
1kubectl get pods -n ops
2kubectl describe pod POD_NAME -n ops
3kubectl get job -n ops
4kubectl get cronjob cleanup-job -n ops -o yaml

Look for the effective serviceAccountName on the pod and confirm the generated job corresponds to the latest spec.

Common Pitfalls

The most common mistake is binding the role to one service account while the pod uses the default service account.

Another frequent problem is namespace mismatch. A valid RoleBinding in the wrong namespace does not help the CronJob.

Developers also grant the wrong resource or API group. pods versus deployments, or the empty group versus apps, matters.

Finally, avoid debugging only from the CronJob YAML. The truth is in the running pod identity and in the exact Forbidden message returned by the API server.

Summary

  • Put serviceAccountName on the pod template inside the CronJob job template.
  • Keep the CronJob, ServiceAccount, Role, and RoleBinding aligned by namespace.
  • Use kubectl auth can-i to test the exact permission quickly.
  • Match the RBAC rule to the real verb, resource, and API group from the error.
  • Inspect the created pod to verify which service account it actually used.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design