Today's challenge was about understanding how Kubernetes uses TLS certificates to secure communication between its components, and getting hands-on by onboarding a "new user" into the cluster using a Certificate Signing Request (CSR).
If you've ever wondered how kubectl actually proves to the API server that you're allowed to run commands, this is the exercise that answers it.
A quick rule of thumb before diving in: if a filename or extension has the word "key" in it, it's a private key. Otherwise, it's a public certificate. Keep that in mind as you read through the commands below.
Step 1: Generate a private key and a CSR
openssl genrsa -out adeoye.key 2048
openssl req -new -key adeoye.key -out adeoye.csr -subj "/CN=adeoye"
genrsa creates the private key, and req -new uses that key to generate a Certificate Signing Request. The -subj "/CN=adeoye" flag sets the "Common Name" on the certificate — this becomes the username Kubernetes will recognize once the cert is issued.
Gotcha: I typed
adam.keyin myreqcommand before actually creating a file with that name, and got:Could not open file or uri for loading private key from adam.key: No such file or directoryLesson learned — the key file has to exist before you reference it when generating the CSR. Double-check your filenames match across commands.
Step 2: Base64-encode the CSR
The CSR object in Kubernetes expects the request in base64 form inside a YAML manifest. This is where things got interesting:
cat adeoye.csr | base64 tr -d "/"
That's wrong — it treats tr as a filename argument for base64, not a separate piped command:
base64: extra operand '/'
The correct version needs a pipe (|) before tr, and the actual newline character (\n), not the literal string "/n":
cat adeoye.csr | base64 | tr -d "\n"
This gives you a single-line base64 blob with no line breaks — exactly what the CSR YAML needs.
Step 3: Create the CSR manifest and apply it
Create a csr.yaml file with the base64-encoded request embedded in it, following the structure from the Kubernetes docs, then:
kubectl apply -f csr.yaml
kubectl get csr
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
adeoye 25s kubernetes.io/kube-apiserver-client kubernetes-admin 24h Pending
The CSR shows up as Pending — Kubernetes has received the request but it hasn't been approved yet.
Step 4: Approve the CSR
kubectl certificate approve adeoye
certificatesigningrequest.certificates.k8s.io/adeoye approved
Checking again with kubectl get csr now shows Approved,Issued — the cluster has signed the certificate.
Gotcha:
kubectl approve certificate adeoyedoesn't work — the order of the words matters:error: unknown command "approve" for "kubectl"It's
kubectl certificate approve <name>, notkubectl approve certificate <name>.
Step 5: Retrieve and decode the issued certificate
kubectl get csr adeoye -o yaml > issuecert.yaml
The issued certificate lives (base64-encoded) inside that YAML file under status.certificate. To read the actual PEM certificate, decode it:
echo "<base64-blob>" | base64 -d
This prints out the human-readable certificate:
-----BEGIN CERTIFICATE REQUEST-----
MIICVjCCAT4CAQAwETEPMA0GA1UEAww...
-----END CERTIFICATE REQUEST-----
Gotcha: Pasting a long base64 string directly into the terminal can trigger "bracketed paste mode," which shows up as weird escape codes and leaves your shell hanging on a
dquote>prompt. If that happens, pressCtrl+Cto cancel and try again — wrapping the string in quotes (echo "...") helps avoid it.
To save the decoded certificate directly to a file instead of just printing it, redirect the output:
echo "<base64-blob>" | base64 -d > learner.crt
Gotcha: Writing
-o learner.crtinstead of> learner.crtdoesn't work here —-oisn't a flag forbase64, and the shell will try to run it as its own command:zsh: command not found: -oUse the
>redirect operator to send output to a file.
Repeating the process for a second user
To make sure the workflow actually sank in, I repeated the whole thing for a second user, learner, this time being more careful with the pipe syntax:
openssl genrsa -out learner.key 2048
openssl req -new -key learner.key -out learner.csr -subj "/CN=learner"
kubectl apply -f learner.yaml
kubectl certificate approve learner
kubectl get csr learner -o yaml > issuecert-learner.yaml
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
learner 4m37s kubernetes.io/kube-apiserver-client kubernetes-admin 7d Approved,Issued
Once the certificate was decoded and saved to learner.crt, it would be ready to plug into a kubeconfig for that user, giving them client-cert-based access to the cluster.
Key commands from today
Generate a private key:
openssl genrsa -out adeoye.key 2048
Generate a CSR:
openssl req -new -key adeoye.key -out adeoye.csr -subj "/CN=adeoye"
Approve a CSR:
kubectl certificate approve <certificate-signing-request-name>
Deny a CSR:
kubectl certificate deny <certificate-signing-request-name>
Reference: Kubernetes docs — Create CertificateSigningRequest
What I took away from today
- A CSR workflow has four clean stages: generate a key → generate a CSR → submit it to the API server as a
CertificateSigningRequestobject → get it approved and retrieve the signed cert. -
base64 | tr -d "\n"(with the pipe, and a real newline character) is the correct way to get a clean, single-line encoded string for a YAML manifest. -
kubectl certificate approve— command, then subcommand, then name — not the other way around. - Almost all my mistakes today were syntax slips (missing pipes, wrong flags, mismatched filenames), not conceptual ones. That's usually a good sign you understand what you're doing, even when the terminal disagrees with you for a minute.
Top comments (0)