1. Problem and Access Contract

Build a separate kubeconfig that reads Pods, Services, and ConfigMaps in namespace1 and namespace2, using one ServiceAccount identity and one token shared by two contexts. It should fail when reading those resources in namespace3 or other unapproved namespaces.

This example grants no writes, Secret access, Pod logs, exec, node access, or cluster-wide namespace listing. Existing cluster policies can grant more: Kubernetes RBAC is additive, so this is not a deny policy. Verify the final credential before handing it over. Standard discovery and self-access-check endpoints may remain accessible; “namespace-limited” does not mean the identity can make no other API requests.

Use an administrator workstation with a compatible kubectl and Python 3. The operator needs permission to create the ServiceAccount and RBAC objects, grant the role’s permissions, and request ServiceAccount tokens. Optional Secret creation and impersonation checks need their own permissions. Confirm the intended cluster first:

kubectl config current-context
kubectl get namespace namespace1 namespace2

Both namespaces must exist. In a disposable example cluster only, create missing namespaces before applying bindings:

kubectl create namespace namespace1
kubectl create namespace namespace2

2. Desired Architecture: One Identity

Create only default/limited-access-user. A RoleBinding in another namespace can reference that exact ServiceAccount. Creating identically named accounts in each target namespace creates different identities; it does not produce one shared identity.

One ServiceAccount bound in two namespaces
Each RoleBinding points to the same ClusterRole and the same ServiceAccount. The namespace of the binding scopes the grant; the subject namespace identifies the account.

The authenticated username is system:serviceaccount:default:limited-access-user. Its home namespace does not automatically grant it access to resources in default. A single token represents this identity across both allowed namespaces.

3. ClusterRole Definition Versus Binding Scope

CombinationEffect for this role
ClusterRole aloneDefines reusable rules; grants nothing by itself.
ClusterRole + RoleBinding in namespace1Grants these namespaced resource permissions in namespace1.
Same ClusterRole + RoleBinding in namespace2Adds the same grant in namespace2.
ClusterRole + ClusterRoleBindingWould grant these rules across all namespaces; unsuitable here.

A RoleBinding does not turn cluster-scoped resources such as Nodes or Namespaces into namespaced resources. This role intentionally contains only namespaced resource rules. Other grants to the account or its groups are combined with these permissions; there is no overriding RBAC deny. See the RBAC reference.

4. Create the ServiceAccount

Save as serviceaccount.yaml:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: limited-access-user
  namespace: default
automountServiceAccountToken: false
kubectl apply -f serviceaccount.yaml
kubectl get serviceaccount limited-access-user -n default

Expect the account to exist in default. Disabling automount avoids default credential injection into Pods using this account; it does not prevent explicit TokenRequest issuance. This account is intended for the external kubeconfig, not to be reused by unrelated workloads.

5. Create the Reusable ClusterRole

Save as clusterrole.yaml:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: custom-namespace-access
rules:
- apiGroups: [""]
  resources: ["pods", "services", "configmaps"]
  verbs: ["get", "list", "watch"]
kubectl apply -f clusterrole.yaml

The empty API group means core resources. get reads one object, list reads a collection, and watch follows changes. Subresources such as pods/log and pods/exec need separate rules. ConfigMaps and Pod specifications can still contain sensitive application configuration; remove any resource category that is not needed.

6. Bind the Role in Each Allowed Namespace

Save both documents as rolebindings.yaml:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: limited-access-user-read
  namespace: namespace1
subjects:
- kind: ServiceAccount
  name: limited-access-user
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: custom-namespace-access
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: limited-access-user-read
  namespace: namespace2
subjects:
- kind: ServiceAccount
  name: limited-access-user
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: custom-namespace-access
kubectl apply -f rolebindings.yaml
kubectl get rolebinding limited-access-user-read -n namespace1 -o yaml
kubectl get rolebinding limited-access-user-read -n namespace2 -o yaml

In each document, metadata.namespace is the namespace receiving access. subjects[].namespace: default identifies where the ServiceAccount exists; do not change it to namespace1 or namespace2. Both roleRef values point to the same reusable ClusterRole. Do not create a ClusterRoleBinding for this setup.

7. Generate the ServiceAccount Token

On Kubernetes 1.24+, creating a ServiceAccount normally does not automatically create a permanent token Secret. This old lookup can return an empty result:

kubectl get sa limited-access-user -n default \
  -o jsonpath='{.secrets[0].name}'

Modern Pod credentials use projected, expiring tokens obtained through TokenRequest, associated with the BoundServiceAccountTokenVolume approach. A manually created legacy token Secret is a separate mechanism. Do not populate the ServiceAccount’s secrets list merely to make the old lookup work. See ServiceAccount token administration.

Option 1: short-lived TokenRequest token — preferred

These illustrative commands print a bearer credential. Use a private terminal without command tracing or session capture. The generator in the next section captures the token internally instead; you do not need to run both routes.

kubectl create token limited-access-user -n default
# Alternative: request a particular lifetime
kubectl create token limited-access-user -n default --duration=24h

The API server chooses the actual expiration, which may differ from the requested duration. Without a custom audience, kubectl requests a token for the Kubernetes API server. A token embedded in a file does not auto-refresh: generate and securely distribute a replacement before expiration. Minting another token does not immediately revoke the old one. See kubectl create token.

Option 2: explicitly managed long-lived Secret

Only when a persistent credential is intentionally required, save this as serviceaccount-token.yaml. The Secret and ServiceAccount must share a namespace:

apiVersion: v1
kind: Secret
metadata:
  name: limited-access-user-token
  namespace: default
  annotations:
    kubernetes.io/service-account.name: limited-access-user
type: kubernetes.io/service-account-token
kubectl apply -f serviceaccount-token.yaml
kubectl get secret limited-access-user-token -n default

The token controller fills its data asynchronously. An empty token immediately after creation can mean it is not populated yet. This extraction prints the credential, so use it only in a private terminal; on Linux, GNU base64 accepts --decode:

kubectl get secret limited-access-user-token -n default \
  -o jsonpath='{.data.token}' | base64 --decode

Possession grants the identity’s effective permissions until this credential is revoked or otherwise invalidated. A manually created Secret token generally has no built-in expiration. Prefer the short-lived route where practical; the generator supports this Secret only as an explicit alternative.

8. Generate the Limited Kubeconfig

Inspect the selected administrator context locally. Standard credential fields are redacted by default, but plugin arguments, environment values, or proxy URLs can still be sensitive; review before sharing the output:

kubectl config view --minify

For reference, these assignments retrieve cluster metadata. --flatten embeds CA material when the source config references a CA file; --raw is needed for the unredacted CA data. Do not dump an entire raw administrator kubeconfig into a terminal or shared log.

CLUSTER_NAME=$(kubectl config view --minify -o jsonpath='{.clusters[0].name}')
SERVER_URL=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
CA_DATA=$(kubectl config view --raw --minify --flatten \
  -o jsonpath='{.clusters[0].cluster.certificate-authority-data}')

The output structure is one cluster, one user/token, and two contexts. The following is a conceptual template, not a usable credential:

apiVersion: v1
kind: Config
clusters:
- name: my-cluster
  cluster:
    server: https://kubernetes.example.com:6443
    certificate-authority-data: <CA_DATA>
users:
- name: limited-access-user
  user:
    token: <TOKEN>
contexts:
- name: namespace1
  context:
    cluster: my-cluster
    user: limited-access-user
    namespace: namespace1
- name: namespace2
  context:
    cluster: my-cluster
    user: limited-access-user
    namespace: namespace2
current-context: namespace1

Save the following complete generator as generate-limited-kubeconfig.sh. Run it in a private directory outside a Git checkout, using your administrator kubeconfig. It requests one short-lived token by default, writes a new file with mode 0600, and refuses to overwrite an existing path. JSON is also a valid kubeconfig representation; kubectl reads it normally.

#!/usr/bin/env bash
set -euo pipefail
set +x
umask 077

python3 <<'PYTHON'
import base64
import json
import os
import subprocess
from pathlib import Path

os.umask(0o077)
output = Path(os.environ.get("OUTPUT", "limited-access.kubeconfig"))
if output.exists() or output.is_symlink():
    raise SystemExit("Output exists; choose a new OUTPUT path.")

mode = os.environ.get("TOKEN_MODE", "short")
if mode not in {"short", "secret"}:
    raise SystemExit("TOKEN_MODE must be short or secret.")

def run(*args):
    return subprocess.check_output(["kubectl", *args], text=True).strip()

admin_context = run("config", "current-context")
def admin(*args):
    return run("--context", admin_context, *args)

def field(path):
    return admin("config", "view", "--raw", "--minify", "--flatten",
                 "-o", "jsonpath={" + path + "}")

cluster_name = field(".clusters[0].name")
server = field(".clusters[0].cluster.server")
ca_data = field(".clusters[0].cluster.certificate-authority-data")
skip_tls = field(".clusters[0].cluster.insecure-skip-tls-verify")
if not cluster_name or not server.startswith("https://"):
    raise SystemExit("A named cluster with an HTTPS endpoint is required.")
if skip_tls == "true" or not ca_data:
    raise SystemExit("Provide a trusted CA; insecure TLS configs are refused.")

cluster = {"server": server, "certificate-authority-data": ca_data}
for key in ("tls-server-name", "proxy-url"):
    value = field(".clusters[0].cluster." + key)
    if value:
        cluster[key] = value

if mode == "short":
    token = admin("create", "token", "limited-access-user", "-n", "default",
                  "--duration=" + os.environ.get("TOKEN_DURATION", "1h"))
else:
    encoded = admin("get", "secret", "limited-access-user-token", "-n", "default",
                    "-o", "jsonpath={.data.token}")
    if not encoded:
        raise SystemExit("Secret token is not populated; inspect it and retry.")
    token = base64.b64decode(encoded, validate=True).decode("utf-8").strip()
if not token:
    raise SystemExit("No token returned; no kubeconfig written.")

config = {
    "apiVersion": "v1", "kind": "Config",
    "clusters": [{"name": cluster_name, "cluster": cluster}],
    "users": [{"name": "limited-access-user", "user": {"token": token}}],
    "contexts": [
        {"name": ns, "context": {"cluster": cluster_name,
         "user": "limited-access-user", "namespace": ns}}
        for ns in ("namespace1", "namespace2")
    ],
    "current-context": "namespace1",
}
with output.open("x", encoding="utf-8") as handle:
    json.dump(config, handle, indent=2)
    handle.write("\n")
print("Created limited kubeconfig; verify access before sharing.")
PYTHON
bash generate-limited-kubeconfig.sh

Alternatively, after creating and populating the explicit Secret above, choose the persistent-token mode. Run only one mode for the intended output file:

TOKEN_MODE=secret bash generate-limited-kubeconfig.sh

The generator copies only cluster connection fields and the new ServiceAccount credential, not administrator users, client keys, or exec plugins. A copied proxy URL or TLS server name must also work for the recipient. It deliberately rejects missing CA data; clusters using system trust need an explicitly verified trust configuration before adapting it. Ensure the API endpoint is reachable from the receiving machine. Keep the full generated file private. The kubeconfig reference explains cluster, user, and context entries.

9. Switch Between Authorized Namespaces

kubectl --kubeconfig=limited-access.kubeconfig config get-contexts
kubectl --kubeconfig=limited-access.kubeconfig config use-context namespace1
kubectl --kubeconfig=limited-access.kubeconfig get pods
kubectl --kubeconfig=limited-access.kubeconfig config use-context namespace2
kubectl --kubeconfig=limited-access.kubeconfig get services

Both contexts use the same user entry and token. The namespace stored in a context is only a default. A caller can override it or edit the file, but that does not add permissions:

kubectl --kubeconfig=limited-access.kubeconfig get pods -n namespace3

Expect Forbidden if the credential is valid and no other grant permits the operation. RBAC is the security boundary; removing a context or omitting a namespace from the file does not revoke access.

10. Verify Allowed and Denied Operations

As an administrator allowed to impersonate this account, perform these preliminary checks:

kubectl auth can-i get pods \
  --as=system:serviceaccount:default:limited-access-user -n namespace1
kubectl auth can-i get pods \
  --as=system:serviceaccount:default:limited-access-user -n namespace2
kubectl auth can-i get pods \
  --as=system:serviceaccount:default:limited-access-user -n namespace3
yes
yes
no

An impersonation authorization error is not evidence that the target account lacks access. Group handling matters when auditing grants to groups: ServiceAccounts belong to system:serviceaccounts, system:serviceaccounts:default, and authenticated identities normally receive system:authenticated. Explicit --as-group checks require permission to impersonate those groups too. The real token is the decisive test of its authentication and effective permissions.

kubectl --kubeconfig=limited-access.kubeconfig get pods -n namespace1
kubectl --kubeconfig=limited-access.kubeconfig get pods -n namespace2
kubectl --kubeconfig=limited-access.kubeconfig get pods -n namespace3
kubectl --kubeconfig=limited-access.kubeconfig auth can-i create pods -n namespace1
kubectl --kubeconfig=limited-access.kubeconfig auth can-i get secrets -n namespace1
kubectl --kubeconfig=limited-access.kubeconfig auth can-i list namespaces

The first two return Pod lists or “No resources found”; the third should be Forbidden. The last three should answer no. Listing Pods requires list, whereas the initial preflight used get; this role contains both. Also verify Services and ConfigMaps as needed. Run expected-denial commands individually: they can return nonzero and stop a shell using set -e.

A request for all namespaces is a different authorization scope. Kubernetes does not filter an all-namespace list to the two allowed namespaces:

kubectl --kubeconfig=limited-access.kubeconfig get pods -A

For broader assurance, test the actual token against other existing namespaces and inspect all grants, including wildcard and group grants. Three sample checks do not prove absence of access everywhere.

11. Troubleshooting

SymptomCheck / correction
ServiceAccount exists, but no token SecretExpected on 1.24+. Use TokenRequest or intentionally create the annotated token Secret.
Secret exists, token field emptyAllow controller reconciliation; inspect account name/namespace, Secret type and annotation, and token-controller health.
Unauthorized / expired credentialReissue a token with an API-server audience and replace the private kubeconfig. Changing contexts cannot renew it.
Forbidden in an allowed namespaceCheck the exact verb/resource, subject namespace, roleRef, and binding namespace. Pods/log and other subresources are not in this role.
ClusterRole exists, but no accessIt defines rules only; check that a RoleBinding references it.
Cannot list namespaces or run get pods -AExpected: the example grants neither operation. Use the two known namespace contexts.
TLS error or connection timeoutCheck CA, certificate hostname, endpoint reachability, and proxy settings; do not disable certificate verification.
RoleBinding apply reports immutable roleRefRecreate the reviewed binding if changing its roleRef is intentional; its subject can be edited without that change.

For unexpected access, inspect bindings using administrator credentials:

kubectl get rolebindings -A
kubectl get clusterrolebindings
kubectl get rolebindings -A -o yaml
kubectl get clusterrolebindings -o yaml

Look for the exact ServiceAccount subject, its full username as a User subject, and grants to its groups. Review referenced roles for wildcard resources/verbs. Restricting this new ClusterRole does not subtract permissions granted elsewhere. Also consider other configured authorizers if RBAC alone does not explain the result.

kubectl auth can-i --list \
  --as=system:serviceaccount:default:limited-access-user -n '<namespace>'
kubectl --kubeconfig=limited-access.kubeconfig auth can-i --list -n namespace1

Treat these rule summaries as diagnostic aids; they can be incomplete with some authorizers. Test specific operations with the actual token. A working binding must still reference the account’s home namespace:

subjects:
- kind: ServiceAccount
  name: limited-access-user
  namespace: default

12. Credential Security and Revocation

Prefer short-lived tokens and least privilege. Treat the kubeconfig as a bearer credential: keep mode 0600, transfer it through an approved secret-sharing channel, and never commit it, paste it into tickets, or log its raw contents. Avoid wildcard verbs/resources. For production human access, prefer external authentication such as OIDC with individual identities and appropriate lifecycle controls.

To remove the grant for namespace2, an administrator deletes its binding. This revokes that binding’s permissions for every token of the account; review other bindings that could still allow access:

kubectl delete rolebinding limited-access-user-read -n namespace2

Deleting the namespace2 context from a local kubeconfig does not change server-side authorization. A recipient could recreate it or pass -n namespace2. Re-test with the real credential after changing RBAC.

For the manually created persistent token, deleting its Secret invalidates that Secret-backed credential for Kubernetes API authentication:

kubectl delete secret limited-access-user-token -n default

This does not revoke independently issued TokenRequest tokens. Those expire, or can be invalidated through applicable bound-object/ServiceAccount lifecycle mechanisms. Deleting the ServiceAccount invalidates its associated credentials, but affects every consumer of that identity. Removing a file from your machine never invalidates copies someone else holds. Do not promise instant termination of already-established watch streams.

13. Complete Execution Sequence

Save the three RBAC manifests and generator shown above. With namespace1 and namespace2 present and the correct administrator context selected, the recommended short-lived route is:

kubectl apply -f serviceaccount.yaml
kubectl apply -f clusterrole.yaml
kubectl apply -f rolebindings.yaml
bash generate-limited-kubeconfig.sh
kubectl --kubeconfig=limited-access.kubeconfig config get-contexts
kubectl --kubeconfig=limited-access.kubeconfig get pods -n namespace1
kubectl --kubeconfig=limited-access.kubeconfig get pods -n namespace2

Then perform the expected-denial tests in the verification section before sharing. No ClusterRoleBinding and no token Secret is required on this route. One execution requests one token; both contexts reference its single user entry.

To prepare a replacement without overwriting the current credential, select a new output filename. The requested duration is still subject to server policy:

OUTPUT=limited-access-next.kubeconfig TOKEN_DURATION=24h \
  bash generate-limited-kubeconfig.sh

Verify and distribute the replacement securely. The previous short-lived token can remain valid until its expiration unless separately invalidated.

14. Quick Command Reference

Inspect identity and namespace grants — administrator

kubectl get sa limited-access-user -n default
kubectl get clusterrole custom-namespace-access -o yaml
kubectl get rolebinding limited-access-user-read -n namespace1 -o yaml
kubectl get rolebinding limited-access-user-read -n namespace2 -o yaml

Generate a token-backed kubeconfig without displaying the token

bash generate-limited-kubeconfig.sh

Switch context and test scope — recipient

kubectl --kubeconfig=limited-access.kubeconfig config use-context namespace1
kubectl --kubeconfig=limited-access.kubeconfig get pods
kubectl --kubeconfig=limited-access.kubeconfig config use-context namespace2
kubectl --kubeconfig=limited-access.kubeconfig get pods
kubectl --kubeconfig=limited-access.kubeconfig auth can-i list pods -n namespace3

Expected final answer: no. Contexts make namespace selection convenient; RoleBindings determine where the identity is authorized.