## Intro

Kubernetes Service Account CI/CD automation turns manual identity management into a repeatable pipeline, but a pipeline that applies YAML without verification is just a script. This guide gives developers, DevOps consultants, and startup platform teams a practical path from baseline observation to verified change for service accounts, their roles, bindings, and token handling.

We focus on the daily operational loop: inspect the current state, make the smallest justified change, verify the outcome, and know how to recover. You will find concrete kubectl commands, version-scoped checks, and example manifests for each stage. The examples assume a working Kubernetes cluster (v1.24 or later) and a namespace named payments unless stated otherwise. The goal is operational safety: observe before changing, limit the blast radius, use placeholders instead of secrets, verify the result, and document recovery before an incident forces the decision.

## Version and Environment Inventory

Before touching service accounts, record the cluster's control plane and API versions. Many CI/CD failures start because the local kubectl client, the cluster API, or a manifest's apiVersion disagree. Begin with read-only commands and capture the output with timestamps.

### Check the Cluster and Client Version

Run kubectl version --short (or kubectl version on newer clients) to see the client and server versions. For example:

$ kubectl version --short
Client Version: v1.28.2
Server Version: v1.28.1 
 If your pipeline targets a cluster with an older API version, the manifest will fail. Service accounts, roles, and role bindings are stable v1 objects. But token request APIs differ: the TokenRequest API is available since v1.20, and the legacy Secret -based service account tokens are no longer auto-created since v1.24. Use kubectl api-versions | grep authentication to confirm token-related APIs.

### Inspect Existing Service Accounts and Dependencies

List service accounts in the target namespace:

kubectl get serviceaccounts -n payments 
 Expected output if only the default account exists:

NAME SECRETS AGE
default 0 45d 
 If a CI/CD pipeline expects a specific service account, confirm it exists with kubectl get serviceaccount ci-deployer -n payments -o yaml . The YAML will show its annotations, image pull secrets, and any auto-generated tokens. Record related RBAC objects: roles ( kubectl get roles -n payments ), role bindings ( kubectl get rolebindings -n payments ), and secrets if you still mount legacy tokens ( kubectl get secrets -n payments | grep ci-deployer ).

### Example Baseline Snapshot

Store a snapshot of the current identity configuration before any change. This is your recovery reference:

mkdir -p /tmp/sa-baseline && cd /tmp/sa-baseline
kubectl get serviceaccount ci-deployer -n payments -o yaml > sa-before.yaml
kubectl get role ci-deployer-role -n payments -o yaml > role-before.yaml
kubectl get rolebinding ci-deployer-binding -n payments -o yaml > binding-before.yaml 
 Use timestamps in filenames or a git commit to freeze the baseline. If the change fails, kubectl apply -f sa-before.yaml restores the previous service account. The recovery path is clear before you change anything.

<style>
.eno-quiz-widget{margin:2rem 0;padding:1.5rem;border-radius:12px;background:var(--eno-surface-lowest,#f7f7f8);border:1px solid var(--eno-border-soft,#e2e2e6);font-family:var(--eno-font-body,Inter,sans-serif)}
.eno-quiz-widget .eno-quiz-kicker{font-family:var(--eno-font-label,"JetBrains Mono",monospace);font-size:.75rem;letter-spacing:.05em;text-transform:uppercase;color:var(--eno-text-muted,#5f5f68);margin:0 0 .5rem}
.eno-quiz-widget .eno-quiz-question{font-family:var(--eno-font-heading,"Hanken Grotesk",sans-serif);font-size:1.0625rem;font-weight:600;margin:0 0 1rem;color:var(--eno-text-strong,#1a1a1f)}
.eno-quiz-widget .eno-quiz-options{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.5rem}
.eno-quiz-widget .eno-quiz-option{display:block;width:100%;min-height:44px;text-align:left;padding:.625rem .875rem;border-radius:8px;border:1.5px solid var(--eno-border-soft,#e2e2e6);background:#fff;font-size:.9375rem;cursor:pointer;transition:border-color var(--eno-motion-base,180ms ease-out),background var(--eno-motion-base,180ms ease-out)}
.eno-quiz-widget .eno-quiz-option:hover{border-color:var(--eno-primary,#0059bb)}
.eno-quiz-widget .eno-quiz-option:focus-visible{outline:none;box-shadow:0 0 0 3px rgb(0 89 187 / 0.15)}
.eno-quiz-widget .eno-quiz-option[aria-pressed="true"]{border-color:var(--eno-primary,#0059bb);background:rgb(0 89 187 / 0.06)}
.eno-quiz-widget .eno-quiz-option[data-correct="true"].eno-quiz-revealed{border-color:var(--eno-success,#17803a);background:rgb(23 128 58 / 0.08)}
.eno-quiz-widget .eno-quiz-option[data-correct="false"].eno-quiz-revealed.eno-quiz-was-selected{border-color:var(--eno-error,#ba1a1a);background:var(--eno-error-soft,#ffdad6)}
.eno-quiz-widget .eno-quiz-option-icon{display:inline-block;width:1.1em;margin-right:.4em;font-weight:700}
.eno-quiz-widget .eno-quiz-submit{margin-top:1rem;min-height:44px;padding:.5rem 1.25rem;border-radius:8px;border:none;background:var(--eno-primary,#0059bb);color:#fff;font-weight:600;font-size:.9375rem;cursor:pointer;transition:opacity var(--eno-motion-base,180ms ease-out)}
.eno-quiz-widget .eno-quiz-submit:disabled{opacity:.5;cursor:not-allowed}
.eno-quiz-widget .eno-quiz-submit:focus-visible{outline:none;box-shadow:0 0 0 3px rgb(0 89 187 / 0.15)}
.eno-quiz-widget .eno-quiz-explanation{margin-top:1rem;padding:.875rem 1rem;border-radius:8px;font-size:.9375rem;line-height:1.5;display:none}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-visible{display:block}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-correct{background:rgb(23 128 58 / 0.08);color:var(--eno-success,#17803a)}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-incorrect{background:var(--eno-error-soft,#ffdad6);color:var(--eno-error,#ba1a1a)}
@media (prefers-reduced-motion: reduce){.eno-quiz-widget *{transition:none!important}}
</style><div class="eno-quiz-widget" role="group" aria-label="Quick check question">
Quick check 1 of 2

What is a recommended way to validate Kubernetes service account credentials in your own code?

- TokenReview API
- OIDC discovery
- Manual JWT parsing
- Using kubectl auth can-i

<button type="button" class="eno-quiz-submit" disabled>Submit</button>
<div class="eno-quiz-explanation">The passage states that the Kubernetes project recommends using the TokenReview API for validating service account credentials, as it invalidates tokens bound to deleted API objects.</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
  document.querySelectorAll('.eno-quiz-widget:not([data-eno-quiz-bound])').forEach(function (widget) {
    widget.setAttribute('data-eno-quiz-bound', '1');
    var options = Array.prototype.slice.call(widget.querySelectorAll('.eno-quiz-option'));
    var submitBtn = widget.querySelector('.eno-quiz-submit');
    var explanation = widget.querySelector('.eno-quiz-explanation');
    var selected = null;
    options.forEach(function (opt) {
      opt.addEventListener('click', function () {
        if (widget.hasAttribute('data-eno-quiz-answered')) return;
        options.forEach(function (o) { o.setAttribute('aria-pressed', 'false'); });
        opt.setAttribute('aria-pressed', 'true');
        selected = opt;
        submitBtn.disabled = false;
      });
    });
    submitBtn.addEventListener('click', function () {
      if (!selected || widget.hasAttribute('data-eno-quiz-answered')) return;
      widget.setAttribute('data-eno-quiz-answered', '1');
      submitBtn.disabled = true;
      var correct = selected.getAttribute('data-correct') === 'true';
      options.forEach(function (o) {
        o.classList.add('eno-quiz-revealed');
        if (o === selected) o.classList.add('eno-quiz-was-selected');
        var icon = o.querySelector('.eno-quiz-option-icon');
        if (o.getAttribute('data-correct') === 'true') icon.textContent = '\u2713';
        else if (o === selected) icon.textContent = '\u2717';
      });
      explanation.classList.add('eno-quiz-visible', correct ? 'eno-quiz-correct' : 'eno-quiz-incorrect');
    });
  });
});
</script>

## Safe Configuration Path

Service accounts are harmless without roles and bindings, but a misconfigured binding can grant unintended access to a pod. The safe path is to create or update one object at a time, verify its effect, and only then proceed. This section covers the recommended sequence for a typical CI/CD service account.

### Step 1: Define the Service Account with Minimal Metadata

Create a service account for a deployment pipeline that will deploy applications in the payments namespace. Apply this YAML:

# sa-ci-deployer.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
 name: ci-deployer
 namespace: payments
 labels:
 app.kubernetes.io/name: payments
 app.kubernetes.io/component: deployer 
 Apply and check the result:

kubectl apply -f sa-ci-deployer.yaml
kubectl get serviceaccount ci-deployer -n payments -o wide 
 Expected output shows no secrets (since v1.24):

NAME SECRETS AGE
ci-deployer 0 2m 
 Do not set automountServiceAccountToken: false unless you are absolutely certain the workload never needs to authenticate to the Kubernetes API. For CI/CD jobs, the token is usually required, so leave it true (the default).

### Step 2: Create a Role with Least Privilege

Define a role that allows the CI/CD system to manage only deployments, services, and config maps in payments . This is a scoped role, not cluster-wide. Apply this YAML:

# role-ci-deployer.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
 name: ci-deployer-role
 namespace: payments
rules:
- apiGroups: ["apps"]
 resources: ["deployments"]
 verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
 resources: ["services", "configmaps"]
 verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] 
 Apply and inspect the role:

kubectl apply -f role-ci-deployer.yaml
kubectl describe role ci-deployer-role -n payments 
 Expected output includes each rule with resources and verbs. If you need to allow the pipeline to scale deployments, add "scale" to the verbs for deployments/scale under apps .

### Step 3: Bind the Role to the Service Account

A role is inert until bound. Use a RoleBinding to connect the service account to the role in the same namespace:

# rolebinding-ci-deployer.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
 name: ci-deployer-binding
 namespace: payments
subjects:
- kind: ServiceAccount
 name: ci-deployer
 namespace: payments
roleRef:
 kind: Role
 name: ci-deployer-role
 apiGroup: rbac.authorization.k8s.io 
 Apply the binding:

kubectl apply -f rolebinding-ci-deployer.yaml
kubectl get rolebinding ci-deployer-binding -n payments -o wide 
 Verify the subject is correct:

kubectl describe rolebinding ci-deployer-binding -n payments 
 Look for a line similar to:

Subjects:
 Kind Name Namespace
 ---- ---- ---------
 ServiceAccount ci-deployer payments 
 If the subject shows a different namespace or kind, the binding will not grant permissions. Fix it before moving on.

## Verification and Diagnostics

After applying the role and binding, you must prove that the service account actually has the intended permissions and that a pod using it can perform the allowed actions. Do not assume success from YAML apply output.

### Verify Permissions with kubectl auth can-i

Use kubectl auth can-i with --as to simulate the service account. For example, check if the CI/CD account can create deployments in payments :

kubectl auth can-i create deployments --as=system:serviceaccount:payments:ci-deployer -n payments 
 Expected result if the role is correct:

yes 
 Check a prohibited action, such as deleting a secret:

kubectl auth can-i delete secrets --as=system:serviceaccount:payments:ci-deployer -n payments 
 Expected result:

no 
 If you get yes for an unintended action, review the role rules and any cluster roles that might be bound to this service account.

### Verify a Pod Can Use the Service Account Token

Create a temporary pod that uses the service account and attempts a Kubernetes API call. Save the following as pod-test-sa.yaml :

apiVersion: v1
kind: Pod
metadata:
 name: sa-token-test
 namespace: payments
spec:
 serviceAccountName: ci-deployer
 containers:
 - name: kubectl
 image: bitnami/kubectl:latest
 command: ["sleep", "3600"]
 restartPolicy: Never 
 Run the pod, exec into it, and inspect the mounted token:

kubectl apply -f pod-test-sa.yaml
kubectl wait --for=condition=Ready pod/sa-token-test -n payments --timeout=60s
kubectl exec -it sa-token-test -n payments -- sh
# inside the container
cat /var/run/secrets/kubernetes.io/serviceaccount/token 
 The token should be a long JWT string. Attempt an API call:

kubectl exec -it sa-token-test -n payments -- sh -c 'kubectl auth can-i list deployments -n payments --token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)' 
 Expected output: yes . A no may indicate that the service account's token is not being projected or the role lacks the permission.

### Debugging Common Issues

- Pod cannot list deployments but kubectl auth can-i says yes : The pod's service account may not be correctly specified in the pod spec. Check kubectl get pod sa-token-test -n payments -o jsonpath='{.spec.serviceAccountName}' to confirm it matches.

- Token expired or empty : Since v1.24, service accounts do not create long-lived secrets by default. The projected token is short-lived and automatically rotated. For CI/CD, use a pod spec that requests a token with a longer expiration via automountServiceAccountToken or use the TokenRequest API. Alternatively, create a secret of type kubernetes.io/service-account-token manually, but prefer the projected token.

- RBAC denials in cluster logs : Enable audit logging temporarily to see why a request was denied. For an EKS cluster, you can use CloudTrail. For a self-managed cluster, check kube-apiserver audit logs. A typical denied request will show the user as system:serviceaccount:payments:ci-deployer and the missing verb/resource.

<style>
.eno-quiz-widget{margin:2rem 0;padding:1.5rem;border-radius:12px;background:var(--eno-surface-lowest,#f7f7f8);border:1px solid var(--eno-border-soft,#e2e2e6);font-family:var(--eno-font-body,Inter,sans-serif)}
.eno-quiz-widget .eno-quiz-kicker{font-family:var(--eno-font-label,"JetBrains Mono",monospace);font-size:.75rem;letter-spacing:.05em;text-transform:uppercase;color:var(--eno-text-muted,#5f5f68);margin:0 0 .5rem}
.eno-quiz-widget .eno-quiz-question{font-family:var(--eno-font-heading,"Hanken Grotesk",sans-serif);font-size:1.0625rem;font-weight:600;margin:0 0 1rem;color:var(--eno-text-strong,#1a1a1f)}
.eno-quiz-widget .eno-quiz-options{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.5rem}
.eno-quiz-widget .eno-quiz-option{display:block;width:100%;min-height:44px;text-align:left;padding:.625rem .875rem;border-radius:8px;border:1.5px solid var(--eno-border-soft,#e2e2e6);background:#fff;font-size:.9375rem;cursor:pointer;transition:border-color var(--eno-motion-base,180ms ease-out),background var(--eno-motion-base,180ms ease-out)}
.eno-quiz-widget .eno-quiz-option:hover{border-color:var(--eno-primary,#0059bb)}
.eno-quiz-widget .eno-quiz-option:focus-visible{outline:none;box-shadow:0 0 0 3px rgb(0 89 187 / 0.15)}
.eno-quiz-widget .eno-quiz-option[aria-pressed="true"]{border-color:var(--eno-primary,#0059bb);background:rgb(0 89 187 / 0.06)}
.eno-quiz-widget .eno-quiz-option[data-correct="true"].eno-quiz-revealed{border-color:var(--eno-success,#17803a);background:rgb(23 128 58 / 0.08)}
.eno-quiz-widget .eno-quiz-option[data-correct="false"].eno-quiz-revealed.eno-quiz-was-selected{border-color:var(--eno-error,#ba1a1a);background:var(--eno-error-soft,#ffdad6)}
.eno-quiz-widget .eno-quiz-option-icon{display:inline-block;width:1.1em;margin-right:.4em;font-weight:700}
.eno-quiz-widget .eno-quiz-submit{margin-top:1rem;min-height:44px;padding:.5rem 1.25rem;border-radius:8px;border:none;background:var(--eno-primary,#0059bb);color:#fff;font-weight:600;font-size:.9375rem;cursor:pointer;transition:opacity var(--eno-motion-base,180ms ease-out)}
.eno-quiz-widget .eno-quiz-submit:disabled{opacity:.5;cursor:not-allowed}
.eno-quiz-widget .eno-quiz-submit:focus-visible{outline:none;box-shadow:0 0 0 3px rgb(0 89 187 / 0.15)}
.eno-quiz-widget .eno-quiz-explanation{margin-top:1rem;padding:.875rem 1rem;border-radius:8px;font-size:.9375rem;line-height:1.5;display:none}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-visible{display:block}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-correct{background:rgb(23 128 58 / 0.08);color:var(--eno-success,#17803a)}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-incorrect{background:var(--eno-error-soft,#ffdad6);color:var(--eno-error,#ba1a1a)}
@media (prefers-reduced-motion: reduce){.eno-quiz-widget *{transition:none!important}}
</style><div class="eno-quiz-widget" role="group" aria-label="Quick check question">
Quick check 2 of 2

Which of the following is a suggested use case for service accounts according to the reference?

- Granting cross-namespace access to resources
- Storing database credentials for applications
- Encrypting network traffic between pods
- Providing DNS resolution for services

<button type="button" class="eno-quiz-submit" disabled>Submit</button>
<div class="eno-quiz-explanation">The passage lists granting cross-namespace access, such as allowing a Pod in namespace &#39;example&#39; to read Lease objects in &#39;kube-node-lease&#39;, as a use case for service accounts.</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
  document.querySelectorAll('.eno-quiz-widget:not([data-eno-quiz-bound])').forEach(function (widget) {
    widget.setAttribute('data-eno-quiz-bound', '1');
    var options = Array.prototype.slice.call(widget.querySelectorAll('.eno-quiz-option'));
    var submitBtn = widget.querySelector('.eno-quiz-submit');
    var explanation = widget.querySelector('.eno-quiz-explanation');
    var selected = null;
    options.forEach(function (opt) {
      opt.addEventListener('click', function () {
        if (widget.hasAttribute('data-eno-quiz-answered')) return;
        options.forEach(function (o) { o.setAttribute('aria-pressed', 'false'); });
        opt.setAttribute('aria-pressed', 'true');
        selected = opt;
        submitBtn.disabled = false;
      });
    });
    submitBtn.addEventListener('click', function () {
      if (!selected || widget.hasAttribute('data-eno-quiz-answered')) return;
      widget.setAttribute('data-eno-quiz-answered', '1');
      submitBtn.disabled = true;
      var correct = selected.getAttribute('data-correct') === 'true';
      options.forEach(function (o) {
        o.classList.add('eno-quiz-revealed');
        if (o === selected) o.classList.add('eno-quiz-was-selected');
        var icon = o.querySelector('.eno-quiz-option-icon');
        if (o.getAttribute('data-correct') === 'true') icon.textContent = '\u2713';
        else if (o === selected) icon.textContent = '\u2717';
      });
      explanation.classList.add('eno-quiz-visible', correct ? 'eno-quiz-correct' : 'eno-quiz-incorrect');
    });
  });
});
</script>

## Failure Modes and Recovery

Even with correct YAML, CI/CD automation can fail due to namespace absence, binding misconfiguration, or token issues. This section outlines common failure modes, how to detect them, and exact recovery steps.

### Failure 1: Namespace Does Not Exist

If the pipeline applies a manifest with namespace: payments but the namespace was never created, kubectl returns an error like:

Error from server (NotFound): namespaces "payments" not found 
 Detection : The CI/CD job fails immediately on apply. In a GitOps system like Argo CD, the sync status shows Unknown or Missing for the namespace.

Recovery : Create the namespace and reapply the manifests:

kubectl create namespace payments
kubectl apply -f sa-ci-deployer.yaml -f role-ci-deployer.yaml -f rolebinding-ci-deployer.yaml 
 Prevent this by adding a namespace creation step at the start of the pipeline or using a cluster-scoped manifest that includes the namespace.

### Failure 2: Role Binding Points to a Different Service Account

A common mistake is binding a role to the wrong subject. For example, the binding subject might have name: default instead of ci-deployer . The pod will start without errors but any API call requiring permissions will fail with 403 Forbidden .

Detection : Run kubectl auth can-i for the intended service account (as shown in Verification and Diagnostics) and check the binding's subject with kubectl get rolebinding ci-deployer-binding -n payments -o yaml . Look for the subjects array.

Recovery : Correct the subject in the RoleBinding YAML and reapply:

subjects:
- kind: ServiceAccount
 name: ci-deployer # corrected from default
 namespace: payments 
 Then reapply:

kubectl apply -f rolebinding-ci-deployer.yaml 
 Verify again with kubectl auth can-i .

### Failure 3: Legacy Secret Token Missing After Cluster Upgrade

If you upgraded from Kubernetes v1.23 to v1.24 or later, existing service accounts may lose their auto-created secrets. A pipeline that references a secret by name for a service account token will fail to find it.

Detection : The pipeline logs show an error like Secret "ci-deployer-token-abcde" not found . kubectl get serviceaccount ci-deployer -n payments -o yaml no longer lists the secret under secrets .

Recovery : Use the projected token instead. If you absolutely need a long-lived token, create a secret manually and link it to the service account:

# sa-token-secret.yaml
apiVersion: v1
kind: Secret
metadata:
 name: ci-deployer-token
 namespace: payments
 annotations:
 kubernetes.io/service-account.name: ci-deployer
type: kubernetes.io/service-account-token 
 Then patch the service account to include the secret:

kubectl apply -f sa-token-secret.yaml
kubectl patch serviceaccount ci-deployer -n payments -p '{"secrets": [{"name": "ci-deployer-token"}]}' 
 However, this approach has security risks and is discouraged. Prefer updating the CI/CD system to authenticate via the projected token or using a short-lived token obtained from the TokenRequest API.

### Rollback Strategy

Because you saved the baseline YAML files at the start, rollback is a simple kubectl apply of the previous state. For example, to revert a role change:

kubectl apply -f /tmp/sa-baseline/role-before.yaml 
 If the change was applied via a pipeline, use the pipeline's rollback feature or a Git revert commit. Always re-run the verification step after rollback to confirm the previous permissions are restored.

## Operations Checklist

Use this checklist before, during, and after any service account change in CI/CD. It is designed to be pasted into a runbook or issue template and filled with concrete values.

### Pre-Change Checklist

kubectl get serviceaccount ci-deployer -n payments -o yaml > /tmp/sa-baseline/sa-before.yaml kubectl get role ci-deployer-role -n payments -o yaml > /tmp/sa-baseline/role-before.yaml kubectl get rolebinding ci-deployer-binding -n payments -o yaml > /tmp/sa-baseline/binding-before.yaml

- [ ] Confirm cluster version with kubectl version --short and note any API differences.

- [ ] Verify target namespace exists: kubectl get namespace payments .

- [ ] List current service accounts and RBAC objects in the namespace: kubectl get sa,role,rolebinding -n payments .

- [ ] Record baseline YAML for the specific objects you will change:

- [ ] Confirm the change is scoped to a single namespace or object set.

- [ ] Note the recovery procedure: restore baseline YAML.

### Change Execution Checklist

- [ ] Apply the service account manifest: kubectl apply -f sa-ci-deployer.yaml

- [ ] Verify the service account is created: kubectl get serviceaccount ci-deployer -n payments

- [ ] Apply the role manifest: kubectl apply -f role-ci-deployer.yaml

- [ ] Verify the role rules: kubectl describe role ci-deployer-role -n payments

- [ ] Apply the role binding: kubectl apply -f rolebinding-ci-deployer.yaml

- [ ] Verify the binding subject: kubectl get rolebinding ci-deployer-binding -n payments -o yaml

- [ ] Test permissions with kubectl auth can-i for at least one allowed and one denied action.

- [ ] (Optional) Run a temporary pod using the service account to test token-based access.

### Post-Change Verification Checklist

- [ ] Confirm kubectl auth can-i results match expectations for all key actions.

- [ ] Check that no unintended cluster-scoped permissions were granted ( kubectl get clusterrolebinding -o yaml | grep ci-deployer should return nothing).

- [ ] Update documentation with the new service account name, permissions, and usage example.

- [ ] Tag the change in git for auditability.

- [ ] If the change is part of a CI/CD pipeline, run the pipeline's own verification step (e.g., a smoke test deployment using the service account).

### Example Filled Checklist for a Deployment Pipeline Upgrade

Here is an example of a completed checklist for a specific change: creating a new service account ci-deployer for the payments backend deployment pipeline.

<div class="my-stack-md overflow-x-auto">
<table class="min-w-[42rem] border-collapse text-left">
<thead><tr><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Item</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Check</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Output or Note</th></tr></thead>
<tbody><tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Cluster version</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">kubectl version --short</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">v1.28.1</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Namespace exists</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">kubectl get namespace payments</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Active</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Baseline saved</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">git commit 4f2a1c</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Files in /tmp/sa-baseline</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">SA applied</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">kubectl apply -f sa-ci-deployer.yaml</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">serviceaccount/ci-deployer created</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Role applied</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">kubectl apply -f role-ci-deployer.yaml</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">role.rbac.authorization.k8s.io/ci-deployer-role created</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Binding applied</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">kubectl apply -f rolebinding-ci-deployer.yaml</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">rolebinding.rbac.authorization.k8s.io/ci-deployer-binding created</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Allowed action</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">kubectl auth can-i create deployments --as=... -n payments</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">yes</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Denied action</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">kubectl auth can-i delete secrets --as=... -n payments</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">no</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Pipeline smoke test</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Run deployment job</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Deployment successful</td></tr></tbody>
</table>
</div>

## Conclusion

Kubernetes Service Account CI/CD automation is valuable only when each step is version-scoped, observable, and reversible. Copying a command without checking prerequisites and expected output is not an operations procedure; it is a gamble.

Start with one low-risk verification: choose a service account, record its current state with the baseline commands, run the documented checks, compare the result with the expected signal, and review dependencies such as roles, role bindings, and secrets.

A reliable technical workflow makes failure visible, protects sensitive values, limits changes to the intended resource, and defines recovery verification before an incident forces the decision. Use the checklist in this guide as a living document: adjust it as your cluster evolves and your CI/CD pipeline matures.

Your next step is to test this process in a development namespace. Create a test service account, bind a minimal role, run the verification commands, and intentionally break a binding to practice recovery. Once the loop feels natural, apply the same rigor to production changes.