Skip to main content
· Kubernetes · 4 min read

Kubernetes RBAC: Granting Read-Only Developer Access via ServiceAccount Tokens

When developers need to view logs, check pod status, or inspect resources in a production-like cluster, the instinct is to give them the admin kubeconfig. That’s unnecessary blast radius. With RBAC, you can give exactly the access that’s needed — nothing more.

The RBAC Permission Model

%%{ init: { 'look': 'handDrawn' } }%%
graph TD
    A[Developer] -->|uses| B[kubeconfig<br/>with ServiceAccount token]
    B --> C[API Server<br/>authentication]
    C -->|token identifies| D[ServiceAccount<br/>dev-reader]
    D --> E[RoleBinding<br/>binds to ClusterRole]
    E --> F[ClusterRole<br/>read-only permissions]
    F -->|allows| G[get/list/watch<br/>pods, logs, events]
    F -->|denies| H[delete/create<br/>any resource]

The flow: developer authenticates with a token → API server maps token to ServiceAccount → RoleBinding links ServiceAccount to ClusterRole → ClusterRole defines allowed verbs on allowed resources.

The Setup Script

#!/bin/bash
set -euo pipefail

# =============================================================================
# CONFIGURATION — Required environment variables
# =============================================================================
: "${K8S_CONTEXT:?Need K8S_CONTEXT}"
: "${CLUSTERNAME:?Need CLUSTERNAME}"
: "${NAMESPACE:?Need NAMESPACE}"
: "${USERNAME:?Need USERNAME}"

# =============================================================================
# STEP 1 — Create namespace
# =============================================================================
kubectl --context "$K8S_CONTEXT" create namespace "$NAMESPACE" --dry-run=client -o yaml | \
  kubectl --context "$K8S_CONTEXT" apply -f -

# =============================================================================
# STEP 2 — Create ServiceAccount
# =============================================================================
kubectl --context "$K8S_CONTEXT" apply -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ${USERNAME}
  namespace: ${NAMESPACE}
EOF

# =============================================================================
# STEP 3 — Create ClusterRole with read-only permissions
# =============================================================================
kubectl --context "$K8S_CONTEXT" apply -f - <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: ${USERNAME}-read-only
rules:
- apiGroups: [""]
  resources: ["pods", "pods/log", "pods/status", "services", "events", "configmaps", "secrets"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
  resources: ["deployments", "replicasets", "statefulsets", "daemonsets"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["batch"]
  resources: ["jobs", "cronjobs"]
  verbs: ["get", "list", "watch"]
EOF

# =============================================================================
# STEP 4 — Bind ClusterRole to ServiceAccount (cluster-wide read)
# =============================================================================
kubectl --context "$K8S_CONTEXT" apply -f - <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: ${USERNAME}-read-only-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: ${USERNAME}-read-only
subjects:
- kind: ServiceAccount
  name: ${USERNAME}
  namespace: ${NAMESPACE}
EOF

# =============================================================================
# STEP 5 — Retrieve ServiceAccount token
# =============================================================================
SECRET=$(kubectl --context "$K8S_CONTEXT" get sa "$USERNAME" -n "$NAMESPACE" \
  -o jsonpath='{.secrets[0].name}')
TOKEN=$(kubectl --context "$K8S_CONTEXT" get secret "$SECRET" -n "$NAMESPACE" \
  -o jsonpath='{.data.token}' | base64 -d)
CA_CERT=$(kubectl --context "$K8S_CONTEXT" get secret "$SECRET" -n "$NAMESPACE" \
  -o jsonpath='{.data.ca\.crt}' | base64 -d)

# =============================================================================
# STEP 6 — Generate kubeconfig file
# =============================================================================
cat > "${USERNAME}-kubeconfig" <<EOF
apiVersion: v1
kind: Config
preferences: {}
clusters:
- name: ${CLUSTERNAME}
  cluster:
    certificate-authority-data: ${CA_CERT}
    server: https://${CLUSTERNAME}:6443
contexts:
- name: ${USERNAME}@${CLUSTERNAME}
  context:
    cluster: ${CLUSTERNAME}
    user: ${USERNAME}
    namespace: ${NAMESPACE}
current-context: ${USERNAME}@${CLUSTERNAME}
users:
- name: ${USERNAME}
  user:
    token: ${TOKEN}
EOF

# =============================================================================
# SUMMARY
# =============================================================================
echo "Kubeconfig written to ${USERNAME}-kubeconfig"
echo "Insecure skip TLS verify is NOT used — TLS cert is embedded correctly"

Usage

# Set required variables and run
K8S_CONTEXT=prod-cluster \
CLUSTERNAME=prod.example.com \
NAMESPACE=dev-team \
USERNAME=alice \
./setup-k8s-rbac.sh

# Distribute the generated kubeconfig
kubectl --kubeconfig ./alice-kubeconfig get pods

What This Permissions Covers

%%{ init: { 'look': 'handDrawn' } }%%
graph TD
    A[alice-kubeconfig] -->|can| B[kubectl get pods]
    A -->|can| C[kubectl logs &lt;pod&gt;]
    A -->|can| D[kubectl describe pod]
    A -->|can| E[kubectl top pod]
    A -->|cannot| F[kubectl delete pod]
    A -->|cannot| G[kubectl exec -it]
    A -->|cannot| H[kubectl apply]
    A -->|cannot| I[Read secret values]

The developer can see what’s running, inspect logs, and check resource status — but cannot modify anything, exec into pods (which would bypass RBAC), or read secret contents (secrets require separate explicit permission).

Scoping to a Single Namespace

If the developer only needs access to one namespace, use RoleBinding instead of ClusterRoleBinding:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: developer-read-only-binding
  namespace: frontend
subjects:
  - kind: ServiceAccount
    name: alice
    namespace: dev-team
roleRef:
  kind: ClusterRole
  name: ${USERNAME}-read-only
  apiGroup: rbac.authorization.k8s.io

This restricts the developer to the frontend namespace only — useful for multi-tenant clusters where teams should only see their own workloads.

Key Security Notes

  • TLS verification is embedded in the kubeconfig — the script extracts the cluster’s CA cert and includes it in the kubeconfig. Using --insecure-skip-tls-verify would defeat the purpose.
  • Tokens don’t expire by default in k3s (unlike GKE which has token rotation). For production use, consider rotating tokens periodically or using a tool like t tokens from kubelogin for short-lived OIDC tokens.
  • Audit logging — every API call with this token is logged in the Kubernetes audit log, so you can trace exactly what a developer queried.