Dev.to Security 🔐 Cybersecurity 👁 0 📖 5 min read

Pull a user's client certificate out of a kubeconfig (CKS)

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,

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.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.