<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: The Cyber Sidekick</title>
    <description>The latest articles on DEV Community by The Cyber Sidekick (@thecybersidekick).</description>
    <link>https://dev.to/thecybersidekick</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3987699%2F79b4c7af-5633-4f83-a6d5-03651461b293.png</url>
      <title>DEV Community: The Cyber Sidekick</title>
      <link>https://dev.to/thecybersidekick</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thecybersidekick"/>
    <language>en</language>
    <item>
      <title>Fix CIS benchmark findings with kube-bench (CKS)</title>
      <dc:creator>The Cyber Sidekick</dc:creator>
      <pubDate>Wed, 14 Oct 2026 12:47:26 +0000</pubDate>
      <link>https://dev.to/thecybersidekick/fix-cis-benchmark-findings-with-kube-bench-cks-1mo7</link>
      <guid>https://dev.to/thecybersidekick/fix-cis-benchmark-findings-with-kube-bench-cks-1mo7</guid>
      <description>&lt;h2&gt;
  
  
  Fix CIS benchmark findings with kube-bench (CKS)
&lt;/h2&gt;

&lt;p&gt;Lesson five of the CKS series: the CIS benchmark, with kube-bench. We will run it on a control plane and a worker, read what it tells us, and fix three specific findings, re-checking each one. And for the kubelet we will not just trust the checklist; we will see what the insecure setting actually allowed.&lt;/p&gt;

&lt;p&gt;🎥 &lt;strong&gt;Watch the video:&lt;/strong&gt; &lt;a href="https://www.youtube.com/watch?v=iJiakq3Kpv4" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=iJiakq3Kpv4&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a CKS Cluster Setup walkthrough. Every command below is real output from a live cluster, and you can reproduce the whole thing yourself (scripts at the end).&lt;/p&gt;

&lt;h2&gt;
  
  
  The scenario
&lt;/h2&gt;

&lt;p&gt;The setup. A two-node cluster, with kube-bench already installed on both nodes. On the control plane, fix check one point four point one, scheduler profiling. On the worker, fix four point one point nine, the permissions of the kubelet config file, and four point two point one, kubelet anonymous authentication. Use the CIS one point twelve benchmark.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two nodes, kube-bench installed on both (benchmark cis-1.12)&lt;/li&gt;
&lt;li&gt;Control plane: 1.4.1 kube-scheduler --profiling&lt;/li&gt;
&lt;li&gt;Worker: 4.1.9 kubelet config.yaml permissions&lt;/li&gt;
&lt;li&gt;Worker: 4.2.1 kubelet anonymous auth&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How kube-bench works
&lt;/h2&gt;

&lt;p&gt;kube-bench runs the CIS Kubernetes Benchmark on the node you are standing on. The &lt;code&gt;--targets&lt;/code&gt; flag picks the section: master for control plane checks, node for kubelet checks, so you run it on each node separately. Every check has a number and prints PASS, FAIL or WARN, and every failure comes with a remediation that names the exact file to change. &lt;code&gt;--check&lt;/code&gt; narrows the run to one or more numbers, which is how you confirm a fix. Control plane fixes land in static pod manifests; worker fixes land in the kubelet's files, followed by a kubelet restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Control plane findings
&lt;/h2&gt;

&lt;p&gt;Start on the control plane. Run kube-bench against the master target and keep only the FAIL lines. Ten failures on a stock kubeadm cluster, which is normal. The task names one of them, one point four point one, the last line: the scheduler's profiling argument. Resist the urge to fix everything; the grader checks what was asked, and every extra change is a chance to break a component.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ssh cks-scenario5-control-plane
&lt;span class="go"&gt;cks-scenario5-control-plane

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kube-bench run &lt;span class="nt"&gt;--benchmark&lt;/span&gt; cis-1.12 &lt;span class="nt"&gt;--targets&lt;/span&gt; master | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^\[FAIL\]'&lt;/span&gt;
&lt;span class="go"&gt;[FAIL] 1.1.12 Ensure that the etcd data directory ownership is set to etcd:etcd (Automated
[FAIL] 1.2.5 Ensure that the --kubelet-certificate-authority argument is set as appropriat
[FAIL] 1.2.15 Ensure that the --profiling argument is set to false (Automated)
[FAIL] 1.2.16 Ensure that the --audit-log-path argument is set (Automated)
[FAIL] 1.2.17 Ensure that the --audit-log-maxage argument is set to 30 or as appropriate (
[FAIL] 1.2.18 Ensure that the --audit-log-maxbackup argument is set to 10 or as appropriat
[FAIL] 1.2.19 Ensure that the --audit-log-maxsize argument is set to 100 or as appropriate
[FAIL] 1.2.30 Ensure that the --service-account-extend-token-expiration parameter is set t
[FAIL] 1.3.2 Ensure that the --profiling argument is set to false (Automated)
[FAIL] 1.4.1 Ensure that the --profiling argument is set to false (Automated)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Read the remediation
&lt;/h2&gt;

&lt;p&gt;Narrow it down with &lt;code&gt;--check&lt;/code&gt; one point four point one. Now we get the remediation: edit the scheduler pod specification, kube-scheduler dot yaml in &lt;code&gt;/etc/kubernetes/manifests&lt;/code&gt;, and set &lt;code&gt;--profiling=false&lt;/code&gt;. Profiling exposes a debug endpoint with performance data about the component, which you do not want available in production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kube-bench run &lt;span class="nt"&gt;--benchmark&lt;/span&gt; cis-1.12 &lt;span class="nt"&gt;--targets&lt;/span&gt; master &lt;span class="nt"&gt;--check&lt;/span&gt; 1.4.1
&lt;span class="go"&gt;[FAIL] 1.4.1 Ensure that the --profiling argument is set to false (Automated)

== Remediations master ==
1.4.1 Edit the Scheduler pod specification file /etc/kubernetes/manifests/kube-scheduler.yaml file
on the control plane node and set the below parameter.
--profiling=false
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Disable scheduler profiling
&lt;/h2&gt;

&lt;p&gt;Open the scheduler manifest in vi, go to the end of the command list and add one line: &lt;code&gt;--profiling=false&lt;/code&gt;, lined up with the other flags. Save with colon w q. The kubelet notices the file changed and restarts the scheduler on its own.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'/- kube-scheduler/,/image:/p'&lt;/span&gt; /etc/kubernetes/manifests/kube-scheduler.yaml
&lt;span class="go"&gt;    - kube-scheduler
    - --authentication-kubeconfig=/etc/kubernetes/scheduler.conf
    - --authorization-kubeconfig=/etc/kubernetes/scheduler.conf
    - --bind-address=127.0.0.1
    - --kubeconfig=/etc/kubernetes/scheduler.conf
    - --leader-elect=true
    image: registry.k8s.io/kube-scheduler:v1.37.0

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'/- kube-scheduler/,/image:/p'&lt;/span&gt; /etc/kubernetes/manifests/kube-scheduler.yaml
&lt;span class="go"&gt;    - kube-scheduler
    - --authentication-kubeconfig=/etc/kubernetes/scheduler.conf
    - --authorization-kubeconfig=/etc/kubernetes/scheduler.conf
    - --bind-address=127.0.0.1
    - --kubeconfig=/etc/kubernetes/scheduler.conf
    - --leader-elect=true
    - --profiling=false
    image: registry.k8s.io/kube-scheduler:v1.37.0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1.4.1 passes
&lt;/h2&gt;

&lt;p&gt;Give the scheduler a few seconds to come back, then rerun just that check. One point four point one, PASS. That is the pattern for every finding: read, fix, recheck.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kube-bench run &lt;span class="nt"&gt;--benchmark&lt;/span&gt; cis-1.12 &lt;span class="nt"&gt;--targets&lt;/span&gt; master &lt;span class="nt"&gt;--check&lt;/span&gt; 1.4.1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^\['&lt;/span&gt;
&lt;span class="go"&gt;[PASS] 1.4.1 Ensure that the --profiling argument is set to false (Automated)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Worker findings
&lt;/h2&gt;

&lt;p&gt;Now the worker. Same command, target node. Three failures: four point one point one, which this newer benchmark flags and the task does not mention, so we leave it; and our two, four point one point nine and four point two point one. The config file is mode six six six, readable and writable by every user on the node. And here is what anonymous auth means in practice: a request to the kubelet API with no credentials at all is accepted as the user &lt;code&gt;system:anonymous&lt;/code&gt;. It only gets Forbidden because authorization happens to deny that user. One RBAC mistake away from reading every pod on this node.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ssh cks-scenario5-worker
&lt;span class="go"&gt;cks-scenario5-worker

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kube-bench run &lt;span class="nt"&gt;--benchmark&lt;/span&gt; cis-1.12 &lt;span class="nt"&gt;--targets&lt;/span&gt; node | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^\[FAIL\]'&lt;/span&gt;
&lt;span class="go"&gt;[FAIL] 4.1.1 Ensure that the kubelet service file permissions are set to 600 or more restr
[FAIL] 4.1.9 If the kubelet config.yaml configuration file is being used validate permissi
[FAIL] 4.2.1 Ensure that the --anonymous-auth argument is set to false (Automated)

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'%a %U:%G %n'&lt;/span&gt; /var/lib/kubelet/config.yaml
&lt;span class="go"&gt;666 root:root /var/lib/kubelet/config.yaml

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-sk&lt;/span&gt; https://localhost:10250/pods&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;span class="go"&gt;Forbidden (user=system:anonymous, verb=get, resource=nodes, subresource(s)=[pods proxy])
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fix the file permissions
&lt;/h2&gt;

&lt;p&gt;Four point one point nine is one command. chmod six hundred: read and write for root, nothing for anyone else. stat confirms it. This benchmark asks for six hundred; older ones accepted six four four, so always read the remediation for the version you are running.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /var/lib/kubelet/config.yaml &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'%a %U:%G %n'&lt;/span&gt; /var/lib/kubelet/config.yaml
&lt;span class="go"&gt;600 root:root /var/lib/kubelet/config.yaml
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Turn off anonymous auth
&lt;/h2&gt;

&lt;p&gt;Four point two point one lives in that same file. Open &lt;code&gt;/var/lib/kubelet/config.yaml&lt;/code&gt;, and under authentication, anonymous, change enabled from true to false. Careful: there is a second enabled line just below it, for webhook authentication, and that one must stay true. Save, then restart the kubelet with systemctl, because unlike static pods the kubelet does not reload its own config.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; /var/lib/kubelet/config.yaml | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 9
&lt;span class="go"&gt;1:apiVersion: kubelet.config.k8s.io/v1beta1
2:authentication:
3:  anonymous:
4:    enabled: true
5:  webhook:
6:    cacheTTL: 0s
7:    enabled: true
8:  x509:
9:    clientCAFile: /etc/kubernetes/pki/ca.crt
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verify the worker
&lt;/h2&gt;

&lt;p&gt;The same anonymous request now gets Unauthorized. It is rejected at authentication and never reaches the authorization check, which is what defense in depth looks like. And kube-bench agrees: four point one point nine and four point two point one, both PASS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-sk&lt;/span&gt; https://localhost:10250/pods&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;span class="go"&gt;Unauthorized

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kube-bench run &lt;span class="nt"&gt;--benchmark&lt;/span&gt; cis-1.12 &lt;span class="nt"&gt;--targets&lt;/span&gt; node &lt;span class="nt"&gt;--check&lt;/span&gt; 4.1.9,4.2.1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^\['&lt;/span&gt;
&lt;span class="go"&gt;[PASS] 4.1.9 If the kubelet config.yaml configuration file is being used validate permissi
[PASS] 4.2.1 Ensure that the --anonymous-auth argument is set to false (Automated)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exam tips
&lt;/h2&gt;

&lt;p&gt;Tips for the exam. Run kube-bench on the node the check belongs to: master checks on the control plane, node checks on workers. Use &lt;code&gt;--check&lt;/code&gt; to get just the remediation you need, instead of scrolling hundreds of lines. Fix only the numbered checks in the task. Back up a manifest outside the manifests folder before editing it. Restart the kubelet after changing its config file; static pods restart themselves. And rerun the check every time, because PASS is the only proof.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;master checks on the control plane, node checks on workers&lt;/li&gt;
&lt;li&gt;--check  for just the remediation you need&lt;/li&gt;
&lt;li&gt;Fix only the numbered checks the task names&lt;/li&gt;
&lt;li&gt;Kubelet config changed? systemctl restart kubelet&lt;/li&gt;
&lt;li&gt;Rerun the check: PASS is the only proof&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;1.4.1: --profiling=false in kube-scheduler.yaml&lt;/li&gt;
&lt;li&gt;4.1.9: chmod 600 /var/lib/kubelet/config.yaml&lt;/li&gt;
&lt;li&gt;4.2.1: anonymous.enabled false + restart kubelet&lt;/li&gt;
&lt;li&gt;Recheck each to PASS; subscribe + dev.to writeup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce this yourself
&lt;/h2&gt;

&lt;p&gt;The entire scenario is scripted on a throwaway &lt;code&gt;kind&lt;/code&gt; cluster: &lt;a href="https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios" rel="noopener noreferrer"&gt;https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios.git
&lt;span class="nb"&gt;cd &lt;/span&gt;TCS_CKS_2026_Exam_Scenarios/scenario5-kube-bench-cis
./setup.sh        &lt;span class="c"&gt;# creates the cluster AND arms the scenario&lt;/span&gt;
&lt;span class="c"&gt;# solve it by hand, or:&lt;/span&gt;
./solution.sh     &lt;span class="c"&gt;# apply the answer key and verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;If this helped, &lt;strong&gt;subscribe to The Cyber SideKick&lt;/strong&gt; on YouTube for more CKS drills, and grab the newsletter at &lt;a href="https://thecybersidekick.beehiiv.com" rel="noopener noreferrer"&gt;https://thecybersidekick.beehiiv.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cks</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Enforce the baseline Pod Security Standard on a namespace (CKS)</title>
      <dc:creator>The Cyber Sidekick</dc:creator>
      <pubDate>Fri, 09 Oct 2026 11:42:27 +0000</pubDate>
      <link>https://dev.to/thecybersidekick/enforce-the-baseline-pod-security-standard-on-a-namespace-cks-2e61</link>
      <guid>https://dev.to/thecybersidekick/enforce-the-baseline-pod-security-standard-on-a-namespace-cks-2e61</guid>
      <description>&lt;h2&gt;
  
  
  Enforce the baseline Pod Security Standard on a namespace (CKS)
&lt;/h2&gt;

&lt;p&gt;Lesson four of the CKS series: Pod Security Standards. One of the pods in our namespace can read the logs of every other pod on its node, including the control plane's. We will shut that down with a single namespace label, preview the impact before we apply it, and capture the exact reason the pod can never come back.&lt;/p&gt;

&lt;p&gt;🎥 &lt;strong&gt;Watch the video:&lt;/strong&gt; &lt;a href="https://www.youtube.com/watch?v=54tUL5OXedk" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=54tUL5OXedk&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a CKS Minimize Microservice Vulnerabilities walkthrough. Every command below is real output from a live cluster, and you can reproduce the whole thing yourself (scripts at the end).&lt;/p&gt;

&lt;h2&gt;
  
  
  The scenario
&lt;/h2&gt;

&lt;p&gt;The setup. In the edge-tools namespace, a Deployment called node-inspector mounts the node's &lt;code&gt;/var/log/pods&lt;/code&gt; folder through a hostPath volume. Three things to do. Enforce the baseline Pod Security Standard on the namespace. Delete the running node-inspector pod. Then find the event that explains why it is not recreated, and write it to &lt;code&gt;pss-denial.log&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ns edge-tools: node-inspector mounts hostPath /var/log/pods&lt;/li&gt;
&lt;li&gt;1. Enforce the baseline Pod Security Standard on the namespace&lt;/li&gt;
&lt;li&gt;2. Delete the running node-inspector pod&lt;/li&gt;
&lt;li&gt;3. Save the event explaining why it is not recreated to pss-denial.log&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Pod Security Admission works
&lt;/h2&gt;

&lt;p&gt;Pod Security Admission is built into every cluster, and you configure it with namespace labels. There are three standards. Privileged allows anything. Baseline blocks the well-known escapes: hostPath volumes, host networking and PID, privileged containers, added capabilities. Restricted goes further and demands things like running as non-root. The label &lt;code&gt;pod-security.kubernetes.io/enforce&lt;/code&gt; decides what gets rejected. There are also audit and warn modes that only report. And the key detail: this is an admission check, so it runs when a pod is created. Pods that are already running are left alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it matters
&lt;/h2&gt;

&lt;p&gt;Two pods running. Now look inside node-inspector at its hostPath mount. Every folder there is another pod's logs, named by namespace. Our own namespace, and then kube-system: coredns, and etcd. Anything those pods print, including tokens and errors that leak data, this one container can read. That is exactly the kind of escape baseline exists to block.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; edge-tools
&lt;span class="go"&gt;NAME                             READY   STATUS    RESTARTS   AGE
node-inspector-ccd7d54cd-sgbwb   1/1     Running   0          1s
status-page-75c9666db-qjvp8      1/1     Running   0          1s

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; edge-tools &lt;span class="nb"&gt;exec &lt;/span&gt;deploy/node-inspector &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; /host/pod-logs | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 6
&lt;span class="go"&gt;edge-tools_node-inspector-ccd7d54cd-sgbwb_6fcb7752-b169-4366-8983-7aea8ac157b0
edge-tools_status-page-75c9666db-qjvp8_f7bf36b0-3324-4112-b0cb-db44325f3638
edge-tools_status-page-75c9666db-x8zs5_ba88a876-ee10-4f31-9267-4ddab254a601
kube-system_coredns-559f6c778d-9jfkc_fdae9f54-94b3-447c-a101-a28fd2841ef3
kube-system_coredns-559f6c778d-fp7rq_dacbc698-80c5-4662-ba8a-3b17a164a1a4
kube-system_etcd-cks-scenario4-control-plane_9a9f430523155faf5e34364a5f598450
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  No labels yet
&lt;/h2&gt;

&lt;p&gt;Check the namespace labels. Only the automatic metadata name label. No Pod Security labels, which means the cluster default applies, and on most clusters that default is privileged: anything goes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get ns edge-tools &lt;span class="nt"&gt;--show-labels&lt;/span&gt;
&lt;span class="go"&gt;NAME         STATUS   AGE   LABELS
edge-tools   Active   1s    kubernetes.io/metadata.name=edge-tools
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Preview with a dry run
&lt;/h2&gt;

&lt;p&gt;Before enforcing anything in a real cluster, preview it. The same label command with &lt;code&gt;--dry-run=server&lt;/code&gt; asks the API server to evaluate the change without saving it. It warns that an existing pod would violate baseline, names the pod, and names the reason: hostPath volumes. Exactly the one we expected, and nothing else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl label &lt;span class="nt"&gt;--dry-run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;server &lt;span class="nt"&gt;--overwrite&lt;/span&gt; ns edge-tools pod-security.kubernetes.io/enforce&lt;span class="o"&gt;=&lt;/span&gt;baseline
&lt;span class="go"&gt;Warning: existing pods in namespace "edge-tools" violate the new PodSecurity enforce level "baseline:latest"
Warning: node-inspector-ccd7d54cd-sgbwb: hostPath volumes
namespace/edge-tools labeled (server dry run)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Enforce baseline
&lt;/h2&gt;

&lt;p&gt;Now apply it for real. Same warning, and the namespace is labelled. But look at the pods: node-inspector is still running. Enforcement happens at admission, when a pod is created. Kubernetes will not kill a running workload because a label changed, which is why the task tells you to delete it yourself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl label &lt;span class="nt"&gt;--overwrite&lt;/span&gt; ns edge-tools pod-security.kubernetes.io/enforce&lt;span class="o"&gt;=&lt;/span&gt;baseline
&lt;span class="go"&gt;Warning: existing pods in namespace "edge-tools" violate the new PodSecurity enforce level "baseline:latest"
Warning: node-inspector-ccd7d54cd-sgbwb: hostPath volumes
namespace/edge-tools labeled

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; edge-tools
&lt;span class="go"&gt;NAME                             READY   STATUS    RESTARTS   AGE
node-inspector-ccd7d54cd-sgbwb   1/1     Running   0          1s
status-page-75c9666db-qjvp8      1/1     Running   0          1s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Delete, and it stays gone
&lt;/h2&gt;

&lt;p&gt;Delete the node-inspector pod. Normally its ReplicaSet would have a replacement running within seconds. Not this time: only status-page is left, and it keeps running because it complies. The Deployment shows zero of one ready. The ReplicaSet is trying, and admission is refusing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; edge-tools delete pod &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;node-inspector
&lt;span class="go"&gt;pod "node-inspector-ccd7d54cd-sgbwb" deleted from edge-tools namespace

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; edge-tools
&lt;span class="go"&gt;NAME                          READY   STATUS    RESTARTS   AGE
status-page-75c9666db-qjvp8   1/1     Running   0          7s

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get deploy &lt;span class="nt"&gt;-n&lt;/span&gt; edge-tools
&lt;span class="go"&gt;NAME             READY   UP-TO-DATE   AVAILABLE   AGE
node-inspector   0/1     0            0           7s
status-page      1/1     1            1           7s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Save the reason
&lt;/h2&gt;

&lt;p&gt;The refusal is recorded as an event with reason &lt;code&gt;FailedCreate&lt;/code&gt;. A field selector pulls exactly that event, custom columns keep just the reason and the message, and we redirect it into &lt;code&gt;pss-denial.log&lt;/code&gt;. The message says it all: forbidden, violates PodSecurity baseline, hostPath volumes, and the volume name, pod-logs. That line is what the task wants in the file. kubectl describe on the ReplicaSet shows the same event if you prefer to copy it from there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; edge-tools get events &lt;span class="nt"&gt;--field-selector&lt;/span&gt; &lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;FailedCreate &lt;span class="nt"&gt;-o&lt;/span&gt; custom-columns&lt;span class="o"&gt;=&lt;/span&gt;REASON:.reason,MESSAGE:.message &lt;span class="nt"&gt;--no-headers&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; pss-denial.log &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cat &lt;/span&gt;pss-denial.log
&lt;span class="go"&gt;FailedCreate   Error creating: pods "node-inspector-ccd7d54cd-ktnj6" is forbidden: violates PodSecurity "baseline:latest": hostPath volumes (volume "pod-logs")
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exam tips
&lt;/h2&gt;

&lt;p&gt;Tips for the exam. The labels are in the Pod Security Standards page of the docs; search for pod security admission and copy the label key rather than typing it. Use the imperative kubectl label with dash dash overwrite, it is faster than editing the namespace. Enforce is the mode that blocks; warn and audit only report. Remember running pods are not evicted, so delete them when asked. And the reason lives on the ReplicaSet, not the Deployment: describe the ReplicaSet or filter events by &lt;code&gt;FailedCreate&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Copy the label key from the Pod Security Admission docs&lt;/li&gt;
&lt;li&gt;kubectl label --overwrite ns  pod-security.kubernetes.io/enforce=baseline&lt;/li&gt;
&lt;li&gt;enforce blocks; warn and audit only report&lt;/li&gt;
&lt;li&gt;Running pods are not evicted: delete them&lt;/li&gt;
&lt;li&gt;The reason is on the ReplicaSet: FailedCreate event&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;hostPath /var/log/pods = every pod's logs&lt;/li&gt;
&lt;li&gt;--dry-run=server first, then enforce=baseline&lt;/li&gt;
&lt;li&gt;Delete the pod: the ReplicaSet is refused&lt;/li&gt;
&lt;li&gt;FailedCreate event saved; subscribe + dev.to writeup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce this yourself
&lt;/h2&gt;

&lt;p&gt;The entire scenario is scripted on a throwaway &lt;code&gt;kind&lt;/code&gt; cluster: &lt;a href="https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios" rel="noopener noreferrer"&gt;https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios.git
&lt;span class="nb"&gt;cd &lt;/span&gt;TCS_CKS_2026_Exam_Scenarios/scenario4-pod-security-baseline
./setup.sh        &lt;span class="c"&gt;# creates the cluster AND arms the scenario&lt;/span&gt;
&lt;span class="c"&gt;# solve it by hand, or:&lt;/span&gt;
./solution.sh     &lt;span class="c"&gt;# apply the answer key and verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;If this helped, &lt;strong&gt;subscribe to The Cyber SideKick&lt;/strong&gt; on YouTube for more CKS drills, and grab the newsletter at &lt;a href="https://thecybersidekick.beehiiv.com" rel="noopener noreferrer"&gt;https://thecybersidekick.beehiiv.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cks</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop exposing the API server through a NodePort (CKS)</title>
      <dc:creator>The Cyber Sidekick</dc:creator>
      <pubDate>Tue, 06 Oct 2026 13:04:56 +0000</pubDate>
      <link>https://dev.to/thecybersidekick/stop-exposing-the-api-server-through-a-nodeport-cks-1ene</link>
      <guid>https://dev.to/thecybersidekick/stop-exposing-the-api-server-through-a-nodeport-cks-1ene</guid>
      <description>&lt;h2&gt;
  
  
  Stop exposing the API server through a NodePort (CKS)
&lt;/h2&gt;

&lt;p&gt;Lesson three of the CKS series. A security review found the Kubernetes API server reachable through a NodePort, and your job is to put it back behind a ClusterIP. It is one line in a manifest, plus a second step that trips up most people, because the API server will not fix the Service for you. Let's do it on a real control plane.&lt;/p&gt;

&lt;p&gt;🎥 &lt;strong&gt;Watch the video:&lt;/strong&gt; &lt;a href="https://www.youtube.com/watch?v=GWOuoCNDZq4" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=GWOuoCNDZq4&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a CKS Cluster Setup walkthrough. Every command below is real output from a live cluster, and you can reproduce the whole thing yourself (scripts at the end).&lt;/p&gt;

&lt;h2&gt;
  
  
  The scenario
&lt;/h2&gt;

&lt;p&gt;The setup. A review of this cluster reports that the API server is reachable through a NodePort Service. Change the API server setup so that it is reachable only through a ClusterIP Service. The cluster was built with kubeadm, so the control plane runs as static pods, and you have root on the control plane node.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Finding: the API server is reachable through a NodePort&lt;/li&gt;
&lt;li&gt;Fix it: reachable through a ClusterIP Service only&lt;/li&gt;
&lt;li&gt;kubeadm cluster: control plane = static pods on the node&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who owns the kubernetes Service
&lt;/h2&gt;

&lt;p&gt;The Service called kubernetes in the default namespace is not like other Services. Nobody applies it; the API server creates it and keeps it pointed at itself. A kube-apiserver flag, &lt;code&gt;--kubernetes-service-node-port&lt;/code&gt;, tells it to create that Service as a NodePort instead. That opens the API server on a high port on every node in the cluster. The catch: the API server only picks the type when it creates the Service. Remove the flag, and the Service that already exists stays a NodePort until you delete it and let the API server recreate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The exposure
&lt;/h2&gt;

&lt;p&gt;We are on the control plane node. The kubernetes Service is type NodePort, with port four four three mapped to three zero four four three. And look at what an anonymous request gets through that port: no credentials at all, and the API server hands back its exact version. That is a gift to an attacker scanning for known vulnerable builds, and it is available on every node's IP.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get svc kubernetes
&lt;span class="go"&gt;NAME         TYPE       CLUSTER-IP   EXTERNAL-IP   PORT(S)         AGE
&lt;/span&gt;&lt;span class="gp"&gt;kubernetes   NodePort   10.96.0.1    &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;443:30443/TCP   3s
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-sk&lt;/span&gt; https://172.18.0.4:30443/version | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 5
&lt;span class="go"&gt;{
  "major": "1",
  "minor": "37",
  "emulationMajor": "1",
  "emulationMinor": "37",
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Find the flag
&lt;/h2&gt;

&lt;p&gt;Since the API server owns this Service, the cause is in the API server's configuration. On a kubeadm cluster that is the static pod manifest, &lt;code&gt;kube-apiserver.yaml&lt;/code&gt;, in &lt;code&gt;/etc/kubernetes/manifests&lt;/code&gt;. Grep it for node-port and there it is, on line sixteen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; node-port /etc/kubernetes/manifests/kube-apiserver.yaml
&lt;span class="go"&gt;16:    - --kubernetes-service-node-port=30443
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Back up the manifest
&lt;/h2&gt;

&lt;p&gt;Before editing a control plane manifest, back it up, and put the backup somewhere other than the manifests folder. Here it goes to &lt;code&gt;/root&lt;/code&gt;. If you drop a copy in the manifests folder, the kubelet treats it as a second static pod and tries to run two API servers, which is a great way to lose the exam's cluster for ten minutes. The listing shows the manifests folder still has exactly four files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /etc/kubernetes/manifests/kube-apiserver.yaml /root/kube-apiserver.yaml.bak &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; /root /etc/kubernetes/manifests
&lt;span class="go"&gt;/etc/kubernetes/manifests:
etcd.yaml
kube-apiserver.yaml
kube-controller-manager.yaml
kube-scheduler.yaml

/root:
kube-apiserver.yaml.bak
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Remove the flag
&lt;/h2&gt;

&lt;p&gt;Now open the manifest in vi, find the node-port line and delete it with d d, then save with colon w q. Setting the value to zero would also work, but removing the line leaves the manifest exactly as kubeadm would write it. The kubelet watches this folder, so saving the file is enough to restart the API server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'    - --'&lt;/span&gt; /etc/kubernetes/manifests/kube-apiserver.yaml | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 8
&lt;span class="go"&gt;    - --advertise-address=172.18.0.4
    - --kubernetes-service-node-port=30443
    - --allow-privileged=true
    - --authorization-mode=Node,RBAC
    - --client-ca-file=/etc/kubernetes/pki/ca.crt
    - --enable-admission-plugins=NodeRestriction
    - --enable-bootstrap-token-auth=true
    - --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'    - --'&lt;/span&gt; /etc/kubernetes/manifests/kube-apiserver.yaml | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 7
&lt;span class="go"&gt;    - --advertise-address=172.18.0.4
    - --allow-privileged=true
    - --authorization-mode=Node,RBAC
    - --client-ca-file=/etc/kubernetes/pki/ca.crt
    - --enable-admission-plugins=NodeRestriction
    - --enable-bootstrap-token-auth=true
    - --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Still a NodePort
&lt;/h2&gt;

&lt;p&gt;Give it thirty seconds or so. crictl shows a brand new API server container, created a few seconds ago, so the change took. Now check the Service again. Still NodePort, still three zero four four three. This is where people assume the edit failed and start changing other things. It did not fail: the API server only sets the type when it creates the Service, and this one already exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;crictl ps &lt;span class="nt"&gt;--name&lt;/span&gt; kube-apiserver &lt;span class="nt"&gt;-o&lt;/span&gt; table
&lt;span class="go"&gt;CONTAINER           IMAGE               CREATED             STATE               NAME                ATTEMPT   
92213fa10199c       bec5f0e1e2eeb       2 seconds ago       Running             kube-apiserver      0

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get svc kubernetes
&lt;span class="go"&gt;NAME         TYPE       CLUSTER-IP   EXTERNAL-IP   PORT(S)         AGE
&lt;/span&gt;&lt;span class="gp"&gt;kubernetes   NodePort   10.96.0.1    &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;443:30443/TCP   42s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Recreate and verify
&lt;/h2&gt;

&lt;p&gt;So delete it. That sounds scary, but the API server owns this Service and recreates it within a second or two, this time using its new configuration. There it is: ClusterIP, port four four three, no node port. And the final proof: the same request to three zero four four three that leaked the version a minute ago now cannot even connect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl delete svc kubernetes
&lt;span class="go"&gt;service "kubernetes" deleted from default namespace

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get svc kubernetes
&lt;span class="go"&gt;NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
&lt;/span&gt;&lt;span class="gp"&gt;kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;443/TCP   6s
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-sSk&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 5 https://172.18.0.4:30443/version
&lt;span class="go"&gt;curl: (7) Failed to connect to 172.18.0.4 port 30443 after 0 ms: Could not connect to server
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exam tips
&lt;/h2&gt;

&lt;p&gt;Tips for the exam. Grep the manifest for the setting instead of scrolling. Back up to a folder outside the manifests directory, never inside it. After saving, wait for a new API server container with crictl or watch, and remember kubectl will refuse connections for a while during the restart; that is normal. If the API server does not come back, check crictl ps dash a and the container logs for a typo. And when the Service type has not changed, delete the kubernetes Service; the API server recreates it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;grep -n the manifest for the flag&lt;/li&gt;
&lt;li&gt;Backup OUTSIDE /etc/kubernetes/manifests&lt;/li&gt;
&lt;li&gt;Wait for the restart: crictl ps, kubectl refuses meanwhile&lt;/li&gt;
&lt;li&gt;Not back? crictl ps -a + crictl logs for the typo&lt;/li&gt;
&lt;li&gt;Type unchanged? delete svc kubernetes; it is recreated&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The kubernetes Service is owned by the API server&lt;/li&gt;
&lt;li&gt;Remove --kubernetes-service-node-port from the static pod&lt;/li&gt;
&lt;li&gt;Delete the Service so it is recreated as ClusterIP&lt;/li&gt;
&lt;li&gt;Verify the port is closed; subscribe + dev.to writeup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce this yourself
&lt;/h2&gt;

&lt;p&gt;The entire scenario is scripted on a throwaway &lt;code&gt;kind&lt;/code&gt; cluster: &lt;a href="https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios" rel="noopener noreferrer"&gt;https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios.git
&lt;span class="nb"&gt;cd &lt;/span&gt;TCS_CKS_2026_Exam_Scenarios/scenario3-apiserver-nodeport
./setup.sh        &lt;span class="c"&gt;# creates the cluster AND arms the scenario&lt;/span&gt;
&lt;span class="c"&gt;# solve it by hand, or:&lt;/span&gt;
./solution.sh     &lt;span class="c"&gt;# apply the answer key and verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;If this helped, &lt;strong&gt;subscribe to The Cyber SideKick&lt;/strong&gt; on YouTube for more CKS drills, and grab the newsletter at &lt;a href="https://thecybersidekick.beehiiv.com" rel="noopener noreferrer"&gt;https://thecybersidekick.beehiiv.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cks</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Pull a user's client certificate out of a kubeconfig (CKS)</title>
      <dc:creator>The Cyber Sidekick</dc:creator>
      <pubDate>Mon, 05 Oct 2026 13:14:58 +0000</pubDate>
      <link>https://dev.to/thecybersidekick/pull-a-users-client-certificate-out-of-a-kubeconfig-cks-1k52</link>
      <guid>https://dev.to/thecybersidekick/pull-a-users-client-certificate-out-of-a-kubeconfig-cks-1k52</guid>
      <description>&lt;h2&gt;
  
  
  Pull a user's client certificate out of a kubeconfig (CKS)
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;🎥 &lt;strong&gt;Watch the video:&lt;/strong&gt; &lt;a href="https://www.youtube.com/watch?v=ikI5PO3UWtE" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=ikI5PO3UWtE&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;h2&gt;
  
  
  The scenario
&lt;/h2&gt;

&lt;p&gt;Here is the setup. Your workstation kubeconfig reaches several clusters, so it has several contexts. Two things are asked. Write every context name into &lt;code&gt;contexts.txt&lt;/code&gt;, one per line. Then take the user called auditor at payments-stage, and write that user's client certificate, decoded, into &lt;code&gt;auditor.crt&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your kubeconfig holds several contexts&lt;/li&gt;
&lt;li&gt;1. Every context name into contexts.txt, one per line&lt;/li&gt;
&lt;li&gt;2. User auditor@payments-stage: client certificate, DECODED, into auditor.crt&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Anatomy of a kubeconfig
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;client-certificate-data&lt;/code&gt; 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 &lt;code&gt;--raw&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are handed
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl config get-contexts
&lt;span class="go"&gt;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
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Context names to a file
&lt;/h2&gt;

&lt;p&gt;Part one is a single flag. &lt;code&gt;-o name&lt;/code&gt; 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 &lt;code&gt;contexts.txt&lt;/code&gt; and cat it back. Never copy the table by hand; a missed character is a lost point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl config get-contexts &lt;span class="nt"&gt;-o&lt;/span&gt; name
&lt;span class="go"&gt;auditor@payments-stage
ci@build-farm
kind-cks-scenario2
ops@payments-prod

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl config get-contexts &lt;span class="nt"&gt;-o&lt;/span&gt; name &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; contexts.txt &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cat &lt;/span&gt;contexts.txt
&lt;span class="go"&gt;auditor@payments-stage
ci@build-farm
kind-cks-scenario2
ops@payments-prod
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Redacted versus raw
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;--raw&lt;/code&gt; and ask for &lt;code&gt;client-certificate-data&lt;/code&gt;, 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl config view &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.users[?(@.name=="auditor@payments-stage")].user}'&lt;/span&gt;
&lt;span class="go"&gt;{"client-certificate-data":"DATA+OMITTED","client-key-data":"DATA+OMITTED"}

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl config view &lt;span class="nt"&gt;--raw&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.users[?(@.name=="auditor@payments-stage")].user.client-certificate-data}'&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-c1-60&lt;/span&gt;
&lt;span class="go"&gt;LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURFekNDQWZ1Z0F3SUJB
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Decode the certificate
&lt;/h2&gt;

&lt;p&gt;Now pipe it through &lt;code&gt;base64 -d&lt;/code&gt; and redirect into &lt;code&gt;auditor.crt&lt;/code&gt;. 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl config view &lt;span class="nt"&gt;--raw&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.users[?(@.name=="auditor@payments-stage")].user.client-certificate-data}'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; auditor.crt &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 3 auditor.crt
&lt;span class="go"&gt;-----BEGIN CERTIFICATE-----
MIIDEzCCAfugAwIBAgIQDd249qkPHnr4qN1/QDlakDANBgkqhkiG9w0BAQsFADAV
MRMwEQYDVQQDEwprdWJlcm5ldGVzMB4XDTI2MDkyMzEzMjkzOFoXDTI2MTAyMzEz
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Read the certificate
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;openssl x509 &lt;span class="nt"&gt;-in&lt;/span&gt; auditor.crt &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-subject&lt;/span&gt; &lt;span class="nt"&gt;-issuer&lt;/span&gt; &lt;span class="nt"&gt;-enddate&lt;/span&gt;
&lt;span class="go"&gt;subject=O=payments-auditors, CN=auditor
issuer=CN=kubernetes
notAfter=Oct 23 13:29:38 2026 GMT
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Who is this user
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;--context&lt;/span&gt; auditor@payments-stage auth &lt;span class="nb"&gt;whoami&lt;/span&gt;
&lt;span class="go"&gt;ATTRIBUTE                                           VALUE
Username                                            auditor
Groups                                              [payments-auditors system:authenticated]
Extra: authentication.kubernetes.io/credential-id   [X509SHA256=9961016f3aeb10ed0518d8f230e4d218ece2079c2e3a86694f015e7edd1bc519]

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;--context&lt;/span&gt; auditor@payments-stage get pods &lt;span class="nt"&gt;-A&lt;/span&gt;
&lt;span class="go"&gt;Error from server (Forbidden): pods is forbidden: User "auditor" cannot list resource "pods" in API group "" at the cluster scope
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exam tips
&lt;/h2&gt;

&lt;p&gt;Tips for the exam. Use &lt;code&gt;-o name&lt;/code&gt; for anything that asks for names one per line. Remember that config view redacts, so the certificate needs &lt;code&gt;--raw&lt;/code&gt;. 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.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;get-contexts -o name: names only, one per line&lt;/li&gt;
&lt;li&gt;config view redacts: add --raw for certificate data&lt;/li&gt;
&lt;li&gt;jsonpath filter on the user name, not grep -A&lt;/li&gt;
&lt;li&gt;Decoded = PEM: the file starts with BEGIN CERTIFICATE&lt;/li&gt;
&lt;li&gt;Mind which kubeconfig: --kubeconfig or KUBECONFIG&lt;/li&gt;
&lt;li&gt;A kubeconfig with embedded keys is a credential&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;get-contexts -o name &amp;gt; file&lt;/li&gt;
&lt;li&gt;config view --raw + jsonpath + base64 -d = PEM certificate&lt;/li&gt;
&lt;li&gt;CN = username, O = group&lt;/li&gt;
&lt;li&gt;authN is not authZ; subscribe + dev.to writeup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce this yourself
&lt;/h2&gt;

&lt;p&gt;The entire scenario is scripted on a throwaway &lt;code&gt;kind&lt;/code&gt; cluster: &lt;a href="https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios" rel="noopener noreferrer"&gt;https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios.git
&lt;span class="nb"&gt;cd &lt;/span&gt;TCS_CKS_2026_Exam_Scenarios/scenario2-kubeconfig-cert
./setup.sh        &lt;span class="c"&gt;# creates the cluster AND arms the scenario&lt;/span&gt;
&lt;span class="c"&gt;# solve it by hand, or:&lt;/span&gt;
./solution.sh     &lt;span class="c"&gt;# apply the answer key and verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;If this helped, &lt;strong&gt;subscribe to The Cyber SideKick&lt;/strong&gt; on YouTube for more CKS drills, and grab the newsletter at &lt;a href="https://thecybersidekick.beehiiv.com" rel="noopener noreferrer"&gt;https://thecybersidekick.beehiiv.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cks</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Decode Kubernetes Secrets, then mount one read-only (CKS)</title>
      <dc:creator>The Cyber Sidekick</dc:creator>
      <pubDate>Fri, 02 Oct 2026 13:07:26 +0000</pubDate>
      <link>https://dev.to/thecybersidekick/decode-kubernetes-secrets-then-mount-one-read-only-cks-l3i</link>
      <guid>https://dev.to/thecybersidekick/decode-kubernetes-secrets-then-mount-one-read-only-cks-l3i</guid>
      <description>&lt;h2&gt;
  
  
  Decode Kubernetes Secrets, then mount one read-only (CKS)
&lt;/h2&gt;

&lt;p&gt;Welcome to lesson one of the CKS series. We start with Secrets, because they show up in nearly every version of the exam, and because the first thing a security engineer should learn about them is how easy they are to read. Two parts today: pull values out of existing Secrets and write them to a file, then create a new Secret and mount it into a pod read-only. Let's run it on a live cluster.&lt;/p&gt;

&lt;p&gt;This is a CKS Minimize Microservice Vulnerabilities walkthrough. Every command below is real output from a live cluster, and you can reproduce the whole thing yourself (scripts at the end).&lt;/p&gt;

&lt;h2&gt;
  
  
  The scenario
&lt;/h2&gt;

&lt;p&gt;Here is the setup. The finance namespace holds two Secrets, vault-seed and ops-login, and each has more than one key. You want the token key from vault-seed and the user key from ops-login, decoded to plain text and written to a file called &lt;code&gt;decoded.txt&lt;/code&gt;, one value per line, token first. Then, in the billing namespace, create a Secret called billing-api holding a username and a password, and run a pod called invoice-runner that mounts it read-only at &lt;code&gt;/etc/billing&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1. ns finance: decode 'token' of vault-seed and 'user' of ops-login&lt;/li&gt;
&lt;li&gt;Write both to decoded.txt, one value per line, token first&lt;/li&gt;
&lt;li&gt;2. ns billing: create Secret billing-api (username + password)&lt;/li&gt;
&lt;li&gt;Pod invoice-runner (busybox:1.36) mounts it READ-ONLY at /etc/billing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why base64 is not encryption
&lt;/h2&gt;

&lt;p&gt;One idea to have straight before typing. The values under data in a Secret are base64 encoded, and base64 is an encoding, not encryption. There is no key. Anyone who can run get on that Secret can read it in one line, which is why CKS spends so much time on RBAC and encryption at rest. The second idea is newlines. &lt;code&gt;base64 -d&lt;/code&gt; prints the value with no newline after it, so when you write several values to a file you add the newline yourself. And when you encode by hand, use &lt;code&gt;echo -n&lt;/code&gt;, because a plain echo slips a newline into the value and the base64 comes out different.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are handed
&lt;/h2&gt;

&lt;p&gt;Start by looking. List the Secrets in finance: two Opaque Secrets, each with two data entries, so you have to pick the right key rather than decode everything. Then look at vault-seed in YAML. Under data you can see both keys, token and endpoint, and their values are not readable yet. They look scrambled, but that is just base64.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get secrets &lt;span class="nt"&gt;-n&lt;/span&gt; finance
&lt;span class="go"&gt;NAME         TYPE     DATA   AGE
ops-login    Opaque   2      0s
vault-seed   Opaque   2      0s

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; finance get secret vault-seed &lt;span class="nt"&gt;-o&lt;/span&gt; yaml | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A2&lt;/span&gt; &lt;span class="s1"&gt;'^data:'&lt;/span&gt;
&lt;span class="go"&gt;data:
  endpoint: aHR0cHM6Ly92YXVsdC5maW5hbmNlLnN2Yzo4MjAw
  token: cjNkLWw0bnQzcm4tOTA0MQ==
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Decode each key
&lt;/h2&gt;

&lt;p&gt;Now decode. Ask kubectl for just the one key with jsonpath, pipe it into &lt;code&gt;base64 -d&lt;/code&gt;, and the plain value appears. The trailing echo only moves the prompt onto a new line so you can read it. Same move for the user key of ops-login. And here is the newline trap, shown for real. Encode the value back with &lt;code&gt;echo -n&lt;/code&gt; and you get the exact string stored in the Secret. Drop the dash n and the base64 changes, because the newline is now part of the value. If you ever build a Secret by hand in the exam, that one flag is the difference between a correct and a broken credential.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; finance get secret vault-seed &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.data.token}'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;span class="go"&gt;r3d-l4nt3rn-9041

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; finance get secret ops-login &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.data.user}'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;span class="go"&gt;sre-oncall

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; sre-oncall | &lt;span class="nb"&gt;base64&lt;/span&gt;
&lt;span class="go"&gt;c3JlLW9uY2FsbA==

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo &lt;/span&gt;sre-oncall | &lt;span class="nb"&gt;base64&lt;/span&gt;
&lt;span class="go"&gt;c3JlLW9uY2FsbAo=
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Write the answer file
&lt;/h2&gt;

&lt;p&gt;Now the answer file. Group both commands in braces with an echo after each decode, and redirect the whole group into &lt;code&gt;decoded.txt&lt;/code&gt;. Doing it in one go means no copy and paste, and no typo in a value you cannot see properly. Then check it with &lt;code&gt;cat -A&lt;/code&gt;, which marks every line end with a dollar sign. Two lines, the token first, each ending cleanly. That is exactly the format the grader compares against.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt; kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; finance get secret vault-seed &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.data.token}'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; finance get secret ops-login &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.data.user}'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; decoded.txt
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; decoded.txt
&lt;span class="gp"&gt;r3d-l4nt3rn-9041$&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;sre-oncall$&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create the new Secret
&lt;/h2&gt;

&lt;p&gt;Part two. Create the Secret imperatively, in the billing namespace, with one &lt;code&gt;--from-literal&lt;/code&gt; flag per key. kubectl does the base64 for you, so there is no echo, no newline to worry about, and nothing to encode by hand. This is the fastest correct way in the exam; only reach for YAML if the task hands you one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; billing create secret generic billing-api &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;svc-invoice &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Qz7-ledger-2026
&lt;span class="go"&gt;secret/billing-api created
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Mount it read-only
&lt;/h2&gt;

&lt;p&gt;Now the pod. Generate a skeleton with kubectl run and dry-run, then add two blocks. Under volumes, a volume of type secret that names billing-api. Under the container, a volumeMount with that volume's name, the mount path &lt;code&gt;/etc/billing&lt;/code&gt;, and &lt;code&gt;readOnly: true&lt;/code&gt;. That last line is the one the task is really about. Apply it and wait for the pod to be Ready.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;invoice-runner.yaml
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;  terminationGracePeriodSeconds: 5
  containers:
    - name: runner
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
      volumeMounts:
        - name: api-creds
          mountPath: /etc/billing
          readOnly: true
  volumes:
    - name: api-creds
      secret:
        secretName: billing-api

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; invoice-runner.yaml &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; billing &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Ready pod/invoice-runner
&lt;span class="go"&gt;pod/invoice-runner created
pod/invoice-runner condition met
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Prove it
&lt;/h2&gt;

&lt;p&gt;Prove both halves. Inside the pod, &lt;code&gt;/etc/billing&lt;/code&gt; holds one file per key, and reading username gives the plain value: the kubelet decodes it for the container. Now try to write there, and the file system refuses: read-only. To be honest about what you are seeing, the kubelet mounts secret volumes read-only anyway. But the grader checks the pod spec, not your intentions, so the last check reads the volumeMount itself, and it says &lt;code&gt;readOnly: true&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; billing &lt;span class="nb"&gt;exec &lt;/span&gt;invoice-runner &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; /etc/billing
&lt;span class="go"&gt;password
username

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; billing &lt;span class="nb"&gt;exec &lt;/span&gt;invoice-runner &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; /etc/billing/username
&lt;span class="go"&gt;svc-invoice

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; billing &lt;span class="nb"&gt;exec &lt;/span&gt;invoice-runner &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;touch&lt;/span&gt; /etc/billing/tamper
&lt;span class="go"&gt;touch: /etc/billing/tamper: Read-only file system
command terminated with exit code 1

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; billing get pod invoice-runner &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.containers[0].volumeMounts[0]}'&lt;/span&gt;
&lt;span class="go"&gt;{"mountPath":"/etc/billing","name":"api-creds","readOnly":true}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exam tips
&lt;/h2&gt;

&lt;p&gt;Things to carry into the exam. Always pass the namespace; both Secrets here live outside default. Decode a single key with jsonpath instead of eyeballing the YAML, and remember the dot in a key name needs a backslash in jsonpath. Write the file in one redirect, then confirm with &lt;code&gt;cat -A&lt;/code&gt;. If you encode by hand, &lt;code&gt;echo -n&lt;/code&gt;, every time. Prefer &lt;code&gt;--from-literal&lt;/code&gt; for new Secrets. And set &lt;code&gt;readOnly: true&lt;/code&gt; on the mount explicitly, even though the kubelet would do it for you. If the file path is on a different host in the exam, remember to go back to the base terminal before you check your work.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always -n ; neither Secret is in default&lt;/li&gt;
&lt;li&gt;jsonpath '{.data.}' | base64 -d; escape dots in key names&lt;/li&gt;
&lt;li&gt;One redirect for the file, then cat -A to check the line ends&lt;/li&gt;
&lt;li&gt;echo -n when encoding by hand&lt;/li&gt;
&lt;li&gt;kubectl create secret generic --from-literal: no manual base64&lt;/li&gt;
&lt;li&gt;Set readOnly: true on the mount explicitly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Secret data is base64: encoding, not encryption&lt;/li&gt;
&lt;li&gt;jsonpath + base64 -d; add a newline per value; check with cat -A&lt;/li&gt;
&lt;li&gt;echo -n or the newline becomes part of the value&lt;/li&gt;
&lt;li&gt;--from-literal Secret, mounted readOnly: true; subscribe + dev.to writeup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce this yourself
&lt;/h2&gt;

&lt;p&gt;The entire scenario is scripted on a throwaway &lt;code&gt;kind&lt;/code&gt; cluster: &lt;a href="https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios" rel="noopener noreferrer"&gt;https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios.git
&lt;span class="nb"&gt;cd &lt;/span&gt;TCS_CKS_2026_Exam_Scenarios/scenario1-secret-decode
./setup.sh        &lt;span class="c"&gt;# creates the cluster AND arms the scenario&lt;/span&gt;
&lt;span class="c"&gt;# solve it by hand, or:&lt;/span&gt;
./solution.sh     &lt;span class="c"&gt;# apply the answer key and verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;If this helped, &lt;strong&gt;subscribe to The Cyber SideKick&lt;/strong&gt; on YouTube for more CKS drills, and grab the newsletter at &lt;a href="https://thecybersidekick.beehiiv.com" rel="noopener noreferrer"&gt;https://thecybersidekick.beehiiv.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cks</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>List an operator's CRDs, then read a field's docs with kubectl explain (CKA)</title>
      <dc:creator>The Cyber Sidekick</dc:creator>
      <pubDate>Thu, 27 Aug 2026 16:44:04 +0000</pubDate>
      <link>https://dev.to/thecybersidekick/list-an-operators-crds-then-read-a-fields-docs-with-kubectl-explain-cka-53om</link>
      <guid>https://dev.to/thecybersidekick/list-an-operators-crds-then-read-a-fields-docs-with-kubectl-explain-cka-53om</guid>
      <description>&lt;h2&gt;
  
  
  List an operator's CRDs, then read a field's docs with kubectl explain (CKA)
&lt;/h2&gt;

&lt;p&gt;Most CKA questions ask you to change something. This one asks you to change nothing at all, and it is still worth two marks. An application is already installed in the cluster, and you have to produce two files about it: the list of custom resource definitions it registered, and the documentation for one field buried inside one of those custom resources. Both are single commands, and both have a detail in the wording that decides whether you get the marks. Let's run it on a live cluster.&lt;/p&gt;

&lt;p&gt;🎥 &lt;strong&gt;Watch the video:&lt;/strong&gt; &lt;a href="https://www.youtube.com/watch?v=lCXp7g1aJ0M" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=lCXp7g1aJ0M&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a CKA Cluster Architecture, Installation &amp;amp; Configuration walkthrough. Every command below is real output from a live cluster, and you can reproduce the whole thing yourself (scripts at the end).&lt;/p&gt;

&lt;h2&gt;
  
  
  The scenario
&lt;/h2&gt;

&lt;p&gt;Here is the setup. The cert-manager application is already running in the cluster, in the pki-system namespace, and it is not the only operator that has installed custom resource definitions here. Two deliverables. First, save the list of the custom resource definitions that belong to cert-manager into a file called &lt;code&gt;crd-inventory.yaml&lt;/code&gt;, using kubectl's default output format, and that phrase is the whole trick of part one. Second, using kubectl, extract the documentation for the subject field inside a Certificate's spec, and save it to &lt;code&gt;subject-doc.yaml&lt;/code&gt;. For that one, any output format kubectl supports is accepted. Nothing in the cluster gets modified.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cert-manager is already installed, in ns 'pki-system', alongside other operators&lt;/li&gt;
&lt;li&gt;1. Save cert-manager's CRDs to crd-inventory.yaml, in the DEFAULT output format&lt;/li&gt;
&lt;li&gt;2. Save the docs for a Certificate's spec.subject field to subject-doc.yaml&lt;/li&gt;
&lt;li&gt;Read-only: nothing in the cluster changes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How CRDs document themselves
&lt;/h2&gt;

&lt;p&gt;One idea makes both halves easy. A custom resource definition adds a new type to the Kubernetes API, and the definition carries an OpenAPI schema that describes every field of that type, including the description text the author wrote. The API server publishes that schema alongside the schemas of the built-in types. That is why &lt;code&gt;kubectl explain&lt;/code&gt; works on a custom resource exactly as well as it works on a Pod: it is reading the cluster's own schema, not a documentation website, so it works with no internet access, which matters on exam day. The same applies to &lt;code&gt;kubectl api-resources&lt;/code&gt;, which lists every type the API server currently serves, along with its API group and its short names. The cluster documents itself, and both parts of this question are just asking you to know that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is installed
&lt;/h2&gt;

&lt;p&gt;Start by looking at what is actually there. The application is running in pki-system, three deployments worth of it, healthy. Now list the custom resource definitions in the cluster, and notice the shape of that list: twelve of them, and only some belong to cert-manager. The others are Gateway API definitions from a different operator entirely. This is why the question says the CRDs of the cert-manager application, and not simply all the CRDs. If you want the same information from the resource side, &lt;code&gt;kubectl api-resources&lt;/code&gt; with an API group filter shows the types cert-manager serves, with their short names and their kinds, which is a handy thing to know exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; pki-system
&lt;span class="go"&gt;NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-689c4c5575-snfgw              1/1     Running   0          125m
cert-manager-cainjector-6fbb9c8cd6-lrjvh   1/1     Running   0          125m
cert-manager-webhook-646c95c5ff-p599p      1/1     Running   0          125m

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get crd
&lt;span class="go"&gt;NAME                                           CREATED AT
backendtlspolicies.gateway.networking.k8s.io   2026-07-31T17:49:43Z
certificaterequests.cert-manager.io            2026-07-31T17:49:06Z
certificates.cert-manager.io                   2026-07-31T17:49:06Z
challenges.acme.cert-manager.io                2026-07-31T17:49:06Z
clusterissuers.cert-manager.io                 2026-07-31T17:49:06Z
gatewayclasses.gateway.networking.k8s.io       2026-07-31T17:49:43Z
gateways.gateway.networking.k8s.io             2026-07-31T17:49:43Z
grpcroutes.gateway.networking.k8s.io           2026-07-31T17:49:43Z
httproutes.gateway.networking.k8s.io           2026-07-31T17:49:43Z
issuers.cert-manager.io                        2026-07-31T17:49:06Z
orders.acme.cert-manager.io                    2026-07-31T17:49:06Z
referencegrants.gateway.networking.k8s.io      2026-07-31T17:49:43Z

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl api-resources &lt;span class="nt"&gt;--api-group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cert-manager.io
&lt;span class="go"&gt;NAME                  SHORTNAMES   APIVERSION           NAMESPACED   KIND
certificaterequests   cr,crs       cert-manager.io/v1   true         CertificateRequest
certificates          cert,certs   cert-manager.io/v1   true         Certificate
clusterissuers        ciss         cert-manager.io/v1   false        ClusterIssuer
issuers               iss          cert-manager.io/v1   true         Issuer
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deliverable one
&lt;/h2&gt;

&lt;p&gt;Part one, then. Get the custom resource definitions, filter to the ones whose names carry the cert-manager API group, and redirect that into &lt;code&gt;crd-inventory.yaml&lt;/code&gt;. Read the command carefully, because what is interesting is what is missing: there is no &lt;code&gt;-o&lt;/code&gt; flag anywhere. The default table is the format the question asked for, so the correct move is to not reach for anything. Read the file back and there are the six definitions, certificates, certificate requests, issuers, cluster issuers, and the two ACME ones, challenges and orders. Six lines, exactly what the grader is looking for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get crd | &lt;span class="nb"&gt;grep &lt;/span&gt;cert-manager &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; crd-inventory.yaml
&lt;span class="go"&gt;(no output: it all went into the file)

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;crd-inventory.yaml
&lt;span class="go"&gt;certificaterequests.cert-manager.io            2026-07-31T17:49:06Z
certificates.cert-manager.io                   2026-07-31T17:49:06Z
challenges.acme.cert-manager.io                2026-07-31T17:49:06Z
clusterissuers.cert-manager.io                 2026-07-31T17:49:06Z
issuers.cert-manager.io                        2026-07-31T17:49:06Z
orders.acme.cert-manager.io                    2026-07-31T17:49:06Z
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The format trap
&lt;/h2&gt;

&lt;p&gt;It is worth seeing why that instruction exists. If you reach for &lt;code&gt;-o yaml&lt;/code&gt; out of habit, here is what you would have written into the file: twenty six thousand lines. Every definition dumped in full, complete with its entire schema. It is not a slightly different answer, it is a different artifact, and it fails the requirement. One refinement is allowed though. If losing the header bothers you, widen the pattern to keep the header line as well, and you get a table with column names and only the rows you want. Still the default format, still no &lt;code&gt;-o&lt;/code&gt; flag, and slightly nicer to read.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get crd &lt;span class="nt"&gt;-o&lt;/span&gt; yaml | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="go"&gt;26006

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get crd | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'NAME|cert-manager'&lt;/span&gt;
&lt;span class="go"&gt;NAME                                           CREATED AT
certificaterequests.cert-manager.io            2026-07-31T17:49:06Z
certificates.cert-manager.io                   2026-07-31T17:49:06Z
challenges.acme.cert-manager.io                2026-07-31T17:49:06Z
clusterissuers.cert-manager.io                 2026-07-31T17:49:06Z
issuers.cert-manager.io                        2026-07-31T17:49:06Z
orders.acme.cert-manager.io                    2026-07-31T17:49:06Z
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  kubectl explain
&lt;/h2&gt;

&lt;p&gt;Part two asks for documentation, not data, and the tool for that is &lt;code&gt;kubectl explain&lt;/code&gt;. Give it the resource followed by the field path, with dots: &lt;code&gt;certificates.spec.subject&lt;/code&gt;. Out comes real documentation: a description of what the field is, and then every attribute nested inside it. Countries, localities, organizational units, organizations, postal codes, provinces, a serial number, street addresses, each with its own type and its own sentence of explanation. That is a certificate's X509 subject, described by the people who wrote the custom resource, and it came out of the cluster rather than out of a browser.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl explain certificates.spec.subject
&lt;span class="go"&gt;GROUP:      cert-manager.io
KIND:       Certificate
VERSION:    v1

&lt;/span&gt;&lt;span class="gp"&gt;FIELD: subject &amp;lt;Object&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;

DESCRIPTION:
    Requested set of X509 certificate subject attributes.
&lt;/span&gt;&lt;span class="gp"&gt;    More info: https://datatracker.ietf.org/doc/html/rfc5280#&lt;/span&gt;section-4.1.2.6
&lt;span class="go"&gt;
    The common name attribute is specified separately in the `commonName` field.
    Cannot be set if the `literalSubject` field is set.

FIELDS:
&lt;/span&gt;&lt;span class="gp"&gt;  countries &amp;lt;[]string&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;    Countries to be used on the Certificate.

&lt;/span&gt;&lt;span class="gp"&gt;  localities    &amp;lt;[]string&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;    Cities to be used on the Certificate.

&lt;/span&gt;&lt;span class="gp"&gt;  organizationalUnits   &amp;lt;[]string&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deliverable two
&lt;/h2&gt;

&lt;p&gt;So redirect that same command into &lt;code&gt;subject-doc.yaml&lt;/code&gt;. The question said any output format kubectl supports is acceptable here, which is a way of saying it does not care, so the plain explain output is fine as it stands. Now read the top of the file back, and there is the header: the group, the kind and the version, so there is no ambiguity about which type this documents, then the field, its type, and the description with a link to the RFC. Then list the directory. Two files, both non-empty, both produced by kubectl alone, and not one object in the cluster was touched.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl explain certificates.spec.subject &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; subject-doc.yaml
&lt;span class="go"&gt;(no output: it all went into the file)

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 12 subject-doc.yaml
&lt;span class="go"&gt;GROUP:      cert-manager.io
KIND:       Certificate
VERSION:    v1

&lt;/span&gt;&lt;span class="gp"&gt;FIELD: subject &amp;lt;Object&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;

DESCRIPTION:
    Requested set of X509 certificate subject attributes.
&lt;/span&gt;&lt;span class="gp"&gt;    More info: https://datatracker.ietf.org/doc/html/rfc5280#&lt;/span&gt;section-4.1.2.6
&lt;span class="go"&gt;
    The common name attribute is specified separately in the `commonName` field.

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="go"&gt;total 8
-rw-r--r-- 1 jeffv jeffv  408 Jul 31 15:54 crd-inventory.yaml
-rw-r--r-- 1 jeffv jeffv 1006 Jul 31 15:54 subject-doc.yaml
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Going deeper
&lt;/h2&gt;

&lt;p&gt;Two things worth knowing before we leave. The field path keeps going: add another dot and the name of one of those attributes, and kubectl explains just that leaf, its type, a list of strings, and its description. Any depth, any field, same command. And if you are wondering where that text physically comes from, read it straight out of the definition object with a jsonpath into the schema. The same sentences, sitting in the CRD's OpenAPI schema, which is what the API server hands to kubectl. Nothing magic, and nothing that needs the internet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl explain certificates.spec.subject.organizations
&lt;span class="go"&gt;GROUP:      cert-manager.io
KIND:       Certificate
VERSION:    v1

&lt;/span&gt;&lt;span class="gp"&gt;FIELD: organizations &amp;lt;[]string&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;

DESCRIPTION:
    Organizations to be used on the Certificate.

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get crd certificates.cert-manager.io &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.subject.description}'&lt;/span&gt;
&lt;span class="go"&gt;Requested set of X509 certificate subject attributes.
&lt;/span&gt;&lt;span class="gp"&gt;More info: https://datatracker.ietf.org/doc/html/rfc5280#&lt;/span&gt;section-4.1.2.6
&lt;span class="go"&gt;
The common name attribute is specified separately in the `commonName` field.
Cannot be set if the `literalSubject` field is set.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exam tips
&lt;/h2&gt;

&lt;p&gt;Things to carry into the exam. Default output format means no &lt;code&gt;-o&lt;/code&gt; flag at all, so resist the habit; when a question specifies a format, that is a graded requirement, not advice. Do not assume an operator lives in a namespace named after itself, check where it actually is. Filter CRDs by the API group in their names, and remember an application can own more than one group, as cert-manager does with its ACME definitions, so a narrow grep can silently drop rows. &lt;code&gt;kubectl explain&lt;/code&gt; takes plurals, singulars and short names, so certificates, certificate and cert all work, and the field path is dotted from the top of the object. Add the &lt;code&gt;--recursive&lt;/code&gt; flag when you want the whole field tree at once and no descriptions. And when you cannot remember which types exist at all, &lt;code&gt;kubectl api-resources&lt;/code&gt; is the map. Finally, read the file back after every redirect; an empty file scores zero just as surely as a wrong one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;'Default output format' = no -o flag; it is a graded requirement&lt;/li&gt;
&lt;li&gt;Never assume the namespace an operator was installed into&lt;/li&gt;
&lt;li&gt;Filter CRDs by API group; cert-manager also owns acme.cert-manager.io&lt;/li&gt;
&lt;li&gt;kubectl explain takes plural, singular or short name, plus a dotted field path&lt;/li&gt;
&lt;li&gt;--recursive prints the whole field tree; api-resources maps the types&lt;/li&gt;
&lt;li&gt;cat the file after every redirect: an empty deliverable scores zero&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;kubectl get crd | grep cert-manager &amp;gt; crd-inventory.yaml (no -o flag)&lt;/li&gt;
&lt;li&gt;kubectl explain certificates.spec.subject &amp;gt; subject-doc.yaml&lt;/li&gt;
&lt;li&gt;A CRD ships an OpenAPI schema; the API server publishes it; explain reads it&lt;/li&gt;
&lt;li&gt;Read-only question, two easy marks; subscribe + dev.to writeup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce this yourself
&lt;/h2&gt;

&lt;p&gt;The entire scenario is scripted on a throwaway &lt;code&gt;kind&lt;/code&gt; cluster: &lt;a href="https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios" rel="noopener noreferrer"&gt;https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios.git
&lt;span class="nb"&gt;cd &lt;/span&gt;TCS_CKA_2026_Exam_Scenarios/learning/scenarios/scenario15-crd-inventory-explain
./setup.sh        &lt;span class="c"&gt;# creates the cluster AND arms the scenario&lt;/span&gt;
&lt;span class="c"&gt;# solve it by hand, or:&lt;/span&gt;
./solution.sh     &lt;span class="c"&gt;# apply the answer key and verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;If this helped, &lt;strong&gt;subscribe to The Cyber SideKick&lt;/strong&gt; on YouTube for more CKA drills, and grab the newsletter at &lt;a href="https://thecybersidekick.beehiiv.com" rel="noopener noreferrer"&gt;https://thecybersidekick.beehiiv.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cka</category>
      <category>kubectl</category>
      <category>devops</category>
    </item>
    <item>
      <title>Add a streaming sidecar so kubectl logs can see a legacy app (CKA)</title>
      <dc:creator>The Cyber Sidekick</dc:creator>
      <pubDate>Fri, 07 Aug 2026 11:50:09 +0000</pubDate>
      <link>https://dev.to/thecybersidekick/add-a-streaming-sidecar-so-kubectl-logs-can-see-a-legacy-app-cka-hed</link>
      <guid>https://dev.to/thecybersidekick/add-a-streaming-sidecar-so-kubectl-logs-can-see-a-legacy-app-cka-hed</guid>
      <description>&lt;h2&gt;
  
  
  Add a streaming sidecar so kubectl logs can see a legacy app (CKA)
&lt;/h2&gt;

&lt;p&gt;Here is a pod that is perfectly healthy, doing real work, and completely silent as far as Kubernetes is concerned. &lt;code&gt;kubectl logs&lt;/code&gt; returns nothing at all. Today's CKA task is to fix that without rewriting the application: add a second container alongside it that streams the app's log file to standard output, and share a volume between the two so it can actually read that file. This is the streaming sidecar pattern, and it is one of the most quietly useful things on the exam. Let's run it on a live cluster.&lt;/p&gt;

&lt;p&gt;🎥 &lt;strong&gt;Watch the video:&lt;/strong&gt; &lt;a href="https://www.youtube.com/watch?v=JDQgrcbmFH0" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=JDQgrcbmFH0&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a CKA Workloads &amp;amp; Scheduling walkthrough. Every command below is real output from a live cluster, and you can reproduce the whole thing yourself (scripts at the end).&lt;/p&gt;

&lt;h2&gt;
  
  
  The scenario
&lt;/h2&gt;

&lt;p&gt;Here is the setup. A legacy application has been packaged into a Deployment called report-engine, in the legacy namespace. It writes everything it does to a log file inside its container and prints nothing to standard output, so it is invisible to the cluster's logging architecture. Three things are asked of you. Add a second, co-located container to the same pod, named log-stream, using the busybox image, whose command streams that log file to standard output. Give both containers a shared volume so the file is visible to the new container. And change nothing else about the container that is already there.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deployment 'report-engine' (ns 'legacy') logs to a FILE, not stdout&lt;/li&gt;
&lt;li&gt;1. Add a co-located container 'log-stream' (busybox) that streams that file&lt;/li&gt;
&lt;li&gt;2. Share a volume between both containers so the file is visible&lt;/li&gt;
&lt;li&gt;3. Change nothing else about the existing 'app' container&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How pod logging actually works
&lt;/h2&gt;

&lt;p&gt;One fact explains this whole question. Kubernetes has a single logging path: the kubelet captures what a container writes to standard output and standard error, and &lt;code&gt;kubectl logs&lt;/code&gt; replays that. It does not know about files, it does not look inside your container's filesystem, and there is no configuration to point it at one. So an app that appends to a log file is not badly configured, it is simply outside the pipeline. The documented fix is a streaming sidecar: put a second container in the same pod whose only job is to tail that file and echo it to its own standard output, where the pipeline picks it up. That works because containers in a pod can share volumes. Mount one volume, an &lt;code&gt;emptyDir&lt;/code&gt; is plenty, into both containers at the directory holding the log file, and both processes see the same file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The empty kubectl logs
&lt;/h2&gt;

&lt;p&gt;Let's see the problem before fixing it. The Deployment is running, one pod, READY one of one, no restarts. Nothing is broken. Now ask for its logs, and look carefully: there is no output. Not an error, not a warning, just nothing, which is the most confusing failure mode there is. The app is definitely producing output though. Exec into the container and tail the file it writes, and there are the log lines, timestamped, several of them, sitting in the container's own filesystem where kubectl cannot reach them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get deployments,pods &lt;span class="nt"&gt;-n&lt;/span&gt; legacy
&lt;span class="go"&gt;NAME                            READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/report-engine   1/1     1            1           8s

NAME                                 READY   STATUS    RESTARTS   AGE
pod/report-engine-7d75b577d9-cs5nz   1/1     Running   0          8s

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; legacy logs deploy/report-engine
&lt;span class="go"&gt;(no output at all)

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; legacy &lt;span class="nb"&gt;exec &lt;/span&gt;deploy/report-engine &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 3 /var/log/report-engine.log
&lt;span class="go"&gt;2026-07-31 10:58:03 report-engine: batch complete
2026-07-31 10:58:08 report-engine: batch complete
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Add the sidecar
&lt;/h2&gt;

&lt;p&gt;Now the edit. In the exam you would run &lt;code&gt;kubectl edit&lt;/code&gt; on the Deployment, or dump it to a file, change it, and re-apply; both are fine. Three things go in. A &lt;code&gt;volumeMount&lt;/code&gt; on the existing container, pointing at the directory that holds the log file. A second container, named log-stream, image busybox, whose command tails that same file, plus the identical &lt;code&gt;volumeMount&lt;/code&gt;. And the volume itself, an &lt;code&gt;emptyDir&lt;/code&gt;, which sits at the pod spec level next to containers, not inside them. Watch the indentation there, because that is where this question is usually lost. Notice what did not change: the app container keeps its image and its command exactly as they were.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;vi report-engine.yaml
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;        app: report-engine
    spec:
      containers:
        - name: app
          image: busybox:1.36
          command:
            - sh
            - -c
&lt;/span&gt;&lt;span class="gp"&gt;            - &amp;gt;&lt;/span&gt;-
&lt;span class="gp"&gt;              mkdir -p /var/log;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;              while true;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="gp"&gt;              echo "$&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%F %T'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; report-engine: batch &lt;span class="nb"&gt;complete&lt;/span&gt;&lt;span class="s2"&gt;" &amp;gt;&amp;gt; /var/log/report-engine.log;
&lt;/span&gt;&lt;span class="gp"&gt;              sleep 5;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;done
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;after editing&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="gp"&gt;              sleep 5;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="go"&gt;          volumeMounts:
            - name: logs
              mountPath: /var/log
        - name: log-stream
          image: busybox:1.36
          command: ["sh", "-c", "tail -n+1 -F /var/log/report-engine.log"]
          volumeMounts:
            - name: logs
              mountPath: /var/log
      volumes:
        - name: logs
          emptyDir: {}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rollout to 2/2
&lt;/h2&gt;

&lt;p&gt;Apply it. Changing the pod template starts a rolling update, so the Deployment builds a replacement pod with both containers and retires the old one. Wait for rollout status to report success, and then look at the pod list. The READY column now says two of two: two containers in a single pod, both running, sharing a network namespace and, more importantly for us, sharing a volume. One pod, not two, and that distinction is the whole point of a sidecar.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; report-engine.yaml
&lt;span class="go"&gt;deployment.apps/report-engine configured

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; legacy rollout status deployment/report-engine
&lt;span class="go"&gt;Waiting for deployment "report-engine" rollout to finish: 0 out of 1 new replicas have been updated...
Waiting for deployment "report-engine" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "report-engine" rollout to finish: 1 old replicas are pending termination...
deployment "report-engine" successfully rolled out

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; legacy
&lt;span class="go"&gt;NAME                             READY   STATUS    RESTARTS   AGE
report-engine-764cb77fd9-bgmqs   2/2     Running   0          33s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The payoff
&lt;/h2&gt;

&lt;p&gt;And here is the payoff. Ask for the logs again, this time naming the container with &lt;code&gt;-c&lt;/code&gt; log-stream, and the app's log file comes streaming out of kubectl. Same lines, same timestamps we saw hiding in that file a minute ago, except now they are flowing through the cluster's normal logging path, which means anything that collects logs from this cluster picks them up too. The application was never touched. Note the &lt;code&gt;-c&lt;/code&gt;: once a pod has more than one container, kubectl needs to know which one you mean.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; legacy logs deploy/report-engine &lt;span class="nt"&gt;-c&lt;/span&gt; log-stream &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4
&lt;span class="go"&gt;2026-07-31 10:58:31 report-engine: batch complete
2026-07-31 10:58:36 report-engine: batch complete
2026-07-31 10:58:41 report-engine: batch complete
2026-07-31 10:58:46 report-engine: batch complete
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One volume, two containers
&lt;/h2&gt;

&lt;p&gt;It is worth proving that this really is one shared volume and not a coincidence. Exec into the sidecar, the second container, and list the log file: it is right there, owned by root, growing, even though a completely different container is the one writing to it. Then read the mounts back from the Deployment. Both containers mount the same volume, named logs, at the same path. That is the mechanism in two lines of output: same volume, same mount path, two processes, one file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; legacy &lt;span class="nb"&gt;exec &lt;/span&gt;deploy/report-engine &lt;span class="nt"&gt;-c&lt;/span&gt; log-stream &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /var/log/report-engine.log
&lt;span class="go"&gt;-rw-r--r--    1 root     root           400 Jul 31 10:58 /var/log/report-engine.log

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; legacy get deployment report-engine &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{range .spec.template.spec.containers[*]}{.name}{" mounts "}{.volumeMounts[0].name}{" at "}{.volumeMounts[0].mountPath}{"\n"}{end}'&lt;/span&gt;
&lt;span class="go"&gt;app mounts logs at /var/log
log-stream mounts logs at /var/log
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Nothing else moved
&lt;/h2&gt;

&lt;p&gt;Last check, and it is the one part three grades. Read the original container back: same name, same image, same command it started with. The only thing that changed about it is the volume mount it needed to share the file, which the task explicitly allows. And the volume itself is a plain &lt;code&gt;emptyDir&lt;/code&gt;, created with the pod and thrown away with it, which is exactly right for a log relay. No PersistentVolumeClaim, no storage class, nothing to provision.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; legacy get deployment report-engine &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.template.spec.containers[0].name}{" / "}{.spec.template.spec.containers[0].image}{"\n"}{.spec.template.spec.containers[0].command[2]}'&lt;/span&gt;
&lt;span class="go"&gt;app / busybox:1.36
&lt;/span&gt;&lt;span class="gp"&gt;mkdir -p /var/log;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%F %T'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; report-engine: batch complete"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /var/log/report-engine.log&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;5&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; legacy get deployment report-engine &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.template.spec.volumes}'&lt;/span&gt;
&lt;span class="go"&gt;[{"emptyDir":{},"name":"logs"}]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exam tips
&lt;/h2&gt;

&lt;p&gt;Things to carry into the exam. The volume mount goes in every container that needs the file, and there are two of them; forgetting the app container is the classic mistake, because then the sidecar tails a file that will never appear. The volume itself lives at the pod spec level, beside containers, not inside one. An empty &lt;code&gt;emptyDir&lt;/code&gt;, written as `&lt;code&gt;emptyDir&lt;/code&gt;: {}&lt;code&gt;, is all you need; search the docs page for&lt;/code&gt;emptyDir&lt;code&gt;and copy the shape. Mount at the directory, not the file. Once the pod has two containers,&lt;/code&gt;kubectl logs&lt;code&gt;and kubectl exec both need&lt;/code&gt;-c&lt;code&gt;to say which one. And one modern footnote: Kubernetes does have native sidecars now, an init container with a&lt;/code&gt;restartPolicy: Always`, but a question that says co-located container in the same pod means a plain second entry under containers, so do the simple thing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;volumeMounts go in BOTH containers; the app one is the one people forget&lt;/li&gt;
&lt;li&gt;The volume itself sits at pod-spec level, beside 'containers', not inside it&lt;/li&gt;
&lt;li&gt;'emptyDir: {}' is enough; it is born and dies with the pod&lt;/li&gt;
&lt;li&gt;Mount the DIRECTORY that holds the log file, not the file&lt;/li&gt;
&lt;li&gt;Two containers means kubectl logs / exec need -c &lt;/li&gt;
&lt;li&gt;Native sidecars (initContainer + restartPolicy: Always) exist, but this asks for a plain container&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;kubectl logs reads stdout only, so a file-logging app is invisible&lt;/li&gt;
&lt;li&gt;Added 'log-stream' (busybox) tailing the file to stdout, in the SAME pod&lt;/li&gt;
&lt;li&gt;emptyDir 'logs' mounted at /var/log in both containers: one file, two processes&lt;/li&gt;
&lt;li&gt;READY 2/2, logs streaming, original container untouched; subscribe + dev.to writeup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce this yourself
&lt;/h2&gt;

&lt;p&gt;The entire scenario is scripted on a throwaway &lt;code&gt;kind&lt;/code&gt; cluster: &lt;a href="https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios" rel="noopener noreferrer"&gt;https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;bash&lt;br&gt;
git clone &lt;a href="https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios.git" rel="noopener noreferrer"&gt;https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios.git&lt;/a&gt;&lt;br&gt;
cd TCS_CKA_2026_Exam_Scenarios/learning/scenarios/scenario14-sidecar-logging&lt;br&gt;
./setup.sh        # creates the cluster AND arms the scenario&lt;/p&gt;

&lt;h1&gt;
  
  
  solve it by hand, or:
&lt;/h1&gt;

&lt;p&gt;./solution.sh     # apply the answer key and verify&lt;br&gt;
&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;




&lt;p&gt;If this helped, &lt;strong&gt;subscribe to The Cyber SideKick&lt;/strong&gt; on YouTube for more CKA drills, and grab the newsletter at &lt;a href="https://thecybersidekick.beehiiv.com" rel="noopener noreferrer"&gt;https://thecybersidekick.beehiiv.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cka</category>
      <category>logging</category>
      <category>devops</category>
    </item>
    <item>
      <title>Add a WaitForFirstConsumer StorageClass and hand it the default (CKA)</title>
      <dc:creator>The Cyber Sidekick</dc:creator>
      <pubDate>Thu, 06 Aug 2026 16:41:33 +0000</pubDate>
      <link>https://dev.to/thecybersidekick/add-a-waitforfirstconsumer-storageclass-and-hand-it-the-default-cka-56gc</link>
      <guid>https://dev.to/thecybersidekick/add-a-waitforfirstconsumer-storageclass-and-hand-it-the-default-cka-56gc</guid>
      <description>&lt;h2&gt;
  
  
  Add a WaitForFirstConsumer StorageClass and hand it the default (CKA)
&lt;/h2&gt;

&lt;p&gt;There is a version of this CKA task that takes ninety seconds, and a version that quietly breaks the cluster you are being graded on. Today: add a StorageClass, set its binding mode so volumes wait for the pod that needs them, and make it the cluster default. The interesting part is that last word. Default is an annotation, only one class is allowed to carry it, and you are told not to touch anything that already exists. Let's run it on a live cluster.&lt;/p&gt;

&lt;p&gt;🎥 &lt;strong&gt;Watch the video:&lt;/strong&gt; &lt;a href="https://www.youtube.com/watch?v=5Pbq42ykbn4" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=5Pbq42ykbn4&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a CKA Storage walkthrough. Every command below is real output from a live cluster, and you can reproduce the whole thing yourself (scripts at the end).&lt;/p&gt;

&lt;h2&gt;
  
  
  The scenario
&lt;/h2&gt;

&lt;p&gt;Here is the setup. The cluster already runs a dynamic provisioner, and there are already StorageClasses using it, one of which is marked as the default. Three things are asked of you. Add a class called fast-local that reuses that same provisioner and binds with &lt;code&gt;WaitForFirstConsumer&lt;/code&gt;. Make fast-local the cluster's default, which means exactly one class ends up carrying the marker. And do all of that without editing any existing Deployment or PersistentVolumeClaim, because there is a workload here already using storage and points come off if you disturb it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1. New StorageClass 'fast-local': reuse the EXISTING provisioner, WaitForFirstConsumer&lt;/li&gt;
&lt;li&gt;2. Make it the cluster default (and only one class may be the default)&lt;/li&gt;
&lt;li&gt;3. Do NOT modify any existing Deployment or PVC&lt;/li&gt;
&lt;li&gt;A running workload in ns 'ledger' is already bound through the old default&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How binding modes and the default class work
&lt;/h2&gt;

&lt;p&gt;Two mechanics to have straight before typing. First, binding mode. Immediate means the provisioner creates a volume as soon as the claim appears, before anyone knows which node the pod will land on. &lt;code&gt;WaitForFirstConsumer&lt;/code&gt; means the claim sits Pending on purpose until a pod that mounts it gets scheduled, and only then is the volume created, on that pod's node. For node-local storage that is the correct choice, and it is what this task asks for. Second, the default. It is not a field in the spec, it is an annotation, &lt;code&gt;storageclass.kubernetes.io/is-default-class&lt;/code&gt;, set to the string true. It only affects claims that name no class at all, and a cluster with two of them is misconfigured. So the order matters: take it off the old class first, then give it to the new one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are handed
&lt;/h2&gt;

&lt;p&gt;Read the cluster before changing it. There are two classes already. Both use the same provisioner, &lt;code&gt;rancher.io/local-path&lt;/code&gt;, which is the one the task tells you to reuse, so there is nothing to install. Standard carries the default marker, and it binds with &lt;code&gt;WaitForFirstConsumer&lt;/code&gt;; bulk-hdd binds Immediately and is not the default. Note which one is default, because that is the class you will have to take it away from. Then look at what you must not break: the ledger namespace has a claim that is already Bound through standard, with a Deployment running on top of it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get storageclass
&lt;span class="go"&gt;NAME                 PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
bulk-hdd             rancher.io/local-path   Delete          Immediate              false                  4m24s
standard (default)   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  4m24s

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get pvc,pods &lt;span class="nt"&gt;-n&lt;/span&gt; ledger
&lt;span class="go"&gt;NAME                                STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
&lt;/span&gt;&lt;span class="gp"&gt;persistentvolumeclaim/ledger-data   Bound    pvc-8bf9e803-20a7-4015-aa77-5d3879dcf33d   128Mi      RWO            standard       &amp;lt;unset&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;                 &lt;/span&gt;4s
&lt;span class="go"&gt;
NAME                              READY   STATUS    RESTARTS   AGE
pod/ledger-api-6bff9497c6-jw27j   1/1     Running   0          4s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create the class
&lt;/h2&gt;

&lt;p&gt;Now the class itself. In the exam, open the StorageClass page in the Kubernetes docs, copy the example, and delete everything the task did not ask for: no parameters, no mount options, no reclaim policy. What is left is four lines that matter. The name, fast-local. The provisioner, copied exactly from the class that already exists, because reusing it is the whole point. And &lt;code&gt;volumeBindingMode&lt;/code&gt; set to &lt;code&gt;WaitForFirstConsumer&lt;/code&gt;. Apply it, and the class is created. Notice there is no default annotation on it yet; that is deliberate, and it is the next step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;fast-local.yaml
&lt;span class="go"&gt;apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-local
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; fast-local.yaml
&lt;span class="go"&gt;storageclass.storage.k8s.io/fast-local created
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Unset the old default
&lt;/h2&gt;

&lt;p&gt;Before handing the title over, take it away from the current holder. A one line patch sets the annotation on standard to the string false. kubectl edit gets you to the same place if you prefer an editor, and in the exam either is fine. Look at the list now: nothing carries the default marker. That is a perfectly valid state, and it is much safer than the alternative, because if two classes claim to be the default, Kubernetes cannot choose between them and an unqualified claim will not get a class at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl patch storageclass standard &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'&lt;/span&gt;
&lt;span class="go"&gt;storageclass.storage.k8s.io/standard patched

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get storageclass
&lt;span class="go"&gt;NAME         PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
bulk-hdd     rancher.io/local-path   Delete          Immediate              false                  4m25s
fast-local   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  0s
standard     rancher.io/local-path   Delete          WaitForFirstConsumer   false                  4m25s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Hand over the default
&lt;/h2&gt;

&lt;p&gt;Same patch, other direction: set the annotation to true on fast-local. And now read the list the way a grader would. Exactly one class shows the default marker, it is ours, the provisioner matches the one that was already in the cluster, and the binding mode says &lt;code&gt;WaitForFirstConsumer&lt;/code&gt;. Every requirement of the task is visible in that single line of output. If you had baked the annotation into the YAML when you created the class, you would still have had to unset standard, so the order of operations is the thing to remember, not which tool you used.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl patch storageclass fast-local &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'&lt;/span&gt;
&lt;span class="go"&gt;storageclass.storage.k8s.io/fast-local patched

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get storageclass
&lt;span class="go"&gt;NAME                   PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
bulk-hdd               rancher.io/local-path   Delete          Immediate              false                  4m25s
fast-local (default)   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  0s
standard               rancher.io/local-path   Delete          WaitForFirstConsumer   false                  4m25s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pending on purpose
&lt;/h2&gt;

&lt;p&gt;Let's prove both halves for real, without touching anything we were told to leave alone. Here is a throwaway claim in its own namespace with no &lt;code&gt;storageClassName&lt;/code&gt; at all. Create it, and look at the class column: fast-local. Nothing told it that; it picked up the default, which is exactly what the annotation is for. And the status is Pending. That is not a failure, and this is the part people panic about in the exam. Describe the claim and the event says it plainly: waiting for first consumer to be created before binding. The volume does not exist yet because no pod has asked for it yet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; scratch-claim.yaml
&lt;span class="go"&gt;persistentvolumeclaim/scratch-claim created

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; scratch get pvc
&lt;span class="go"&gt;NAME            STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
&lt;/span&gt;&lt;span class="gp"&gt;scratch-claim   Pending                                      fast-local     &amp;lt;unset&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;                 &lt;/span&gt;4s
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; scratch describe pvc scratch-claim | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A3&lt;/span&gt; &lt;span class="s1"&gt;'Events:'&lt;/span&gt;
&lt;span class="go"&gt;Events:
  Type    Reason                Age   From                         Message
  ----    ------                ----  ----                         -------
  Normal  WaitForFirstConsumer  5s    persistentvolume-controller  waiting for first consumer to be created before binding
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The first consumer binds it
&lt;/h2&gt;

&lt;p&gt;Now create the first consumer, a single pod that mounts that claim. The scheduler places the pod, the provisioner creates the volume on that node, and the claim goes straight to Bound with a real volume behind it. That is the entire point of &lt;code&gt;WaitForFirstConsumer&lt;/code&gt;: the storage follows the pod instead of the pod being dragged to wherever the storage happened to land. And the last check is the one the task actually grades you on for part three. The ledger namespace is exactly as we found it: still Bound, still through standard, its Deployment still running. We changed which class is default without editing a single existing object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; scratch-writer.yaml
&lt;span class="go"&gt;pod/scratch-writer created

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; scratch get pvc,pods
&lt;span class="go"&gt;NAME                                  STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
&lt;/span&gt;&lt;span class="gp"&gt;persistentvolumeclaim/scratch-claim   Bound    pvc-2bcf522a-4a2e-4eba-9e5d-2887b123165b   64Mi       RWO            fast-local     &amp;lt;unset&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;                 &lt;/span&gt;10s
&lt;span class="go"&gt;
NAME                 READY   STATUS    RESTARTS   AGE
pod/scratch-writer   1/1     Running   0          5s

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get pvc,pods &lt;span class="nt"&gt;-n&lt;/span&gt; ledger
&lt;span class="go"&gt;NAME                                STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
&lt;/span&gt;&lt;span class="gp"&gt;persistentvolumeclaim/ledger-data   Bound    pvc-8bf9e803-20a7-4015-aa77-5d3879dcf33d   128Mi      RWO            standard       &amp;lt;unset&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;                 &lt;/span&gt;15s
&lt;span class="go"&gt;
NAME                              READY   STATUS    RESTARTS   AGE
pod/ledger-api-6bff9497c6-jw27j   1/1     Running   0          15s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exam tips
&lt;/h2&gt;

&lt;p&gt;Things worth carrying into the exam. Copy the StorageClass example from the docs and delete every field the task did not mention, because extra fields are extra chances to be wrong. Reuse the provisioner string exactly as it appears in the existing class; do not type it from memory. Remember the annotation value is a quoted string, true, not a boolean. Always unset the old default before setting the new one, and confirm with &lt;code&gt;kubectl get storageclass&lt;/code&gt; that exactly one class shows the marker. Do not try to move an existing claim onto your new class: a bound PVC's storage class cannot be changed anyway, and here you would be modifying an object the task explicitly protects. And if a new claim sits Pending on a &lt;code&gt;WaitForFirstConsumer&lt;/code&gt; class, that is the design, not a bug.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Strip the docs example to what was asked: name, provisioner, volumeBindingMode&lt;/li&gt;
&lt;li&gt;Copy the provisioner string from the existing class, do not retype it&lt;/li&gt;
&lt;li&gt;The annotation value is the STRING "true" / "false"&lt;/li&gt;
&lt;li&gt;Unset the old default FIRST, then confirm exactly one (default) marker&lt;/li&gt;
&lt;li&gt;Never re-point an existing PVC: immutable once bound, and off limits here&lt;/li&gt;
&lt;li&gt;Pending on a WaitForFirstConsumer class is expected, not a failure&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;fast-local created: existing provisioner + volumeBindingMode WaitForFirstConsumer&lt;/li&gt;
&lt;li&gt;Default annotation moved off 'standard', onto 'fast-local' (exactly one marker)&lt;/li&gt;
&lt;li&gt;Unqualified claim landed on fast-local, Pending -&amp;gt; Bound on first consumer&lt;/li&gt;
&lt;li&gt;ns 'ledger' untouched, still Bound through 'standard'; subscribe + dev.to writeup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce this yourself
&lt;/h2&gt;

&lt;p&gt;The entire scenario is scripted on a throwaway &lt;code&gt;kind&lt;/code&gt; cluster: &lt;a href="https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios" rel="noopener noreferrer"&gt;https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios.git
&lt;span class="nb"&gt;cd &lt;/span&gt;TCS_CKA_2026_Exam_Scenarios/learning/scenarios/scenario13-storageclass-default
./setup.sh        &lt;span class="c"&gt;# creates the cluster AND arms the scenario&lt;/span&gt;
&lt;span class="c"&gt;# solve it by hand, or:&lt;/span&gt;
./solution.sh     &lt;span class="c"&gt;# apply the answer key and verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;If this helped, &lt;strong&gt;subscribe to The Cyber SideKick&lt;/strong&gt; on YouTube for more CKA drills, and grab the newsletter at &lt;a href="https://thecybersidekick.beehiiv.com" rel="noopener noreferrer"&gt;https://thecybersidekick.beehiiv.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cka</category>
      <category>storage</category>
      <category>devops</category>
    </item>
    <item>
      <title>Name a container port, then expose it with a NodePort Service (CKA)</title>
      <dc:creator>The Cyber Sidekick</dc:creator>
      <pubDate>Fri, 31 Jul 2026 16:39:54 +0000</pubDate>
      <link>https://dev.to/thecybersidekick/name-a-container-port-then-expose-it-with-a-nodeport-service-cka-44do</link>
      <guid>https://dev.to/thecybersidekick/name-a-container-port-then-expose-it-with-a-nodeport-service-cka-44do</guid>
      <description>&lt;h2&gt;
  
  
  Name a container port, then expose it with a NodePort Service (CKA)
&lt;/h2&gt;

&lt;p&gt;This is a two minute question. In the exam you should recognize it, answer it, and move on to something hard. Today's CKA task: give an existing webserver container a named port, then expose it with a Service that is reachable both inside the cluster and on the nodes themselves. It is three deliverables, and one of them is a single command. Let's run it on a live cluster.&lt;/p&gt;

&lt;p&gt;🎥 &lt;strong&gt;Watch the video:&lt;/strong&gt; &lt;a href="https://www.youtube.com/watch?v=K8BCAu9Fnvw" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=K8BCAu9Fnvw&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a CKA Services &amp;amp; Networking walkthrough. Every command below is real output from a live cluster, and you can reproduce the whole thing yourself (scripts at the end).&lt;/p&gt;

&lt;h2&gt;
  
  
  The scenario
&lt;/h2&gt;

&lt;p&gt;Here is the setup. A Deployment called catalog is already running in the storefront namespace, and its container, webserver, declares no ports at all. Three things are asked of you. Give that container a port entry: name it web, port 80, protocol TCP. Put a Service called catalog-svc in front of the Deployment, and have it target that named port rather than a raw number. And make the Service answer on each node's own address as well as inside the cluster, which is a decision about the Service type. One edit, one Service, and the right type.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Container 'webserver' in Deployment 'catalog' (ns 'storefront') declares no ports&lt;/li&gt;
&lt;li&gt;1. Give it a port entry: name 'web', port 80, protocol TCP&lt;/li&gt;
&lt;li&gt;2. Service 'catalog-svc' in front of it, targeting that NAMED port&lt;/li&gt;
&lt;li&gt;3. It must answer on each node's address too, so pick the right Service type&lt;/li&gt;
&lt;li&gt;Budget: two to three minutes, then move on&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How the ports chain together
&lt;/h2&gt;

&lt;p&gt;Before we touch anything, understand what a &lt;code&gt;containerPort&lt;/code&gt; is, because that is what this task is really testing. Declaring a &lt;code&gt;containerPort&lt;/code&gt; does not open a port. nginx already listens on 80 whether or not the pod spec mentions it, and nothing blocks traffic to an undeclared port. What the entry gives you is documentation and, more importantly, a NAME. A Service's &lt;code&gt;targetPort&lt;/code&gt; can then say web instead of 80, so the backend port can move without editing the Service. That is the chain: the Service has its own port, its &lt;code&gt;targetPort&lt;/code&gt; points at the container's named port, and if the Service type is NodePort, the API server also allocates a port between 30000 and 32767 that is opened on every node in the cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are handed
&lt;/h2&gt;

&lt;p&gt;Start by reading what you were handed. In the storefront namespace there is one Deployment, catalog, with three pods Running, and no Service at all. Now read the pod template. The container is named webserver, not nginx, which is a good habit to build: read the container name out of the template instead of assuming it matches the image. And there is no ports block anywhere in it. The pods serve traffic fine right now, because the container listens on 80 regardless. What is missing is the declaration, and specifically the name the Service will need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get deployments,pods,svc &lt;span class="nt"&gt;-n&lt;/span&gt; storefront
&lt;span class="go"&gt;NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/catalog   3/3     3            3           2s

NAME                          READY   STATUS    RESTARTS   AGE
pod/catalog-f9fb568c4-c4dqm   1/1     Running   0          2s
pod/catalog-f9fb568c4-hfksk   1/1     Running   0          2s
pod/catalog-f9fb568c4-skpjt   1/1     Running   0          2s

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; storefront get deployment catalog &lt;span class="nt"&gt;-o&lt;/span&gt; yaml | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A5&lt;/span&gt; &lt;span class="s1"&gt;'containers:'&lt;/span&gt;
&lt;span class="go"&gt;      containers:
      - image: nginx:1.27-alpine
        imagePullPolicy: IfNotPresent
        name: webserver
        resources: {}
        terminationMessagePath: /dev/termination-log
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Add the named port
&lt;/h2&gt;

&lt;p&gt;Part one. Edit the Deployment in place with &lt;code&gt;kubectl edit deployment catalog&lt;/code&gt; in the storefront namespace. Find the container named webserver and add a ports block underneath it: name web, &lt;code&gt;containerPort&lt;/code&gt; 80, protocol TCP. Copy the shape from the Kubernetes docs if you want, the Service page has it, but watch the indentation. Ports belongs to the container, at the same level as name and image, not to the pod spec. Write and quit, and kubectl pushes the change straight back to the API server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; storefront edit deployment catalog
&lt;span class="go"&gt;      containers:
      - image: nginx:1.27-alpine
        imagePullPolicy: IfNotPresent
        name: webserver
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;after editing&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;      containers:
      - image: nginx:1.27-alpine
        imagePullPolicy: IfNotPresent
        name: webserver
        ports:
        - containerPort: 80
          name: web
          protocol: TCP
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The rollout
&lt;/h2&gt;

&lt;p&gt;Changing the pod template triggers a rolling update, so the Deployment replaces all three pods. Wait for rollout status to say successfully rolled out, because a half-finished rollout is how you end up verifying against a pod that does not have your change. Then read the field back. The container now carries &lt;code&gt;containerPort&lt;/code&gt; 80, name web, protocol TCP. Part one is graded on exactly that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; storefront rollout status deployment/catalog
&lt;span class="go"&gt;Waiting for deployment "catalog" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "catalog" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "catalog" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "catalog" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "catalog" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "catalog" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "catalog" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "catalog" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "catalog" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "catalog" rollout to finish: 1 old replicas are pending termination...
deployment "catalog" successfully rolled out

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; storefront get deployment catalog &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.template.spec.containers[0].ports}'&lt;/span&gt;
&lt;span class="go"&gt;[{"containerPort":80,"name":"web","protocol":"TCP"}]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Expose it as NodePort
&lt;/h2&gt;

&lt;p&gt;Parts two and three are one command. &lt;code&gt;kubectl expose&lt;/code&gt; deployment catalog, name it catalog-svc, port 80, target port web, protocol TCP, type NodePort. Every part of the task maps onto a flag. Target port web is the payoff for part one, since we can now reference the port by name. And expose copies the selector from the Deployment for you, which matters more than it sounds: a hand-written Service with a selector that does not match the pod labels is the classic way to end up with a Service that has no endpoints and answers nothing. Look at the result: type NodePort, port 80, and the API server allocated a node port from the 30000 range.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; storefront expose deployment catalog &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;catalog-svc &lt;span class="nt"&gt;--port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;80 &lt;span class="nt"&gt;--target-port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;web &lt;span class="nt"&gt;--protocol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;TCP &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;NodePort
&lt;span class="go"&gt;service/catalog-svc exposed

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get svc &lt;span class="nt"&gt;-n&lt;/span&gt; storefront
&lt;span class="go"&gt;NAME          TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
&lt;/span&gt;&lt;span class="gp"&gt;catalog-svc   NodePort   10.96.83.91   &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;80:31307/TCP   0s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  TargetPort + endpoints
&lt;/h2&gt;

&lt;p&gt;Describe the Service and check the three lines a grader would check. TargetPort reads web, not a number, which proves the Service is following the name we added. NodePort shows the allocated port, and it is open on every node in the cluster, not just the one the pods happen to run on. And Endpoints lists all three pod IPs on port 80, which means the selector matched. An empty Endpoints line is the failure mode to look for: the Service exists, it answers nothing, and the task is marked wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; storefront describe svc catalog-svc
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;Type:                     NodePort
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.96.83.91
IPs:                      10.96.83.91
&lt;/span&gt;&lt;span class="gp"&gt;Port:                     &amp;lt;unset&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;80/TCP
&lt;span class="go"&gt;TargetPort:               web/TCP
&lt;/span&gt;&lt;span class="gp"&gt;NodePort:                 &amp;lt;unset&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;31307/TCP
&lt;span class="go"&gt;Endpoints:                10.244.0.23:80,10.244.0.21:80,10.244.0.22:80
Session Affinity:         None
External Traffic Policy:  Cluster
Internal Traffic Policy:  Cluster
&lt;/span&gt;&lt;span class="gp"&gt;Events:                   &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Curl both paths
&lt;/h2&gt;

&lt;p&gt;Now prove it carries traffic, both ways. First through the ClusterIP, by name: &lt;code&gt;catalog-svc.storefront&lt;/code&gt; resolves through cluster DNS and nginx answers. Then through the node port, using the node's own IP and the allocated port, which is the part deliverable three was really about. Same welcome page, a completely different path through the stack. One note on the lab: these curls run from a small pod inside the cluster, because on kind the node addresses are not routable from the laptop. On a real exam node you would curl the node IP and port directly from your terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; probe &lt;span class="nb"&gt;exec &lt;/span&gt;deploy/tester &lt;span class="nt"&gt;--&lt;/span&gt; sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"curl -s --max-time 10 http://catalog-svc.storefront | grep -i '&amp;lt;title&amp;gt;'"&lt;/span&gt;
&lt;span class="gp"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Welcome to nginx!&amp;lt;/title&amp;gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; probe &lt;span class="nb"&gt;exec &lt;/span&gt;deploy/tester &lt;span class="nt"&gt;--&lt;/span&gt; sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"curl -s --max-time 10 http://172.18.0.17:31307 | grep -i '&amp;lt;title&amp;gt;'"&lt;/span&gt;
&lt;span class="gp"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Welcome to nginx!&amp;lt;/title&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exam tips
&lt;/h2&gt;

&lt;p&gt;A few things to carry into the exam. Ports goes inside the container, not the pod spec, and the wrong indentation is the number one way to lose this one. The port NAME matters: if the task names the port web, then &lt;code&gt;targetPort&lt;/code&gt; has to say web. Use &lt;code&gt;kubectl expose&lt;/code&gt; instead of writing YAML, it is faster and it gets the selector right by construction. If you do write YAML, the selector must match the pod labels, and remember it is the pod template labels, not the Deployment's own name. NodePort does not replace the ClusterIP, it adds to it, so nothing else has to change. And do not set a nodePort value yourself unless a specific one is asked for, since letting the API server allocate is both faster and less likely to collide.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;'ports' belongs to the CONTAINER (same level as name/image), not the pod spec&lt;/li&gt;
&lt;li&gt;targetPort: web references the NAME you added; that is why part one exists&lt;/li&gt;
&lt;li&gt;kubectl expose --port/--target-port/--protocol/--type does parts 2 and 3 in one line&lt;/li&gt;
&lt;li&gt;Hand-written YAML? the selector must match the POD TEMPLATE labels or Endpoints is empty&lt;/li&gt;
&lt;li&gt;NodePort is a superset of ClusterIP; let the API server allocate the 30000-32767 port&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Container 'webserver' now declares: name web, containerPort 80, protocol TCP&lt;/li&gt;
&lt;li&gt;kubectl expose ... --target-port=web --type=NodePort created catalog-svc&lt;/li&gt;
&lt;li&gt;describe svc: TargetPort web/TCP, NodePort allocated, 3 endpoints&lt;/li&gt;
&lt;li&gt;Verified through the ClusterIP and through :; subscribe + dev.to writeup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce this yourself
&lt;/h2&gt;

&lt;p&gt;The entire scenario is scripted on a throwaway &lt;code&gt;kind&lt;/code&gt; cluster: &lt;a href="https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios" rel="noopener noreferrer"&gt;https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios.git
&lt;span class="nb"&gt;cd &lt;/span&gt;TCS_CKA_2026_Exam_Scenarios/learning/scenarios/scenario12-nodeport-service-expose
./setup.sh        &lt;span class="c"&gt;# creates the cluster AND arms the scenario&lt;/span&gt;
&lt;span class="c"&gt;# solve it by hand, or:&lt;/span&gt;
./solution.sh     &lt;span class="c"&gt;# apply the answer key and verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;If this helped, &lt;strong&gt;subscribe to The Cyber SideKick&lt;/strong&gt; on YouTube for more CKA drills, and grab the newsletter at &lt;a href="https://thecybersidekick.beehiiv.com" rel="noopener noreferrer"&gt;https://thecybersidekick.beehiiv.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cka</category>
      <category>networking</category>
      <category>devops</category>
    </item>
    <item>
      <title>Create a PriorityClass, patch the Deployment, let the scheduler evict the neighbors (CKA)</title>
      <dc:creator>The Cyber Sidekick</dc:creator>
      <pubDate>Thu, 30 Jul 2026 17:56:13 +0000</pubDate>
      <link>https://dev.to/thecybersidekick/create-a-priorityclass-patch-the-deployment-let-the-scheduler-evict-the-neighbors-cka-2oij</link>
      <guid>https://dev.to/thecybersidekick/create-a-priorityclass-patch-the-deployment-let-the-scheduler-evict-the-neighbors-cka-2oij</guid>
      <description>&lt;h2&gt;
  
  
  Patch a Deployment with a new PriorityClass (and watch preemption evict the neighbors)
&lt;/h2&gt;

&lt;p&gt;Kubernetes is about to evict a perfectly healthy, running pod, and that is the correct answer to this exam question. Today's CKA task: create a new PriorityClass, patch a Deployment to use it, and roll it out on a node that has no room left. The question even warns you that pods from other deployments will be evicted. Most people read that sentence twice. By the end of this video you will know exactly why it happens, and why you should leave the victims alone. Let's run it.&lt;/p&gt;

&lt;p&gt;This is a CKA Workloads &amp;amp; Scheduling walkthrough. Every command below is real output from a live cluster, and you can reproduce the whole thing yourself (scripts at the end).&lt;/p&gt;

&lt;h2&gt;
  
  
  The scenario
&lt;/h2&gt;

&lt;p&gt;Here is the question. Create a new PriorityClass named high-priority for user workloads, with a value that is one less than the highest existing user-defined priority class. Then patch the busybox-logger Deployment, running in the priority namespace, to use that new class, and ensure the Deployment rolls out successfully. Two warnings come with it. First: it is expected that pods from other Deployments in the priority namespace get evicted. Second: do not modify those other Deployments, or you lose points. So the deliverables are one PriorityClass, one patch, one healthy rollout, and the discipline to not touch anything else.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create PriorityClass 'high-priority': value = highest USER-DEFINED class minus one&lt;/li&gt;
&lt;li&gt;Patch Deployment 'busybox-logger' in ns 'priority' to use it, then roll out&lt;/li&gt;
&lt;li&gt;EXPECTED: pods from other Deployments in the namespace get evicted&lt;/li&gt;
&lt;li&gt;Do NOT modify the other Deployments (you lose points if you do)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How priority and preemption work
&lt;/h2&gt;

&lt;p&gt;Two mechanisms drive this question. First, priority. A PriorityClass is a tiny cluster-scoped object: a name and an integer. When a pod's spec names it, the pod is admitted with that integer as its priority; bigger means more important, and the default is zero. Second, preemption. When the scheduler cannot find a node with room for a pending pod, it looks for nodes where evicting pods of LOWER priority would make room, evicts them, and schedules the pending pod in the freed space. Now connect that to the task: patching the Deployment's pod template triggers a rolling update, the replacement pods carry the new high priority, and on a full node the scheduler has to preempt someone to place them. That is why the question can promise evictions in advance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The existing classes
&lt;/h2&gt;

&lt;p&gt;Step one: what already exists? kubectl get priorityclass. Read this table the way the question wants you to. The two classes starting with system, node critical and cluster critical, sit around two billion; they belong to Kubernetes itself and you never touch them or count them. The USER-DEFINED classes are the other two: low-priority at minus ten, yes, negative values are legal, and user-high-priority at one million. The highest user-defined value is therefore 1000000, and one less than that is 999999. That single subtraction is the entire trick of part one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get priorityclass
&lt;span class="go"&gt;NAME                      VALUE        GLOBAL-DEFAULT   AGE     PREEMPTIONPOLICY
low-priority              -10          false            6m31s   PreemptLowerPriority
system-cluster-critical   2000000000   false            6m37s   PreemptLowerPriority
system-node-critical      2000001000   false            6m37s   PreemptLowerPriority
user-high-priority        1000000      false            6m31s   PreemptLowerPriority
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create high-priority
&lt;/h2&gt;

&lt;p&gt;You do not need YAML for this. kubectl create priorityclass high-priority, value 999999, and a description, because the question says it is for user workloads. One line, done. If you prefer the declarative route, the docs page for Pod Priority and Preemption has a copy-paste example, but in an exam the imperative command is thirty seconds faster. Confirm it: high-priority now exists with exactly the value we derived, one less than the highest user-defined class.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl create priorityclass high-priority &lt;span class="nt"&gt;--value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;999999 &lt;span class="nt"&gt;--description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"high priority for user workloads"&lt;/span&gt;
&lt;span class="go"&gt;priorityclass.scheduling.k8s.io/high-priority created

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get priorityclass high-priority
&lt;span class="go"&gt;NAME            VALUE    GLOBAL-DEFAULT   AGE   PREEMPTIONPOLICY
high-priority   999999   false            0s    PreemptLowerPriority
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The full node
&lt;/h2&gt;

&lt;p&gt;Before the patch, look at the namespace we are about to disturb. Three Deployments: busybox-logger, the one we are told to patch, plus queue-worker and metrics-agent, the other deployments the question warns about. Everything is Running. Now print each pod's actual priority. The busybox-logger pods run at zero, the default, because their template names no priority class. The neighbors run at minus ten, from the low-priority class. Keep this picture in mind: the node is nearly out of CPU, and every replacement pod we are about to create outranks everything else here by roughly a million.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get deployments &lt;span class="nt"&gt;-n&lt;/span&gt; priority
&lt;span class="go"&gt;NAME             READY   UP-TO-DATE   AVAILABLE   AGE
busybox-logger   2/2     2            2           3s
metrics-agent    1/1     1            1           2s
queue-worker     2/2     2            2           2s

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; priority &lt;span class="nt"&gt;-o&lt;/span&gt; custom-columns&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'NAME:.metadata.name,STATUS:.status.phase,PRIORITY:.spec.priority'&lt;/span&gt;
&lt;span class="go"&gt;NAME                              STATUS    PRIORITY
busybox-logger-594f746ddf-dvfnj   Running   0
busybox-logger-594f746ddf-mnqkv   Running   0
metrics-agent-8c89d9895-g2ktn     Running   -10
queue-worker-76df598f64-vbrrj     Running   -10
queue-worker-76df598f64-w4mtl     Running   -10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Patch the deployment
&lt;/h2&gt;

&lt;p&gt;Part two: patch the Deployment. The field lives in the POD TEMPLATE: &lt;code&gt;spec.template.spec.&lt;/code&gt;priorityClassName``, set to high-priority. kubectl patch with a small JSON snippet does it in one command; kubectl edit gets you to the same place if you prefer an editor, the docs show the exact field under the Pod spec. The moment this lands, the Deployment controller starts a rolling update: it must bring up new pods that carry priority 999999 on a node that has no space for them. Watch what the scheduler does about that.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`console&lt;br&gt;
$ kubectl -n priority patch deployment busybox-logger -p '{"spec":{"template":{"spec":{"priorityClassName":"high-priority"}}}}'&lt;br&gt;
deployment.apps/busybox-logger patched&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The eviction
&lt;/h2&gt;

&lt;p&gt;Here is the moment the question promised. The fresh busybox-logger pod went Pending, the scheduler compared priorities, and a low-priority neighbor is being terminated to make room; its replacement sits Pending because, at priority minus ten, it cannot evict anyone. Kubernetes writes this decision down: the events show Preempted, naming the victim, evicted by the scheduler on behalf of the higher-priority pod. This is not a failure and there is nothing to fix. The question told you it was expected; your only job is to let it happen.&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;console&lt;br&gt;
$ kubectl get pods -n priority&lt;br&gt;
NAME                              READY   STATUS        RESTARTS   AGE&lt;br&gt;
busybox-logger-594f746ddf-dvfnj   1/1     Running       0          4s&lt;br&gt;
busybox-logger-594f746ddf-mnqkv   1/1     Running       0          4s&lt;br&gt;
busybox-logger-645b98678b-kfwmw   0/1     Pending       0          0s&lt;br&gt;
metrics-agent-8c89d9895-68282     0/1     Pending       0          0s&lt;br&gt;
metrics-agent-8c89d9895-g2ktn     1/1     Terminating   0          3s&lt;br&gt;
queue-worker-76df598f64-vbrrj     1/1     Running       0          3s&lt;br&gt;
queue-worker-76df598f64-w4mtl     1/1     Running       0          3s&lt;/p&gt;

&lt;p&gt;$ kubectl get events -n priority | grep -i preempt&lt;br&gt;
0s          Warning   FailedScheduling    pod/metrics-agent-8c89d9895-68282      0/1 nodes are available: 1 Insufficient cpu. no new claims to deallocate, preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod.&lt;br&gt;
0s          Normal    Preempted           pod/metrics-agent-8c89d9895-g2ktn      Preempted by pod 6a344267-de23-4005-b466-00fe7699cf20 on node cka-scenario11-control-plane&lt;br&gt;
&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollout + verify
&lt;/h2&gt;

&lt;p&gt;Part three: prove the rollout finished. rollout status reports the deployment successfully rolled out. Now verify like a grader: print the priorities again. Both busybox-logger pods now show priority 999999 with class high-priority, and the neighbors still hold their original spec, untouched. Notice the namespace healed itself: once the rolling update retired the old zero-priority pods, their CPU freed up and the evicted neighbor's replacement scheduled on its own. In the exam it may just as well stay Pending, and that is fine too; the question graded the PriorityClass, the patch, and the rollout, not the neighbors' comfort.&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;console&lt;br&gt;
$ kubectl -n priority rollout status deployment/busybox-logger&lt;br&gt;
Waiting for deployment "busybox-logger" rollout to finish: 1 out of 2 new replicas have been updated...&lt;br&gt;
Waiting for deployment "busybox-logger" rollout to finish: 1 out of 2 new replicas have been updated...&lt;br&gt;
Waiting for deployment "busybox-logger" rollout to finish: 1 out of 2 new replicas have been updated...&lt;br&gt;
Waiting for deployment "busybox-logger" rollout to finish: 1 old replicas are pending termination...&lt;br&gt;
Waiting for deployment "busybox-logger" rollout to finish: 1 old replicas are pending termination...&lt;br&gt;
deployment "busybox-logger" successfully rolled out&lt;/p&gt;

&lt;p&gt;$ kubectl get pods -n priority -o custom-columns='NAME:.metadata.name,STATUS:.status.phase,PRIORITY:.spec.priority,CLASS:.spec.priorityClassName'&lt;br&gt;
NAME                              STATUS    PRIORITY   CLASS&lt;br&gt;
busybox-logger-645b98678b-bf5t5   Running   999999     high-priority&lt;br&gt;
busybox-logger-645b98678b-kfwmw   Running   999999     high-priority&lt;br&gt;
metrics-agent-8c89d9895-68282     Running   -10        low-priority&lt;br&gt;
queue-worker-76df598f64-vbrrj     Running   -10        low-priority&lt;br&gt;
queue-worker-76df598f64-w4mtl     Running   -10        low-priority&lt;/p&gt;

&lt;p&gt;$ kubectl get pods -n priority&lt;br&gt;
NAME                              READY   STATUS    RESTARTS   AGE&lt;br&gt;
busybox-logger-645b98678b-bf5t5   1/1     Running   0          7s&lt;br&gt;
busybox-logger-645b98678b-kfwmw   1/1     Running   0          13s&lt;br&gt;
metrics-agent-8c89d9895-68282     1/1     Running   0          13s&lt;br&gt;
queue-worker-76df598f64-vbrrj     1/1     Running   0          16s&lt;br&gt;
queue-worker-76df598f64-w4mtl     1/1     Running   0          16s&lt;br&gt;
&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;h2&gt;
  
  
  Exam tips
&lt;/h2&gt;

&lt;p&gt;The traps in this question are all reading comprehension. Highest USER-DEFINED class means you skip everything that starts with system; counting the two billion system classes gives a wildly wrong value. One less means minus one, so from one million you write 999999; do not invent a round number. kubectl create priorityclass is the fastest correct answer, and the &lt;code&gt;priorityClassName&lt;/code&gt; field goes in the pod template, not on the Deployment's own spec. When the neighbors get evicted, leave them alone: the question explicitly says modifying other deployments costs points, and preemption is the intended behavior, not an incident. And verify with the pods, not the deployment: custom-columns on &lt;code&gt;.spec.priority&lt;/code&gt; shows exactly what the grader checks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;'Highest user-defined' = ignore system-node-critical / system-cluster-critical&lt;/li&gt;
&lt;li&gt;'One less' = 1000000 - 1 = 999999; kubectl create priorityclass does it in one line&lt;/li&gt;
&lt;li&gt;priorityClassName goes in spec.template.spec (the POD template)&lt;/li&gt;
&lt;li&gt;Evicted neighbors are EXPECTED: do not 'fix' them, do not modify their Deployments&lt;/li&gt;
&lt;li&gt;Verify like the grader: pods' .spec.priority + rollout status&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Highest user-defined class 1000000 -&amp;gt; high-priority created at 999999&lt;/li&gt;
&lt;li&gt;One patch: spec.template.spec.priorityClassName = high-priority&lt;/li&gt;
&lt;li&gt;Full node -&amp;gt; scheduler PREEMPTS a low-priority pod (Preempted event), rollout completes&lt;/li&gt;
&lt;li&gt;Neighbors evicted as promised, never modified; subscribe + dev.to writeup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce this yourself
&lt;/h2&gt;

&lt;p&gt;The entire scenario is scripted on a throwaway &lt;code&gt;kind&lt;/code&gt; cluster: &lt;a href="https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios" rel="noopener noreferrer"&gt;https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;bash&lt;br&gt;
git clone &lt;a href="https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios.git" rel="noopener noreferrer"&gt;https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios.git&lt;/a&gt;&lt;br&gt;
cd TCS_CKA_2026_Exam_Scenarios/learning/scenarios/scenario11-priorityclass-preemption&lt;br&gt;
./setup.sh        # creates the cluster AND arms the scenario&lt;/p&gt;

&lt;h1&gt;
  
  
  solve it by hand, or:
&lt;/h1&gt;

&lt;p&gt;./solution.sh     # apply the answer key and verify&lt;br&gt;
&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;




&lt;p&gt;If this helped, &lt;strong&gt;subscribe to The Cyber SideKick&lt;/strong&gt; on YouTube for more CKA drills, and grab the newsletter at &lt;a href="https://thecybersidekick.beehiiv.com" rel="noopener noreferrer"&gt;https://thecybersidekick.beehiiv.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cka</category>
      <category>scheduling</category>
      <category>priorityclass</category>
    </item>
    <item>
      <title>Install Argo CD with Helm, and survive the missing-CRD trap (CKA)</title>
      <dc:creator>The Cyber Sidekick</dc:creator>
      <pubDate>Wed, 22 Jul 2026 16:38:38 +0000</pubDate>
      <link>https://dev.to/thecybersidekick/install-argo-cd-with-helm-and-survive-the-missing-crd-trap-cka-4ph7</link>
      <guid>https://dev.to/thecybersidekick/install-argo-cd-with-helm-and-survive-the-missing-crd-trap-cka-4ph7</guid>
      <description>&lt;h2&gt;
  
  
  Install Argo CD with Helm (and survive the missing-CRD trap)
&lt;/h2&gt;

&lt;p&gt;This exam question looks like three Helm commands: add a repository, render a template, install a release. And it hands you a comforting line: the Argo CD CRDs have already been pre-installed in the cluster. In this lab that line is false, on purpose, and what happens next teaches you exactly what &lt;code&gt;crds.install=false&lt;/code&gt; really means. Let's run it.&lt;/p&gt;

&lt;p&gt;🎥 &lt;strong&gt;Watch the video:&lt;/strong&gt; &lt;a href="https://www.youtube.com/watch?v=kYcHmIh_eHw" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=kYcHmIh_eHw&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a CKA Cluster Architecture, Installation &amp;amp; Configuration walkthrough. Every command below is real output from a live cluster, and you can reproduce the whole thing yourself (scripts at the end).&lt;/p&gt;

&lt;h2&gt;
  
  
  The scenario
&lt;/h2&gt;

&lt;p&gt;Here is the question. Add the official Argo CD Helm repository to the cluster under the name argo; the URL is given in the question, you never have to memorize repository links. The Argo CD CRDs, it says, have already been pre-installed. Generate a Helm template for release argocd, chart version 7.7.3, in the argocd namespace, save it to argo-helm.yaml, and configure the chart to not install the CRDs. Then install the release with the same name, version and namespace, again without CRDs. And one thing you do not have to do: configure access to the Argo CD server UI. Three commands, one premise. Remember the premise.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add the official Argo CD Helm repo as 'argo' (URL is given in the question)&lt;/li&gt;
&lt;li&gt;Premise: the Argo CD CRDs 'have already been pre-installed in the cluster'&lt;/li&gt;
&lt;li&gt;helm template: release argocd, chart version 7.7.3, ns argocd -&amp;gt; argo-helm.yaml, NO CRDs&lt;/li&gt;
&lt;li&gt;helm install: same release/version/namespace, NO CRDs; the server UI is out of scope&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How the Helm flow works
&lt;/h2&gt;

&lt;p&gt;Three Helm verbs do the work. helm repo add registers where charts come from, under a name you choose. helm template renders the chart into plain YAML on your machine; nothing touches the cluster, which is why the question can ask you to save the output to a file. helm install submits that same render to the cluster as a managed release. The flag that ties them together is set &lt;code&gt;crds.install=false&lt;/code&gt;. The argo-cd chart bundles the Argo CD custom resource definitions, and when a question says the CRDs are managed separately, your job is to keep the chart's hands off them, in the template and in the install. Keep the two commands identical apart from the verb; if they drift, the file you saved describes a different system than the one you deployed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add the argo repo
&lt;/h2&gt;

&lt;p&gt;Step one. helm repo add argo, then the URL from the question. Helm fetches the repository index immediately, so a mistyped URL fails right here instead of at install time. helm repo list confirms the repository is registered under exactly the name the question asked for: argo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;helm repo add argo https://argoproj.github.io/argo-helm
&lt;span class="go"&gt;"argo" has been added to your repositories

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;helm repo list
&lt;span class="go"&gt;NAME        URL                                     
argo        https://argoproj.github.io/argo-helm
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Render the template
&lt;/h2&gt;

&lt;p&gt;Step two, the template. Release name argocd, chart &lt;code&gt;argo/argo-cd&lt;/code&gt;, version 7.7.3 pinned exactly, namespace argocd, &lt;code&gt;crds.install&lt;/code&gt; set to false, and the whole thing redirected into argo-helm.yaml. The command prints nothing because the entire render went into the file, and it is substantial: over three thousand lines of YAML. Here is the check that proves the flag worked: grep the file for kind CustomResourceDefinition. Zero. This chart normally ships three CRDs; with the flag set, the render contains none of them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;helm template argocd argo/argo-cd &lt;span class="nt"&gt;--version&lt;/span&gt; 7.7.3 &lt;span class="nt"&gt;--namespace&lt;/span&gt; argocd &lt;span class="nt"&gt;--set&lt;/span&gt; crds.install&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; argo-helm.yaml
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; argo-helm.yaml
&lt;span class="go"&gt;3057 argo-helm.yaml

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'kind: CustomResourceDefinition'&lt;/span&gt; argo-helm.yaml
&lt;span class="go"&gt;0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  helm install
&lt;/h2&gt;

&lt;p&gt;Step three is the same command with the verb swapped: helm install, same release name, same chart, same version, same namespace, same &lt;code&gt;crds.install&lt;/code&gt; false. Helm prints the release notes, and helm ls in the argocd namespace shows release argocd, revision one, status deployed, chart argo-cd 7.7.3, app version 2.13.0. As far as Helm is concerned, the job is done. The cluster is about to disagree.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;helm &lt;span class="nb"&gt;install &lt;/span&gt;argocd argo/argo-cd &lt;span class="nt"&gt;--version&lt;/span&gt; 7.7.3 &lt;span class="nt"&gt;--namespace&lt;/span&gt; argocd &lt;span class="nt"&gt;--set&lt;/span&gt; crds.install&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;span class="go"&gt;NAME: argocd
LAST DEPLOYED: Mon Jul 13 09:49:46 2026
NAMESPACE: argocd
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
TEST SUITE: None

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;helm &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; argocd
&lt;span class="go"&gt;NAME    NAMESPACE   REVISION    UPDATED                                 STATUS      CHART           APP VERSION
argocd  argocd      1           2026-07-13 09:49:46.583903829 -0400 EDT deployed    argo-cd-7.7.3   v2.13.0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The crashloop
&lt;/h2&gt;

&lt;p&gt;Give the pods a moment and look again. Redis and dex are up, but the server and the applicationset controller are already in a crash loop, and the application controller never goes ready; everything that watches Argo CD resources is failing. Pull the logs from the server and the last line says exactly why: level fatal, the server could not find the requested resource. That is a component asking the API server for a resource type that does not exist. We told the chart not to install the CRDs because the question said they were pre-installed. They are not. The premise was false, and Helm had no way to know.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; argocd
&lt;span class="go"&gt;NAME                                               READY   STATUS             RESTARTS      AGE
argocd-application-controller-0                    0/1     Running            0             84s
argocd-applicationset-controller-bcdc99fcf-jdfj9   0/1     CrashLoopBackOff   3 (41s ago)   84s
argocd-dex-server-77f8fcf9d9-plbt8                 1/1     Running            0             84s
argocd-notifications-controller-7769fd5fd-rdqzh    1/1     Running            0             84s
argocd-redis-768545f6f-rjsgq                       1/1     Running            0             84s
argocd-repo-server-fd74968b8-dr772                 1/1     Running            0             84s
argocd-server-74b5fbf858-cn5t4                     0/1     CrashLoopBackOff   4 (4s ago)    84s

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl logs deploy/argocd-server &lt;span class="nt"&gt;-n&lt;/span&gt; argocd &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3
&lt;span class="go"&gt;time="2026-07-13T13:51:10Z" level=info msg="Starting configmap/secret informers"
time="2026-07-13T13:51:11Z" level=info msg="Configmap/secret informer synced"
time="2026-07-13T13:51:11Z" level=fatal msg="the server could not find the requested resource (post appprojects.argoproj.io)"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The missing CRDs
&lt;/h2&gt;

&lt;p&gt;Confirm the diagnosis in two seconds: kubectl get crd, grep argoproj, nothing. So we install the CRDs ourselves, but pinned to the right version, because CRDs and controllers drift apart across releases. The chart's appVersion tells you which Argo CD this chart deploys: helm show chart says 7.7.3 ships version 2.13.0. Apply the three CRD manifests from the Argo CD repository at exactly that tag: applications, applicationsets, and appprojects. Now the same grep finds all three.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get crd | &lt;span class="nb"&gt;grep &lt;/span&gt;argoproj
&lt;span class="go"&gt;No resources found

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;helm show chart argo/argo-cd &lt;span class="nt"&gt;--version&lt;/span&gt; 7.7.3 | &lt;span class="nb"&gt;grep &lt;/span&gt;appVersion
&lt;span class="go"&gt;appVersion: v2.13.0
version: 7.7.3

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; crds/
&lt;span class="go"&gt;customresourcedefinition.apiextensions.k8s.io/applications.argoproj.io created
customresourcedefinition.apiextensions.k8s.io/applicationsets.argoproj.io created
customresourcedefinition.apiextensions.k8s.io/appprojects.argoproj.io created

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get crd | &lt;span class="nb"&gt;grep &lt;/span&gt;argoproj
&lt;span class="go"&gt;applications.argoproj.io      2026-07-13T13:51:16Z
applicationsets.argoproj.io   2026-07-13T13:51:16Z
appprojects.argoproj.io       2026-07-13T13:51:16Z
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Restart + verify
&lt;/h2&gt;

&lt;p&gt;The crashlooped pods would eventually recover on their own as the backoff retries, but do not sit and wait in an exam. One rollout restart of the workloads in the namespace brings fresh pods up immediately, and this time every informer finds its CRDs. A minute later the whole namespace is Running and Ready. That is the full question: repository added as argo, template saved with zero CRDs in it, release installed at 7.7.3, and a healthy Argo CD. And notice what the fix was not: we never touched the Helm release. The release was correct; the environment broke the promise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; argocd rollout restart deployment
&lt;span class="go"&gt;deployment.apps/argocd-applicationset-controller restarted
deployment.apps/argocd-dex-server restarted
deployment.apps/argocd-notifications-controller restarted
deployment.apps/argocd-redis restarted
deployment.apps/argocd-repo-server restarted
deployment.apps/argocd-server restarted

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; argocd
&lt;span class="go"&gt;NAME                                                READY   STATUS    RESTARTS   AGE
argocd-application-controller-0                     1/1     Running   0          11s
argocd-applicationset-controller-547b7778c8-psvtd   1/1     Running   0          58s
argocd-dex-server-65cd9bfbb8-ptbnj                  1/1     Running   0          58s
argocd-notifications-controller-76fcfb8864-6s7sb    1/1     Running   0          58s
argocd-redis-6d9cb5f875-6z8xw                       1/1     Running   0          58s
argocd-repo-server-87cf9c697-dcqzx                  1/1     Running   0          58s
argocd-server-6f7b69c655-wgrhn                      1/1     Running   0          58s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exam tips
&lt;/h2&gt;

&lt;p&gt;A few traps. The repository URL and the chart version are printed in the question; copy them, pin &lt;code&gt;--version&lt;/code&gt; exactly, and never guess. Template and install must carry identical values: same namespace, same set &lt;code&gt;crds.install=false&lt;/code&gt;, only the verb changes. When a question states a premise like CRDs are pre-installed, verify it, it costs two seconds: kubectl get crd, grep argoproj. And learn the signature: a controller crashlooping with the server could not find the requested resource means a missing CRD, and the chart's appVersion tells you exactly which CRD version to apply.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The repo URL + chart version are IN the question: copy them, pin --version exactly&lt;/li&gt;
&lt;li&gt;template and install take identical values; only the verb changes&lt;/li&gt;
&lt;li&gt;Premises are verifiable: 'CRDs pre-installed' -&amp;gt; kubectl get crd | grep argoproj (2 seconds)&lt;/li&gt;
&lt;li&gt;'could not find the requested resource' = missing CRD; match it to the chart's appVersion&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;helm repo add argo -&amp;gt; helm template -&amp;gt; helm install, pinned to 7.7.3, crds.install=false on both&lt;/li&gt;
&lt;li&gt;helm ls said 'deployed'; the pods crashlooped: Helm status is not health&lt;/li&gt;
&lt;li&gt;Missing argoproj CRDs -&amp;gt; 'could not find the requested resource'; apply CRDs at appVersion v2.13.0&lt;/li&gt;
&lt;li&gt;rollout restart, all pods Running; subscribe + dev.to writeup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce this yourself
&lt;/h2&gt;

&lt;p&gt;The entire scenario is scripted on a throwaway &lt;code&gt;kind&lt;/code&gt; cluster: &lt;a href="https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios" rel="noopener noreferrer"&gt;https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios.git
&lt;span class="nb"&gt;cd &lt;/span&gt;TCS_CKA_2026_Exam_Scenarios/learning/scenarios/scenario10-argocd-helm-install
./setup.sh        &lt;span class="c"&gt;# creates the cluster AND arms the scenario&lt;/span&gt;
&lt;span class="c"&gt;# solve it by hand, or:&lt;/span&gt;
./solution.sh     &lt;span class="c"&gt;# apply the answer key and verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;If this helped, &lt;strong&gt;subscribe to The Cyber SideKick&lt;/strong&gt; on YouTube for more CKA drills, and grab the newsletter at &lt;a href="https://thecybersidekick.beehiiv.com" rel="noopener noreferrer"&gt;https://thecybersidekick.beehiiv.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cka</category>
      <category>helm</category>
      <category>argocd</category>
    </item>
    <item>
      <title>Install Calico and prove NetworkPolicy enforcement (CKA Services &amp; Networking)</title>
      <dc:creator>The Cyber Sidekick</dc:creator>
      <pubDate>Wed, 22 Jul 2026 16:09:06 +0000</pubDate>
      <link>https://dev.to/thecybersidekick/install-calico-and-prove-networkpolicy-enforcement-cka-services-networking-28k6</link>
      <guid>https://dev.to/thecybersidekick/install-calico-and-prove-networkpolicy-enforcement-cka-services-networking-28k6</guid>
      <description>&lt;h2&gt;
  
  
  Install Calico and prove NetworkPolicy enforcement
&lt;/h2&gt;

&lt;p&gt;The exam says install and configure a CNI of your choice, flannel or Calico, and then adds one line that makes the choice for you: it must support NetworkPolicy enforcement. flannel does not enforce policies, so this is a Calico question. Let's install it from the manifests, verify it, and prove a policy actually blocks traffic.&lt;/p&gt;

&lt;p&gt;🎥 &lt;strong&gt;Watch the video:&lt;/strong&gt; &lt;a href="https://www.youtube.com/watch?v=7Eaf7Wogsa4" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=7Eaf7Wogsa4&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a CKA Services &amp;amp; Networking walkthrough. Every command below is real output from a live cluster, and you can reproduce the whole thing yourself (scripts at the end).&lt;/p&gt;

&lt;h2&gt;
  
  
  The scenario
&lt;/h2&gt;

&lt;p&gt;Here is the setup. A fresh cluster has no CNI, so its nodes are NotReady and CoreDNS is Pending. The question offers flannel or Calico, says install from manifest files, do not use Helm, and lists three requirements: the CNI is properly installed and configured, pods can communicate with each other, and it supports NetworkPolicy enforcement. That last requirement is the trap. flannel gives you pod networking but silently ignores NetworkPolicy objects. Only Calico satisfies all three.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A fresh cluster with NO CNI: nodes NotReady, CoreDNS Pending&lt;/li&gt;
&lt;li&gt;Install a CNI of your choice (flannel or Calico) from manifests, no Helm&lt;/li&gt;
&lt;li&gt;Pods must communicate with each other, and the CNI must be properly installed&lt;/li&gt;
&lt;li&gt;'Must support NetworkPolicy enforcement' =&amp;gt; flannel is out, Calico is the answer&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How the Calico operator install works
&lt;/h2&gt;

&lt;p&gt;Calico's manifest install is operator based and comes in three parts. First the operator CRDs, then the Tigera operator itself, a deployment that knows how to install and manage Calico. Then you hand the operator an Installation resource describing the Calico you want, and it deploys everything into the calico-system namespace to match. Two things to know before you touch the keyboard. The Installation's IP pool cidr must agree with the cluster's pod CIDR, which is 192.168.0.0/16 here. And these manifests need kubectl create, not apply: the CRD file is bigger than the annotation kubectl apply attaches, so apply fails on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  No CNI: nodes NotReady
&lt;/h2&gt;

&lt;p&gt;Start by confirming the starting state. Every node is NotReady, the classic symptom of a missing CNI. And listing pods across all namespaces shows two things: CoreDNS is Pending because there is no pod network for it to join, and there are no calico or flannel pods anywhere, so no CNI is installed yet. This is a fresh cluster, and the network plugin is ours to install.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get nodes
&lt;span class="go"&gt;NAME                          STATUS     ROLES           AGE   VERSION
cka-scenario9-control-plane   NotReady   control-plane   8s    v1.36.1
&lt;/span&gt;&lt;span class="gp"&gt;cka-scenario9-worker          NotReady   &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;0s    v1.36.1
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get pods &lt;span class="nt"&gt;-A&lt;/span&gt;
&lt;span class="go"&gt;NAMESPACE            NAME                                                  READY   STATUS    RESTARTS   AGE
kube-system          coredns-589f44dc88-522x8                              0/1     Pending   0          1s
kube-system          coredns-589f44dc88-zsxh2                              0/1     Pending   0          1s
kube-system          etcd-cka-scenario9-control-plane                      0/1     Running   0          8s
kube-system          kube-apiserver-cka-scenario9-control-plane            1/1     Running   0          8s
kube-system          kube-controller-manager-cka-scenario9-control-plane   1/1     Running   0          8s
kube-system          kube-proxy-ds9pp                                      1/1     Running   0          1s
kube-system          kube-proxy-lwx6m                                      0/1     Pending   0          3s
kube-system          kube-scheduler-cka-scenario9-control-plane            0/1     Running   0          8s
local-path-storage   local-path-provisioner-855c7b7774-75cbk               0/1     Pending   0          1s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create the CRDs + operator
&lt;/h2&gt;

&lt;p&gt;Install the operator pieces with kubectl create. The first file registers the custom resource definitions, thirty two of them. The second deploys the Tigera operator itself. Note the verb: create, not apply. Some of these CRDs are larger than the last-applied annotation kubectl apply wants to attach, so apply fails on this file, and the Calico docs say create for exactly that reason. A few seconds later the operator pod is Running in the tigera-operator namespace, waiting for us to tell it what to install.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl create &lt;span class="nt"&gt;-f&lt;/span&gt; operator-crds.yaml
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;customresourcedefinition.apiextensions.k8s.io/ipamconfigs.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamhandles.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ippools.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipreservations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/kubecontrollersconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/networkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/networksets.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/stagedglobalnetworkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/stagedkubernetesnetworkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/stagednetworkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/tiers.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/adminnetworkpolicies.policy.networking.k8s.io created
customresourcedefinition.apiextensions.k8s.io/baselineadminnetworkpolicies.policy.networking.k8s.io created

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl create &lt;span class="nt"&gt;-f&lt;/span&gt; tigera-operator.yaml
&lt;span class="go"&gt;namespace/tigera-operator created
serviceaccount/tigera-operator created
clusterrole.rbac.authorization.k8s.io/tigera-operator-secrets created
clusterrole.rbac.authorization.k8s.io/tigera-operator created
clusterrolebinding.rbac.authorization.k8s.io/tigera-operator created
rolebinding.rbac.authorization.k8s.io/tigera-operator-secrets created
deployment.apps/tigera-operator created

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; tigera-operator
&lt;span class="go"&gt;NAME                               READY   STATUS    RESTARTS   AGE
tigera-operator-696d7c8fc4-59bf5   1/1     Running   0          4s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Installation resource
&lt;/h2&gt;

&lt;p&gt;Now describe the Calico you want. This Installation resource is the heart of the stock custom-resources file, and the field that matters is the IP pool cidr: 192.168.0.0/16, matching this cluster's pod CIDR. If those two disagree you get the same class of pain as a flannel CIDR mismatch, so always check before you create it. Create it and the operator takes over, pulling Calico up piece by piece. The tigerastatus resource is your progress bar: when every row reports Available True, the install is done.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;custom-resources.yaml
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;The Installation resource: the Tigera operator watches &lt;span class="k"&gt;for &lt;/span&gt;this and deploys Calico
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;to match it. The ipPool cidr MUST agree with the cluster&lt;span class="s1"&gt;'s pod CIDR (192.168.0.0/16).
&lt;/span&gt;&lt;span class="go"&gt;apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
  name: default
spec:
  calicoNetwork:
    ipPools:
      - cidr: 192.168.0.0/16
        encapsulation: VXLANCrossSubnet

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl create &lt;span class="nt"&gt;-f&lt;/span&gt; custom-resources.yaml
&lt;span class="go"&gt;installation.operator.tigera.io/default created

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get tigerastatus
&lt;span class="go"&gt;NAME      AVAILABLE   PROGRESSING   DEGRADED   SINCE
calico    True        False         False      0s
ippools   True        False         False      55s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Calico up, nodes Ready
&lt;/h2&gt;

&lt;p&gt;Trust but verify. In calico-system there is a calico-node pod on every node, that is the dataplane that wires up pods and enforces policy, plus typha and the kube-controllers, all Running. And the payoff: both nodes have flipped to Ready, and CoreDNS is finally scheduled. The CNI is properly installed and configured, which was requirement one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; calico-system &lt;span class="nt"&gt;-o&lt;/span&gt; wide
&lt;span class="go"&gt;NAME                                       READY   STATUS    RESTARTS   AGE   IP                NODE                          NOMINATED NODE   READINESS GATES
&lt;/span&gt;&lt;span class="gp"&gt;calico-kube-controllers-6c8496f5c8-flpxp   1/1     Running   0          63s   192.168.227.197   cka-scenario9-control-plane   &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&amp;lt;none&amp;gt;
&lt;span class="gp"&gt;calico-node-dpqqj                          1/1     Running   0          63s   172.18.0.15       cka-scenario9-worker          &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&amp;lt;none&amp;gt;
&lt;span class="gp"&gt;calico-node-pdgjt                          1/1     Running   0          63s   172.18.0.16       cka-scenario9-control-plane   &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&amp;lt;none&amp;gt;
&lt;span class="gp"&gt;calico-typha-6f8c54fdf6-mnvnn              1/1     Running   0          63s   172.18.0.15       cka-scenario9-worker          &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&amp;lt;none&amp;gt;
&lt;span class="gp"&gt;csi-node-driver-6mlr5                      2/2     Running   0          63s   192.168.227.194   cka-scenario9-control-plane   &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&amp;lt;none&amp;gt;
&lt;span class="gp"&gt;csi-node-driver-ntcxl                      2/2     Running   0          63s   192.168.178.65    cka-scenario9-worker          &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&amp;lt;none&amp;gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get nodes
&lt;span class="go"&gt;NAME                          STATUS   ROLES           AGE   VERSION
cka-scenario9-control-plane   Ready    control-plane   82s   v1.36.1
&lt;/span&gt;&lt;span class="gp"&gt;cka-scenario9-worker          Ready    &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;73s   v1.36.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Prove pods can talk
&lt;/h2&gt;

&lt;p&gt;Requirement two: pods must communicate with each other. Create two test pods pinned to different nodes and list them wide, each gets an IP from 192.168.0.0/16 on a different host. Then exec into test one and ping test two's pod IP: replies come back across the nodes, so pod-to-pod networking works end to end. One practical note: ping the IP, not the pod name. Bare pods do not get DNS records, so a name lookup fails with bad address even on a perfectly healthy network.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; connectivity-test.yaml
&lt;span class="go"&gt;pod/test1 created
pod/test2 created

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get pods &lt;span class="nt"&gt;-o&lt;/span&gt; wide &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;conn-test
&lt;span class="go"&gt;NAME    READY   STATUS    RESTARTS   AGE   IP                NODE                          NOMINATED NODE   READINESS GATES
&lt;/span&gt;&lt;span class="gp"&gt;test1   1/1     Running   0          3s    192.168.178.66    cka-scenario9-worker          &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&amp;lt;none&amp;gt;
&lt;span class="gp"&gt;test2   1/1     Running   0          3s    192.168.227.198   cka-scenario9-control-plane   &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&amp;lt;none&amp;gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;test1 &lt;span class="nt"&gt;--&lt;/span&gt; ping &lt;span class="nt"&gt;-c&lt;/span&gt; 3 &amp;lt;test2-ip&amp;gt;
&lt;span class="go"&gt;PING 192.168.227.198 (192.168.227.198): 56 data bytes
64 bytes from 192.168.227.198: seq=0 ttl=62 time=0.351 ms
64 bytes from 192.168.227.198: seq=1 ttl=62 time=0.091 ms
64 bytes from 192.168.227.198: seq=2 ttl=62 time=0.100 ms

--- 192.168.227.198 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.091/0.180/0.351 ms
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Prove policy enforcement
&lt;/h2&gt;

&lt;p&gt;Requirement three is the one flannel cannot do: NetworkPolicy enforcement. This default-deny policy comes straight from the Kubernetes docs. The empty podSelector selects every pod in the default namespace, and listing both Ingress and Egress with no rules denies all traffic in both directions. Apply it, confirm it exists, and run the exact same ping again. Three packets, zero replies, one hundred percent loss. The policy is not just stored in the API, Calico is enforcing it on the wire. That failing ping is the proof the question asked for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;deny-all.yaml
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Default-deny-all, straight from the Kubernetes docs: the empty podSelector selects
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;EVERY pod &lt;span class="k"&gt;in &lt;/span&gt;the namespace&lt;span class="p"&gt;;&lt;/span&gt; both policyTypes with no rules &lt;span class="o"&gt;=&lt;/span&gt; deny all traffic.
&lt;span class="go"&gt;apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: default
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; deny-all.yaml
&lt;span class="go"&gt;networkpolicy.networking.k8s.io/default-deny-all created

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get networkpolicy
&lt;span class="go"&gt;NAME               POD-SELECTOR   AGE
&lt;/span&gt;&lt;span class="gp"&gt;default-deny-all   &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;0s
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;test1 &lt;span class="nt"&gt;--&lt;/span&gt; ping &lt;span class="nt"&gt;-c&lt;/span&gt; 3 &lt;span class="nt"&gt;-w&lt;/span&gt; 5 &amp;lt;test2-ip&amp;gt;
&lt;span class="go"&gt;PING 192.168.227.198 (192.168.227.198): 56 data bytes

--- 192.168.227.198 ping statistics ---
3 packets transmitted, 0 packets received, 100% packet loss
command terminated with exit code 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exam tips
&lt;/h2&gt;

&lt;p&gt;A few traps. When a question says the CNI must support NetworkPolicy enforcement, that one phrase eliminates flannel; pick Calico and move on. Use kubectl create for the Calico operator manifests, because apply chokes on the big CRD file. Check that the Installation's IP pool cidr matches the cluster pod CIDR before you create it, then watch kubectl get tigerastatus until everything is Available. And verification means behavior, not status: prove the ping works, then prove a default-deny breaks the same ping.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;'Must support NetworkPolicy enforcement' =&amp;gt; flannel is out, use Calico&lt;/li&gt;
&lt;li&gt;kubectl create -f (not apply) for the Calico manifests: the CRD file is too big for apply&lt;/li&gt;
&lt;li&gt;Installation ipPool cidr must match the cluster pod CIDR; watch 'kubectl get tigerastatus'&lt;/li&gt;
&lt;li&gt;Verify behavior: ping works, then a default-deny makes the same ping fail&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;NetworkPolicy enforcement required =&amp;gt; Calico, installed with kubectl create (no Helm)&lt;/li&gt;
&lt;li&gt;operator CRDs -&amp;gt; Tigera operator -&amp;gt; Installation (ipPool = cluster pod CIDR)&lt;/li&gt;
&lt;li&gt;tigerastatus Available, nodes Ready, cross-node ping works&lt;/li&gt;
&lt;li&gt;default-deny =&amp;gt; the same ping fails: enforcement proven; subscribe + dev.to writeup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce this yourself
&lt;/h2&gt;

&lt;p&gt;The entire scenario is scripted on a throwaway &lt;code&gt;kind&lt;/code&gt; cluster: &lt;a href="https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios" rel="noopener noreferrer"&gt;https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios.git
&lt;span class="nb"&gt;cd &lt;/span&gt;TCS_CKA_2026_Exam_Scenarios/learning/scenarios/scenario9-calico-network-policy
./setup.sh        &lt;span class="c"&gt;# creates the cluster AND arms the scenario&lt;/span&gt;
&lt;span class="c"&gt;# solve it by hand, or:&lt;/span&gt;
./solution.sh     &lt;span class="c"&gt;# apply the answer key and verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;If this helped, &lt;strong&gt;subscribe to The Cyber SideKick&lt;/strong&gt; on YouTube for more CKA drills, and grab the newsletter at &lt;a href="https://thecybersidekick.beehiiv.com" rel="noopener noreferrer"&gt;https://thecybersidekick.beehiiv.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cka</category>
      <category>networking</category>
      <category>calico</category>
    </item>
  </channel>
</rss>
