Pull a user's client certificate out of a kubeconfig (CKS)
Lesson two of the CKS series, and one of the easiest sets of points on the exam if you know two flags. We will list every context in a kubeconfig into a file, then pull one user's client certificate out of it, decoded. And because this is the security exam, we will not stop at the file: we will read the certificate and find out exactly who it lets you be.
🎥 Watch the video: https://www.youtube.com/watch?v=ikI5PO3UWtE
This is a CKS Cluster Hardening walkthrough. Every command below is real output from a live cluster, and you can reproduce the whole thing yourself (scripts at the end).
The scenario
Here is the setup. Your workstation kubeconfig reaches several clusters, so it has several contexts. Two things are asked. Write every context name into contexts.txt, one per line. Then take the user called auditor at payments-stage, and write that user's client certificate, decoded, into auditor.crt.
- Your kubeconfig holds several contexts
- 1. Every context name into contexts.txt, one per line
- 2. User auditor@payments-stage: client certificate, DECODED, into auditor.crt
Anatomy of a kubeconfig
A kubeconfig is three lists. Clusters say where the API server is and which CA to trust. Users hold credentials, here a client certificate and its private key. Contexts pair one cluster with one user, and that name is what you switch between. The credentials are stored base64 encoded, in fields called client-certificate-data and client-key-data. And kubectl protects you from yourself: a normal config view replaces them with DATA plus OMITTED. To see the real value you ask for it explicitly with --raw.
What you are handed
Start with get-contexts. Four contexts, each pairing a cluster with a user, and the star marks the current one. Notice the name column and the authinfo column: for the auditor, the context and the user share the same name, which is common and easy to mix up. The task asks for the user, so that is the list we will search.
$ kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
auditor@payments-stage payments-stage auditor@payments-stage
ci@build-farm build-farm ci@build-farm
* kind-cks-scenario2 kind-cks-scenario2 kind-cks-scenario2
ops@payments-prod payments-prod ops@payments-prod
Context names to a file
Part one is a single flag. -o name prints only the names, one per line, with no header and no star. That is already the exact format the task wants, so redirect it into contexts.txt and cat it back. Never copy the table by hand; a missed character is a lost point.
$ kubectl config get-contexts -o name
auditor@payments-stage
ci@build-farm
kind-cks-scenario2
ops@payments-prod
$ kubectl config get-contexts -o name > contexts.txt && cat contexts.txt
auditor@payments-stage
ci@build-farm
kind-cks-scenario2
ops@payments-prod
Redacted versus raw
Part two. Select just the auditor user with a jsonpath filter on the name, and look at what comes back: DATA plus OMITTED, for both the certificate and the key. That is not the certificate, and writing it to the file would score zero. Run the same query with --raw and ask for client-certificate-data, and now there is real base64. I cut it to sixty characters here, but notice it starts with L S zero t, which is what a base64 encoded PEM header always looks like.
$ kubectl config view -o jsonpath='{.users[?(@.name=="auditor@payments-stage")].user}'
{"client-certificate-data":"DATA+OMITTED","client-key-data":"DATA+OMITTED"}
$ kubectl config view --raw -o jsonpath='{.users[?(@.name=="auditor@payments-stage")].user.client-certificate-data}' | cut -c1-60
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURFekNDQWZ1Z0F3SUJB
Decode the certificate
Now pipe it through base64 -d and redirect into auditor.crt. The head of the file shows begin certificate: a proper PEM file, which is what decoded means in this task. If you see base64 soup starting with L S zero t in your answer file, you forgot the decode.
$ kubectl config view --raw -o jsonpath='{.users[?(@.name=="auditor@payments-stage")].user.client-certificate-data}' | base64 -d > auditor.crt && head -n 3 auditor.crt
-----BEGIN CERTIFICATE-----
MIIDEzCCAfugAwIBAgIQDd249qkPHnr4qN1/QDlakDANBgkqhkiG9w0BAQsFADAV
MRMwEQYDVQQDEwprdWJlcm5ldGVzMB4XDTI2MDkyMzEzMjkzOFoXDTI2MTAyMzEz
Read the certificate
The task is done, but a security engineer should know what they just extracted. openssl x509 with subject, issuer and enddate answers it. The CN, auditor, is the username Kubernetes will see. The O, payments-auditors, is a group, and RBAC can bind to it. The issuer is kubernetes, this cluster's own CA. And it expires in thirty days, which is a good habit for client certificates, because Kubernetes has no way to revoke one.
$ openssl x509 -in auditor.crt -noout -subject -issuer -enddate
subject=O=payments-auditors, CN=auditor
issuer=CN=kubernetes
notAfter=Oct 23 13:29:38 2026 GMT
Who is this user
Let's prove it. Switch to the auditor context and ask auth whoami. The API server agrees: username auditor, in the group payments-auditors. That is authentication, and it worked. Now try to list pods, and it is Forbidden. Nobody has bound a role to this user or its group yet. Being able to log in and being allowed to do something are two separate checks, and RBAC is the second one.
$ kubectl --context auditor@payments-stage auth whoami
ATTRIBUTE VALUE
Username auditor
Groups [payments-auditors system:authenticated]
Extra: authentication.kubernetes.io/credential-id [X509SHA256=9961016f3aeb10ed0518d8f230e4d218ece2079c2e3a86694f015e7edd1bc519]
$ kubectl --context auditor@payments-stage get pods -A
Error from server (Forbidden): pods is forbidden: User "auditor" cannot list resource "pods" in API group "" at the cluster scope
Exam tips
Tips for the exam. Use -o name for anything that asks for names one per line. Remember that config view redacts, so the certificate needs --raw. Filter by the user name with jsonpath instead of grepping a few lines after a match; it is exact and survives long files. Decoded means PEM, so check the file starts with begin certificate. If the question says use a specific kubeconfig file, pass dash dash kubeconfig or set the KUBECONFIG variable, because the default file may be a different one. And outside the exam, treat any kubeconfig with embedded keys as a secret.
- get-contexts -o name: names only, one per line
- config view redacts: add --raw for certificate data
- jsonpath filter on the user name, not grep -A
- Decoded = PEM: the file starts with BEGIN CERTIFICATE
- Mind which kubeconfig: --kubeconfig or KUBECONFIG
- A kubeconfig with embedded keys is a credential
Recap
- get-contexts -o name > file
- config view --raw + jsonpath + base64 -d = PEM certificate
- CN = username, O = group
- authN is not authZ; subscribe + dev.to writeup
Reproduce this yourself
The entire scenario is scripted on a throwaway kind cluster: https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios
git clone https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios.git
cd TCS_CKS_2026_Exam_Scenarios/scenario2-kubeconfig-cert
./setup.sh # creates the cluster AND arms the scenario
# solve it by hand, or:
./solution.sh # apply the answer key and verify
If this helped, subscribe to The Cyber SideKick on YouTube for more CKS drills, and grab the newsletter at https://thecybersidekick.beehiiv.com.
Top comments (0)