<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged cka</title>
<link>https://coderlegion.com/tag/cka</link>
<description>Powered by Question2Answer</description>
<item>
<title>Manage TLS Certificates In a Kubernetes Cluster</title>
<link>https://coderlegion.com/21833/manage-tls-certificates-in-a-kubernetes-cluster</link>
<description>&lt;h3 id=&quot;section&quot;&gt;&lt;/h3&gt;
&lt;p&gt;Today's challenge was about understanding how Kubernetes uses TLS certificates to secure communication between its components, and getting hands-on by onboarding a &quot;new user&quot; into the cluster using a Certificate Signing Request (CSR).&lt;/p&gt;
&lt;p&gt;If you've ever wondered how &lt;code&gt;kubectl&lt;/code&gt; actually proves to the API server that you're allowed to run commands, this is the exercise that answers it.&lt;/p&gt;
&lt;p&gt;A quick rule of thumb before diving in: &lt;strong&gt;if a filename or extension has the word &quot;key&quot; in it, it's a private key. Otherwise, it's a public certificate.&lt;/strong&gt; Keep that in mind as you read through the commands below.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;step-1-generate-a-private-key-and-a-csr&quot;&gt;Step 1: Generate a private key and a CSR&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;openssl genrsa -out adeoye.key 2048
openssl req -new -key adeoye.key -out adeoye.csr -subj &quot;/CN=adeoye&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;genrsa&lt;/code&gt; creates the private key, and &lt;code&gt;req -new&lt;/code&gt; uses that key to generate a Certificate Signing Request. The &lt;code&gt;-subj &quot;/CN=adeoye&quot;&lt;/code&gt; flag sets the &quot;Common Name&quot; on the certificate — this becomes the username Kubernetes will recognize once the cert is issued.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; I typed &lt;code&gt;adam.key&lt;/code&gt; in my &lt;code&gt;req&lt;/code&gt; command before actually creating a file with that name, and got:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Could not open file or uri for loading private key from adam.key: No such file or directory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lesson learned — the key file has to exist &lt;em&gt;before&lt;/em&gt; you reference it when generating the CSR. Double-check your filenames match across commands.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;step-2-base64-encode-the-csr&quot;&gt;Step 2: Base64-encode the CSR&lt;/h4&gt;
&lt;p&gt;The CSR object in Kubernetes expects the request in base64 form inside a YAML manifest. This is where things got interesting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat adeoye.csr | base64 tr -d &quot;/&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's wrong — it treats &lt;code&gt;tr&lt;/code&gt; as a filename argument for &lt;code&gt;base64&lt;/code&gt;, not a separate piped command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;base64: extra operand '/'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The correct version needs a pipe (&lt;code&gt;|&lt;/code&gt;) before &lt;code&gt;tr&lt;/code&gt;, and the actual newline character (&lt;code&gt;\n&lt;/code&gt;), not the literal string &lt;code&gt;&quot;/n&quot;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat adeoye.csr | base64 | tr -d &quot;\n&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives you a single-line base64 blob with no line breaks — exactly what the CSR YAML needs.&lt;/p&gt;
&lt;h4 id=&quot;step-3-create-the-csr-manifest-and-apply-it&quot;&gt;Step 3: Create the CSR manifest and apply it&lt;/h4&gt;
&lt;p&gt;Create a &lt;code&gt;csr.yaml&lt;/code&gt; file with the base64-encoded request embedded in it, following the structure from the &lt;a rel=&quot;nofollow&quot; href=&quot;https://kubernetes.io/docs/reference/access-authn-authz/certificate-signing-requests/#create-certificatessigningrequest&quot;&gt;Kubernetes docs&lt;/a&gt;, then:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl apply -f csr.yaml
kubectl get csr
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;NAME     AGE   SIGNERNAME                            REQUESTOR          REQUESTEDDURATION   CONDITION
adeoye   25s   kubernetes.io/kube-apiserver-client   kubernetes-admin   24h                 Pending
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The CSR shows up as &lt;code&gt;Pending&lt;/code&gt; — Kubernetes has received the request but it hasn't been approved yet.&lt;/p&gt;
&lt;h4 id=&quot;step-4-approve-the-csr&quot;&gt;Step 4: Approve the CSR&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl certificate approve adeoye
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;certificatesigningrequest.certificates.k8s.io/adeoye approved
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Checking again with &lt;code&gt;kubectl get csr&lt;/code&gt; now shows &lt;code&gt;Approved,Issued&lt;/code&gt; — the cluster has signed the certificate.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; &lt;code&gt;kubectl approve certificate adeoye&lt;/code&gt; doesn't work — the order of the words matters:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error: unknown command &quot;approve&quot; for &quot;kubectl&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's &lt;code&gt;kubectl certificate approve &amp;lt;name&amp;gt;&lt;/code&gt;, not &lt;code&gt;kubectl approve certificate &amp;lt;name&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;step-5-retrieve-and-decode-the-issued-certificate&quot;&gt;Step 5: Retrieve and decode the issued certificate&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get csr adeoye -o yaml &amp;gt; issuecert.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The issued certificate lives (base64-encoded) inside that YAML file under &lt;code&gt;status.certificate&lt;/code&gt;. To read the actual PEM certificate, decode it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo &quot;&amp;lt;base64-blob&amp;gt;&quot; | base64 -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This prints out the human-readable certificate:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-----BEGIN CERTIFICATE REQUEST-----
MIICVjCCAT4CAQAwETEPMA0GA1UEAww...
-----END CERTIFICATE REQUEST-----
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; Pasting a long base64 string directly into the terminal can trigger &quot;bracketed paste mode,&quot; which shows up as weird escape codes and leaves your shell hanging on a &lt;code&gt;dquote&amp;gt;&lt;/code&gt; prompt. If that happens, press &lt;code&gt;Ctrl+C&lt;/code&gt; to cancel and try again — wrapping the string in quotes (&lt;code&gt;echo &quot;...&quot;&lt;/code&gt;) helps avoid it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To save the decoded certificate directly to a file instead of just printing it, redirect the output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo &quot;&amp;lt;base64-blob&amp;gt;&quot; | base64 -d &amp;gt; learner.crt
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; Writing &lt;code&gt;-o learner.crt&lt;/code&gt; instead of &lt;code&gt;&amp;gt; learner.crt&lt;/code&gt; doesn't work here — &lt;code&gt;-o&lt;/code&gt; isn't a flag for &lt;code&gt;base64&lt;/code&gt;, and the shell will try to run it as its own command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zsh: command not found: -o
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use the &lt;code&gt;&amp;gt;&lt;/code&gt; redirect operator to send output to a file.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;repeating-the-process-for-a-second-user&quot;&gt;Repeating the process for a second user&lt;/h4&gt;
&lt;p&gt;To make sure the workflow actually sank in, I repeated the whole thing for a second user, &lt;code&gt;learner&lt;/code&gt;, this time being more careful with the pipe syntax:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;openssl genrsa -out learner.key 2048
openssl req -new -key learner.key -out learner.csr -subj &quot;/CN=learner&quot;
kubectl apply -f learner.yaml
kubectl certificate approve learner
kubectl get csr learner -o yaml &amp;gt; issuecert-learner.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;NAME      AGE     SIGNERNAME                            REQUESTOR          REQUESTEDDURATION   CONDITION
learner   4m37s   kubernetes.io/kube-apiserver-client   kubernetes-admin   7d                  Approved,Issued
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the certificate was decoded and saved to &lt;code&gt;learner.crt&lt;/code&gt;, it would be ready to plug into a kubeconfig for that user, giving them client-cert-based access to the cluster.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-commands-from-today&quot;&gt;Key commands from today&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Generate a private key:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;openssl genrsa -out adeoye.key 2048
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Generate a CSR:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;openssl req -new -key adeoye.key -out adeoye.csr -subj &quot;/CN=adeoye&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Approve a CSR:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl certificate approve &amp;lt;certificate-signing-request-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Deny a CSR:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl certificate deny &amp;lt;certificate-signing-request-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Reference:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://kubernetes.io/docs/reference/access-authn-authz/certificate-signing-requests/#create-certificatessigningrequest&quot;&gt;Kubernetes docs — Create CertificateSigningRequest&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;what-i-took-away-from-today&quot;&gt;What I took away from today&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;A CSR workflow has four clean stages: generate a key → generate a CSR → submit it to the API server as a &lt;code&gt;CertificateSigningRequest&lt;/code&gt; object → get it approved and retrieve the signed cert.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;base64 | tr -d &quot;\n&quot;&lt;/code&gt; (with the pipe, and a real newline character) is the correct way to get a clean, single-line encoded string for a YAML manifest.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubectl certificate approve&lt;/code&gt; — command, then subcommand, then name — not the other way around.&lt;/li&gt;
&lt;li&gt;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 &lt;em&gt;what&lt;/em&gt; you're doing, even when the terminal disagrees with you for a minute.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21833/manage-tls-certificates-in-a-kubernetes-cluster</guid>
<pubDate>Fri, 03 Jul 2026 11:57:21 +0000</pubDate>
</item>
</channel>
</rss>