As soon as you run more than one Kubernetes cluster (production and staging, or one cluster per region), you need a reliable way to reach each of them and to know which one a command will hit. In this tutorial you will merge several kubeconfig files into one, switch clusters and namespaces with kubectl contexts and kubectx/kubens, build a separate read-only kubeconfig for automation, and run a command against every cluster at once. Everything runs on an Ubuntu 24.04 workstation.
Prerequisites
To follow this guide you need:
- An Ubuntu 24.04 workstation (or a CubePath VPS used as a jump host) with
kubectlinstalled. Check withkubectl version --client. - Admin kubeconfig files for two or more clusters. On kubeadm clusters it is
/etc/kubernetes/admin.confon a control plane node; on k3s it is/etc/rancher/k3s/k3s.yaml. - Network access from the workstation to each cluster's API server, usually TCP port 6443.
This guide uses two example clusters called prod and staging. Replace the names, hosts and paths with your own.
Step 1 - Understanding the kubeconfig structure
A kubeconfig file has three lists and one pointer:
| Section | What it holds |
|---|---|
clusters | API server URL and the CA certificate used to trust it |
users | Credentials: client certificate, token or an exec plugin |
contexts | A named combination of one cluster, one user and an optional default namespace |
current-context | The context kubectl uses when you do not pass --context |
Switching clusters means switching contexts. Check what your current file contains:
kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
* kubernetes-admin@kubernetes kubernetes kubernetes-admin
Most installers name everything kubernetes or default. If you merge two files with identical names, entries from the first file win and the second cluster silently disappears. That is why every file gets unique names before merging.
Step 2 - Collecting and renaming the kubeconfig files
Create a directory for the individual files with restrictive permissions, since they contain admin credentials:
mkdir -p ~/.kube/clusters
chmod 700 ~/.kube ~/.kube/clusters
Copy the admin kubeconfig of each cluster. With kubeadm the file is only readable by root, so read it with sudo cat over SSH (this needs passwordless sudo on the server; otherwise copy the file to your home directory on the server first and fetch it with scp):
ssh your_user@prod_control_plane_ip 'sudo cat /etc/kubernetes/admin.conf' > ~/.kube/clusters/prod.yaml
ssh your_user@staging_control_plane_ip 'sudo cat /etc/kubernetes/admin.conf' > ~/.kube/clusters/staging.yaml
chmod 600 ~/.kube/clusters/*.yaml
For k3s, use /etc/rancher/k3s/k3s.yaml and then replace https://127.0.0.1:6443 in the copied file with the server's real address.
Now give each file unique names. Renaming the context is one command:
kubectl --kubeconfig ~/.kube/clusters/prod.yaml config rename-context kubernetes-admin@kubernetes prod
kubectl --kubeconfig ~/.kube/clusters/staging.yaml config rename-context kubernetes-admin@kubernetes staging
The cluster and user entries have no rename command, so edit them with sed, which also updates the references inside the context. For the production file:
sed -i -e 's/^ name: kubernetes$/ name: prod/' \
-e 's/cluster: kubernetes$/cluster: prod/' \
-e 's/^- name: kubernetes-admin$/- name: prod-admin/' \
-e 's/user: kubernetes-admin$/user: prod-admin/' \
~/.kube/clusters/prod.yaml
Run the same command on staging.yaml, replacing prod with staging. Then verify each file still works on its own:
kubectl --kubeconfig ~/.kube/clusters/prod.yaml get nodes
kubectl --kubeconfig ~/.kube/clusters/staging.yaml get nodes
Both commands should list the nodes of the right cluster. If one fails with context was not found or user not found, open the file with nano and check that the names in contexts match those in clusters and users.
Step 3 - Merging the files into one kubeconfig
kubectl can read several files at once through the KUBECONFIG variable, a colon-separated list. Combined with kubectl config view --flatten, which embeds all certificates, this produces a single self-contained file.
Back up your current config first, if you have one:
cp ~/.kube/config ~/.kube/config.bak 2>/dev/null || true
Merge the files into a temporary file and move it into place. Never redirect the output directly to ~/.kube/config while kubectl is reading it, or you end up with an empty file:
KUBECONFIG=~/.kube/clusters/prod.yaml:~/.kube/clusters/staging.yaml kubectl config view --flatten > /tmp/kubeconfig.merged
install -m 600 /tmp/kubeconfig.merged ~/.kube/config
rm /tmp/kubeconfig.merged
List the contexts again:
kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
* prod prod prod-admin
staging staging staging-admin
When you add a third cluster later, prepare its file as in Step 2 and repeat the merge with ~/.kube/config as the first entry of KUBECONFIG.
TipInstead of merging, you can export
KUBECONFIGwith the list of files in~/.bashrc. Merging is simpler to back up and works with tools that only read~/.kube/config.
Step 4 - Switching clusters and namespaces
Select the cluster you want to work with:
kubectl config use-context staging
kubectl config current-context
Switched to context "staging".
staging
The selected context is saved in the kubeconfig file, so it also applies to every other terminal. For one-off commands it is safer to pass the context explicitly, which does not change the global state:
kubectl --context prod get nodes
Each context can also carry a default namespace, which saves typing -n all the time:
kubectl config set-context staging --namespace=my-app
The NAMESPACE column of kubectl config get-contexts now shows my-app for staging.
Step 5 - Installing kubectx and kubens
kubectx and kubens are two small tools that shorten context and namespace switching. Ubuntu 24.04 packages them in the universe repository:
sudo apt update
sudo apt install kubectx
The package installs both commands. Switch contexts and namespaces:
kubectx prod
kubens kube-system
Switched to context "prod".
Context "prod" modified.
Active namespace is "kube-system".
Other useful forms:
kubectx
kubectx -
kubectx -c
kubens -
kubectx alone lists the contexts and highlights the current one, kubectx - returns to the previous context, kubectx -c prints the current one, and kubens - returns to the previous namespace. If fzf is installed (sudo apt install fzf), running kubectx or kubens without arguments opens an interactive picker instead.
You can also shorten long context names: kubectx p=prod renames the prod context to p.
Step 6 - Creating a read-only kubeconfig for automation
Do not give CI pipelines, dashboards or colleagues a copy of the admin kubeconfig. Create a dedicated ServiceAccount with only the permissions it needs and a kubeconfig that uses its token. This example gives read-only access with the built-in view ClusterRole.
Run these commands against the target cluster:
kubectl --context prod create namespace automation
kubectl --context prod -n automation create serviceaccount ci-reader
kubectl --context prod create clusterrolebinding ci-reader-view --clusterrole=view --serviceaccount=automation:ci-reader
Since Kubernetes 1.24, ServiceAccounts no longer get a token Secret automatically. Create a long-lived token Secret explicitly:
nano ci-reader-token.yaml
apiVersion: v1
kind: Secret
metadata:
name: ci-reader-token
namespace: automation
annotations:
kubernetes.io/service-account.name: ci-reader
type: kubernetes.io/service-account-token
kubectl --context prod apply -f ci-reader-token.yaml
Read the token, the CA certificate and the API server URL into shell variables:
TOKEN=$(kubectl --context prod -n automation get secret ci-reader-token -o jsonpath='{.data.token}' | base64 -d)
CA_DATA=$(kubectl --context prod -n automation get secret ci-reader-token -o jsonpath='{.data.ca\.crt}')
SERVER=$(kubectl config view --minify --context prod -o jsonpath='{.clusters[0].cluster.server}')
Build a new kubeconfig file with those values. The --kubeconfig flag makes sure your own config is not modified:
KCFG=~/ci-reader-prod.kubeconfig
kubectl --kubeconfig "$KCFG" config set-cluster prod --server="$SERVER"
kubectl --kubeconfig "$KCFG" config set clusters.prod.certificate-authority-data "$CA_DATA"
kubectl --kubeconfig "$KCFG" config set-credentials ci-reader --token="$TOKEN"
kubectl --kubeconfig "$KCFG" config set-context ci-reader@prod --cluster=prod --user=ci-reader
kubectl --kubeconfig "$KCFG" config use-context ci-reader@prod
chmod 600 "$KCFG"
Check what the new identity can and cannot do:
kubectl --kubeconfig ~/ci-reader-prod.kubeconfig get pods -A | head -n 3
kubectl --kubeconfig ~/ci-reader-prod.kubeconfig auth can-i delete pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-7c65d6cfc9-5hnbw 1/1 Running 0 12d
kube-system coredns-7c65d6cfc9-kqz8x 1/1 Running 0 12d
no
Listing pods works and deleting is denied. Store this file as a secret in your CI system. To revoke access, delete the Secret; the token stops working immediately.
NoteWhen a tool can request tokens on demand, prefer short-lived tokens with
kubectl -n automation create token ci-reader --duration=1hinstead of a long-lived Secret.
Step 7 - Running a command against every cluster
For quick checks across your fleet, loop over the contexts. kubectl config get-contexts -o name prints only the names, one per line:
for ctx in $(kubectl config get-contexts -o name); do
echo "== ${ctx}"
kubectl --context "$ctx" get nodes --no-headers
done
== prod
prod-cp-1 Ready control-plane 40d v1.33.4
prod-worker-1 Ready <none> 40d v1.33.4
== staging
staging-cp-1 Ready control-plane 12d v1.33.4
The loop uses --context on every call, so it never changes your current context. Use it for read-only checks such as versions, node status or certificate expiry; for changes, apply manifests to each cluster explicitly or use a GitOps tool.
Choosing tools for larger fleets
Contexts and kubectx are enough for a handful of clusters managed by a small team. As the number of clusters grows, these tools cover the next problems:
| Need | Tool |
|---|---|
| Same applications deployed to many clusters from Git | Argo CD (ApplicationSets) or Flux |
| Web UI, central RBAC and cluster import | Rancher |
| Creating and upgrading the clusters themselves declaratively | Cluster API |
Each of them builds on the kubeconfig and RBAC concepts from this guide: they still need a credential per cluster, ideally a dedicated ServiceAccount rather than an admin certificate.
Troubleshooting
Unable to connect to the server: x509: certificate is valid for ..., not .... You are reaching the API server through an address that is not in its certificate, typically a public IP when the certificate lists only private ones. Use an address included in the certificate, or add it to the API server's certificate SANs (on kubeadm, apiServer.certSANs).
A context disappeared after merging. Two files used the same cluster, user or context name. Rename the entries as in Step 2 and merge again from your backup files.
error: You must be logged in to the server (Unauthorized). The credential expired or was revoked. kubeadm admin client certificates are valid for one year; renew them with sudo kubeadm certs renew admin.conf on the control plane and copy the file again.
Conclusion
You now have a single kubeconfig with clearly named contexts, fast switching with kubectx and kubens, a least-privilege kubeconfig for automation, and a safe way to query all clusters at once. Next, consider adding the current context to your shell prompt so you always see which cluster you are on, moving deployments to a GitOps tool, and replacing long-lived tokens with short-lived ones where your tooling supports it.
