click the screen · press Enter
← back to blog
Kubernetes Journey · Topic 8

A Kubeconfig Is a Credential Wearing a Config File's Clothes

Kubeconfig and Kubernetes API groups explained - Kubernetes Journey

After Topic 7 I could talk to the API server with raw certificates, and it was miserable. Every single command meant typing out three file paths. There had to be a better way, and there is: the kubeconfig file. Then, once you can actually reach the API, the next question is how the thing is laid out, which is where API groups come in. Two topics today, and they're a natural pair, because both are really about the same thing: how you talk to a cluster, and how an attacker who steals your access does too.

I'll keep this simple. For each idea: what it is, how it works, why it matters, and the one thing to take away.

The pain kubeconfig removes

Here's what talking to the API by hand looks like. You point curl at the API server and hand it your client key, client certificate and the CA certificate so both sides trust each other:

curl https://my-kube-playground:6443/api/v1/pods \
  --key admin.key \
  --cert admin.crt \
  --cacert ca.crt

The server checks those and answers. An empty cluster just gives you back an empty list:

{
  "kind": "PodList",
  "apiVersion": "v1",
  "items": []
}

kubectl can take the same details as flags, which is a little nicer but still a mouthful:

kubectl get pods \
  --server my-kube-playground:6443 \
  --client-key admin.key \
  --client-certificate admin.crt \
  --certificate-authority ca.crt

Typing that before every command is the kind of thing that makes you quietly give up on security and hardcode something dumb. So Kubernetes gives you a file that remembers it all: the kubeconfig. Drop it at the default path $HOME/.kube/config and kubectl reads it automatically, no flags, no objects to create. That's the whole pitch. Now let's open it up.

The three pieces inside every kubeconfig

A kubeconfig is just YAML with three lists in it, plus a pointer to say which one you're using right now. Once these three click, the whole file stops looking scary.

A context is the glue
CONTEXT = cluster + user (+ namespace)
clusters · where to connect (API URL + CA)
users · who you are (cert, key or token)
contexts · pair a user with a cluster
current-context · the one you're using now
You never "use" a cluster or a user directly. You use a context, which points at one of each.

Clusters answer "where do I connect?". Each entry is an API server URL plus the CA certificate you trust for it. You might have dev, staging and prod all listed here.

Users answer "who am I?". Each entry holds authentication material: a client certificate and key, a bearer token, or an exec plugin that fetches a cloud token. This is the sensitive half.

Contexts answer "which user talks to which cluster?". A context is nothing more than a named pairing of one cluster and one user, optionally with a default namespace. The context admin@production means "log into the production cluster as admin".

current-context is the single line that says which context is live right now. It's the first thing I check on any box, because it tells me who I'm about to act as. Here's a trimmed real file so you can see the shape:

apiVersion: v1
kind: Config
current-context: dev-user@google

clusters:
- name: production
  cluster:
    server: https://prod-api.example.com:6443
    certificate-authority: prod-ca.crt

users:
- name: dev-user
  user:
    client-certificate: dev-user.crt
    client-key: dev-user.key

contexts:
- name: dev-user@google
  context:
    cluster: google
    user: dev-user
    namespace: development

Clusters and users are the raw ingredients, a context is the recipe that combines them, and current-context is the plate in front of you.

Driving kubeconfig from the command line

You rarely hand-edit this file. kubectl has a whole config sub-command for it. To see what you've got loaded:

kubectl config view
# or point at a specific file
kubectl config view --kubeconfig=my-custom-config

To find out who you currently are before you touch anything:

kubectl config current-context

To switch identity, you change the active context. This one line can move you from a harmless dev cluster to production, so read it twice:

kubectl config use-context prod-user@production
What is a kubeconfig, in one line ~/.kube/config tells kubectl (and any client) which cluster to talk to, who you are, and therefore what you're allowed to do. Anyone who can read it can potentially become you.

A context can also carry a default namespace, which saves you typing --namespace on every command. Add one field and kubectl quietly scopes everything to, say, finance:

contexts:
- name: admin@production
  context:
    cluster: production
    user: admin
    namespace: finance

If you keep a separate kubeconfig and don't want to pass --kubeconfig every time, point the KUBECONFIG environment variable at it and make it stick across shells by adding it to your shell profile:

# in ~/.bashrc
export KUBECONFIG=$HOME/my-kube-config

# reload the current shell
source ~/.bashrc

Certificates: by path or baked in

In the cluster and user blocks you can reference certificates two ways. Either a file path, which is portable-ish but assumes the file exists on that machine, or the certificate content itself, base64-encoded, using the -data variants (certificate-authority-data, client-certificate-data, client-key-data). You use one or the other for a given entry, not both:

# path
certificate-authority: /etc/kubernetes/pki/ca.crt

# OR embedded (base64 of the PEM)
certificate-authority-data: LS0tLS1CRUdJTiBDRVJU...

If you ever pull a kubeconfig off a box and see that base64 blob, remember it decodes straight back to the PEM. Embedded creds mean the whole file is self-contained, which is convenient for you and just as convenient for anyone who walks off with it.

Why a kubeconfig is an attacker's dream

This is the part I care about most as a pentester. A kubeconfig is not config, it's a credential. Treat it like an SSH private key. And unlike an SSH key, it often carries several identities and several clusters in one tidy file.

One stolen file, four moves
01
Steal

Grab it from a dev laptop, CI runner or jump host, often world-readable.

02
Orient

Check current-context and the contexts list to see who and where you are.

03
Escalate

Even a low-priv user may list secrets or create pods if RBAC is loose.

04
Pivot

Multiple contexts mean one file can unlock several clusters.

Credential theft, privilege escalation, lateral movement, and if they get write access, persistence by adding their own user and context.

So when I land on a machine, the kubeconfig is one of the first things I look at. What I check, roughly in order:

  • File permissions on ~/.kube/config. Is it readable by more than its owner? It usually is.
  • Does a non-admin box somehow have an admin or prod context sitting in it?
  • Long-lived client certificates or tokens that never expire.
  • Over-privileged users, which really means loose RBAC bindings behind those user entries.
  • CI/CD kubeconfigs stored as plaintext secrets in a pipeline.
  • Whether you can switch context and act with zero re-auth or MFA (you almost always can).
Defender takeaway Lock ~/.kube/config to 600, keep prod contexts off developer and CI machines, prefer short-lived tokens over long-lived client certs, and remember that "we use RBAC" means nothing if the bindings behind these users are generous.

Now the map: Kubernetes API groups

Kubeconfig gets you to the API. API groups are how the API itself is organised, and you need that map before authorisation (RBAC) makes any sense, because RBAC rules are written in terms of groups, resources and verbs.

Everything in Kubernetes is the API. kubectl is just a friendly wrapper around REST calls. You can prove it by asking the server its version directly:

curl https://kube-master:6443/version
{
  "major": "1",
  "minor": "13",
  "gitVersion": "v1.13.0",
  ...
}

The API is split into groups by function. A handful of top-level paths give you the lay of the land: /version for the build, /healthz and /metrics for health, /logs for logging, and the two that actually hold resources: /api and /apis.

Core group vs named groups

Those two paths are the split that trips everyone up at first, so here it is plainly.

Core group · /api

The old, essential resources. No group name in the path. pods, namespaces, nodes, services, endpoints, events, configmaps, secrets, persistentvolumes.

Named groups · /apis

Everything added later, sorted into named buckets: apps (deployments, replicasets, statefulsets), networking.k8s.io (networkpolicies), storage.k8s.io, authentication.k8s.io, certificates.k8s.io (CSRs).

If a resource feels "core" to running a cluster, it's under /api. If it feels like a feature bolted on, it's a named group under /apis.

Inside any group, a resource supports a set of actions called verbs: get, list, create, delete, update and watch. That trio, group plus resource plus verb, is the exact language RBAC uses to allow or deny you. Hold that thought for the next post.

The full shape is worth memorising, because it's how you read any RBAC rule or API path:

# named groups
/apis  →  group  →  version  →  resource
       e.g. /apis/apps/v1/deployments

# core group (no group segment)
/api   →  version  →  resource
       e.g. /api/v1/pods

Talking to the API directly

You can browse the groups yourself. Hit the server with no path and it lists what's there:

root@controlplane ~
$ curl https://localhost:6443/apis -k --cert admin.crt --key admin.key --cacert ca.crt { "paths": [ "/api", "/api/v1", "/apis", "/apis/apps", "/apis/networking.k8s.io", "/apis/certificates.k8s.io", "/healthz", "/metrics", "/logs" ] }
Without credentials you'll usually only reach harmless endpoints like /version. Add your cert and the real groups appear.

Passing certs on every curl is tedious (sound familiar?), so kubectl gives you a shortcut: kubectl proxy. It opens a local HTTP proxy that reuses your kubeconfig credentials, so you can curl plain http://localhost:8001 and it authenticates for you:

kubectl proxy
# Starting to serve on 127.0.0.1:8001

curl http://localhost:8001/apis
Don't mix these two up kube-proxy is a cluster component that wires up pod and service networking on every node. kubectl proxy is a little local HTTP proxy on your laptop for reaching the API server. Same word, completely different jobs.

If you'd rather not use the proxy, you authenticate the request yourself with a bearer token instead of certs:

curl -X GET $APISERVER/api --header "Authorization: Bearer $TOKEN" --insecure

where a service account's token, for example, comes from its secret and is base64 the whole way, so you decode it before use.

The pentest angle on API groups

Once I have any identity on a cluster, API groups become an enumeration map. The workflow is simple and it's mostly about finding the gap between what you are and what you can do.

  • List the groups. curl https://<api-server>/apis to see what's exposed.
  • Find what you can touch. kubectl auth can-i --list shows your effective permissions, and kubectl auth can-i create pods answers a single question fast.
  • Hunt high-impact resources. Not all resources are equal. Some are a straight line to owning the cluster.
Resources worth abusing if RBAC lets you
ResourceGroupWhy it's dangerous
deployments / podsapps, corecode execution run a container, get a shell
secretscorecredential access read tokens and keys
networkpoliciesnetworking.k8s.ionetwork bypass open blocked paths
certificatesigningrequestscertificates.k8s.ioprivilege escalation mint a trusted client cert
If you can control a resource, you can often control cluster behaviour. That's the whole game.

API groups aren't trivia. They're the coordinate system for every RBAC rule, and the shortlist above is where a "read-only" account quietly turns into cluster-admin when the bindings are sloppy.

Two things that clicked

Two things clicked writing this. First, a kubeconfig is a credential wearing a config file's clothes, and I'll never look at ~/.kube/config as harmless again. Second, API groups finally gave me the map I was missing: /api for the core stuff, /apis/group/version/resource for everything else, and verbs on top. I'm still fuzzy on exec-plugin auth in kubeconfig (the cloud IAM path), so if you've abused one of those, tell me. Next post is the obvious one: RBAC, where these groups, resources and verbs finally turn into allow-or-deny.

If this helped, come say hi on LinkedIn or the contact page, and tell me if you want RBAC broken down the same way.

Further reading

FAQ

What is a kubeconfig file?

It is the file kubectl reads to know which cluster to talk to and how to prove who you are. It bundles clusters, users and contexts, a context being a pairing of a cluster and a user.

What are Kubernetes API groups?

The API is split into groups of related resources (core, apps, rbac and more), each with resources and verbs. Understanding groups, resources and verbs is what makes RBAC rules make sense.

Where is my kubeconfig stored?

By default at ~/.kube/config. Because it can hold credentials to the whole cluster it is a high-value file, so treat it like a secret and never commit it to a repository.