<?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: sean-mancho</title>
    <description>The latest articles on DEV Community by sean-mancho (@seanmancho).</description>
    <link>https://dev.to/seanmancho</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%2F4173203%2Fc2e04917-35cb-4188-ad3d-2d3e9db73baa.png</url>
      <title>DEV Community: sean-mancho</title>
      <link>https://dev.to/seanmancho</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/seanmancho"/>
    <language>en</language>
    <item>
      <title>Building a Private Kubernetes Lab on AWS</title>
      <dc:creator>sean-mancho</dc:creator>
      <pubDate>Wed, 04 Nov 2026 05:11:07 +0000</pubDate>
      <link>https://dev.to/seanmancho/building-a-private-kubernetes-lab-on-aws-15kc</link>
      <guid>https://dev.to/seanmancho/building-a-private-kubernetes-lab-on-aws-15kc</guid>
      <description>&lt;p&gt;For the hands-on side of the CKS exam, I wanted a lab where I could experiment, break things and start over, and learn the infrastructure layer underneath the cluster too. Hosted practice environments are great for timed exam drills, but not for hardening one cluster over weeks. Local tools like kind or minikube mostly hide the static pod manifests, audit logging and AppArmor profiles the CKS curriculum covers. Practicing on a real kubeadm cluster is the closest match. kubeadm is the official tool for bootstrapping Kubernetes on your own machines.&lt;/p&gt;

&lt;p&gt;The lab also had to stay secure and low-cost. You get full control of the cluster for troubleshooting and hardening, and you can stop the instances when you're not using them to keep costs down.&lt;/p&gt;

&lt;p&gt;The lab is a &lt;strong&gt;two-node kubeadm cluster on AWS&lt;/strong&gt; with four properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Private nodes.&lt;/strong&gt; The control plane and worker sit in private subnets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One way in.&lt;/strong&gt; You reach the cluster over SSH through a single edge instance, the only point of contact with it. The edge is both the &lt;em&gt;bastion&lt;/em&gt; and the &lt;em&gt;NAT&lt;/em&gt; that gives the private nodes outbound-only internet access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure defaults.&lt;/strong&gt; Security groups allow only SSH from your IP and only the ports the cluster needs between nodes, plus a few convenience ports. IMDSv2, the token-protected EC2 instance metadata service, is required, and disks are encrypted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pay only for compute you use.&lt;/strong&gt; A small shell console starts and stops the instances, so you're billed for compute only while you're working. Stopped, the disks still cost about $4 a month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post covers the &lt;em&gt;design&lt;/em&gt;: what the lab looks like and why. The code, the step-by-step build and the reference details live in the repo, tagged for this post: &lt;a href="https://github.com/sean-mancho/k8s-hardening-lab/tree/ep01" rel="noopener noreferrer"&gt;&lt;code&gt;k8s-hardening-lab&lt;/code&gt; at &lt;code&gt;ep01&lt;/code&gt;&lt;/a&gt;. One script builds the lab. Then you bootstrap the cluster yourself, as practice, or let the same script do it with one command.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9z59yxx4yu8s9j6qyp17.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9z59yxx4yu8s9j6qyp17.png" alt="Network diagram: the laptop reaches k8s-edge, the only public instance, over SSH. The control plane and worker sit in private subnets and reach the internet only through the edge." width="800" height="738"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The edge is the only instance with a public IP, alone in a public subnet. The control plane at &lt;code&gt;10.0.2.10&lt;/code&gt; and the worker at &lt;code&gt;10.0.3.11&lt;/code&gt; sit in private subnets with no route from the internet, each subnet in its own &lt;em&gt;availability zone&lt;/em&gt; (AZ). Nothing outside can reach the cluster except SSH from your IP to the edge, and the security groups on each instance decide what may pass between them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;From&lt;/th&gt;
&lt;th&gt;To&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Your IP (/32)&lt;/td&gt;
&lt;td&gt;k8s-edge&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;SSH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private subnets&lt;/td&gt;
&lt;td&gt;k8s-edge&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;NAT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;k8s-edge&lt;/td&gt;
&lt;td&gt;k8s-control-plane&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;SSH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;k8s-control-plane&lt;/td&gt;
&lt;td&gt;k8s-worker&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;SSH hop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;k8s-control-plane&lt;/td&gt;
&lt;td&gt;k8s-worker&lt;/td&gt;
&lt;td&gt;10250&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;kubelet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;k8s-worker&lt;/td&gt;
&lt;td&gt;k8s-control-plane&lt;/td&gt;
&lt;td&gt;6443&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;API server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Each node&lt;/td&gt;
&lt;td&gt;The other node&lt;/td&gt;
&lt;td&gt;8472&lt;/td&gt;
&lt;td&gt;UDP&lt;/td&gt;
&lt;td&gt;VXLAN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Each node&lt;/td&gt;
&lt;td&gt;The other node&lt;/td&gt;
&lt;td&gt;4240&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;Cilium health&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Each node&lt;/td&gt;
&lt;td&gt;The other node&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;ICMP&lt;/td&gt;
&lt;td&gt;Cilium health&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Each node&lt;/td&gt;
&lt;td&gt;The other node&lt;/td&gt;
&lt;td&gt;4244&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;Hubble&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Each node&lt;/td&gt;
&lt;td&gt;The other node&lt;/td&gt;
&lt;td&gt;30000-32767&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;NodePorts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All three instances&lt;/td&gt;
&lt;td&gt;0.0.0.0/0&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;Outbound&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Node-to-node peers are security groups, not IP ranges; only the edge's NAT rule uses the private subnets' ranges. The VPC's default security group has no rules.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkinxc4pe58kpyqxgol80.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkinxc4pe58kpyqxgol80.png" alt="SSH path: the laptop connects to k8s-edge on port 22, ProxyJumps to k8s-control-plane at 10.0.2.10, and from there to k8s-worker at 10.0.3.11." width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Traffic flows in two directions, and both pass through the edge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In: SSH only.&lt;/strong&gt; You run &lt;code&gt;kubectl&lt;/code&gt; on the control plane over SSH. SSH uses &lt;code&gt;ProxyJump&lt;/code&gt;, so your private key never leaves your laptop. The control plane can still ask your laptop's SSH agent to sign with it while you're connected; that's the agent forwarding covered at the end of this post. Nothing forwards the API server's port to your laptop, and only the worker's security group may reach port 6443, so no machine besides the cluster's own nodes can reach the API server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Out: through the edge.&lt;/strong&gt; The private route table sends &lt;code&gt;0.0.0.0/0&lt;/code&gt; to the edge, which accepts any traffic from the nodes and sends it out under its own address, like a home router. Connections in the other direction are blocked: apart from SSH to the control plane, the edge can't open connections to the nodes, and the worker refuses connections from the edge altogether.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Design decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  One edge instance: the only way in and the only way out
&lt;/h3&gt;

&lt;p&gt;The edge is a bastion that doubles as a NAT instance for the nodes in the private subnets. The nodes need internet access, and a managed NAT gateway alone costs about $33 a month before data charges. That's more than this whole lab costs at around 40 hours of use a month, and it brings scalability the lab would never use. The edge does the NAT work only while it's running, and it stops when the lab stops.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three AZs, one instance each
&lt;/h3&gt;

&lt;p&gt;Each instance sits in its own availability zone, so node-to-node traffic crosses subnets and routing boundaries, which makes NetworkPolicy and CNI troubleshooting realistic. The spread doesn't buy redundancy, though. Losing the edge's AZ takes your SSH access and the cluster's internet with it, and losing the control plane's AZ takes the API server down; real high availability needs duplicates. Cross-AZ traffic costs about $0.01/GB in each direction, which is negligible at lab scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secure defaults from the first boot
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IMDSv2 required, hop limit 1, on every instance.&lt;/strong&gt; The metadata service answers the node itself, but a reply to an ordinary pod needs one more network hop than the limit allows, so pods can't reach it by default. The instances have no IAM role yet, so there are no cloud credentials to steal today. The hop limit keeps that true for ordinary pods once Post 6 adds least-privilege IAM. Pods with &lt;code&gt;hostNetwork: true&lt;/code&gt; share the node's network and still get through; Post 5 comes back to this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A pod has its own network namespace, so it's like a small separate machine inside the node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Node:  node → IMDS → node                  reply arrives
  Pod:   pod → node → IMDS → node ✗ pod      reply dropped at the node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With hop limit 1, the token reply can cross one hop, which only gets it as far as the node. Passing it on to the pod would take a second hop, so it's dropped. IMDSv2 is also required, so without a token the pod can't read metadata at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;  &lt;span class="n"&gt;metadata_options&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;http_tokens&lt;/span&gt;                 &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"required"&lt;/span&gt;
    &lt;span class="n"&gt;http_put_response_hop_limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encrypted disks.&lt;/strong&gt; Every root volume is encrypted gp3 (&lt;code&gt;encrypted = true&lt;/code&gt;), with AWS's default EBS key, so etcd's data on the control plane is encrypted at rest at the disk level. That's not the same as encrypting Secrets inside etcd, which the API server does with its own key; Post 7 turns that on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Version and tooling: Kubernetes 1.35, OpenTofu, Cilium
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes 1.35&lt;/strong&gt; is the CKS exam's version as of November 2026, according to the &lt;a href="https://docs.linuxfoundation.org/tc-docs/certification/faq-cka-ckad-cks" rel="noopener noreferrer"&gt;Linux Foundation's exam FAQ&lt;/a&gt;. The &lt;a href="https://training.linuxfoundation.org/certification/certified-kubernetes-security-specialist/" rel="noopener noreferrer"&gt;certification page&lt;/a&gt; already shows v1.37, so check before you book.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenTofu&lt;/strong&gt; is the open-source fork of Terraform. The lab's infrastructure as code is written in OpenTofu, and the scripts call &lt;code&gt;tofu&lt;/code&gt; to apply it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cilium&lt;/strong&gt; is the CNI (Container Network Interface) plugin that gives pods their IPs and carries traffic between them. Later posts use it for NetworkPolicy enforcement and pod-to-pod encryption.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Using the lab
&lt;/h2&gt;

&lt;p&gt;With the design in place, the lab is driven from one console. It deploys into &lt;em&gt;your&lt;/em&gt; AWS account, with &lt;em&gt;your&lt;/em&gt; SSH key and IP; nothing here connects to my environment. Expect about $7–8 a month if you stop it between sessions. You need AWS CLI v2 with credentials, OpenTofu, OpenSSH and &lt;code&gt;curl&lt;/code&gt;; the full &lt;a href="https://github.com/sean-mancho/k8s-hardening-lab/blob/ep01/infra/README.md" rel="noopener noreferrer"&gt;prerequisites&lt;/a&gt; are in the repo. Clone the repo at this post's tag and run the console from the repo root:&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 --branch ep01 https://github.com/sean-mancho/k8s-hardening-lab.git
&lt;span class="nb"&gt;cd&lt;/span&gt; k8s-hardening-lab
./infra/scripts/lab.sh                     &lt;span class="c"&gt;# opens the lab console: a cks-lab&amp;gt; prompt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cks-lab&amp;gt; start                             # build or start the lab, check first boot
cks-lab&amp;gt; init                              # optional: bootstrap the cluster for you (or use the manual process)
cks-lab&amp;gt; ssh worker                        # shell on any node; the jumps happen automatically
cks-lab&amp;gt; stop                              # stop the instances (or quit, which offers to)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;lab.sh&lt;/code&gt;&lt;/strong&gt; is your base terminal: start the lab, &lt;code&gt;ssh&lt;/code&gt; into a node, and &lt;code&gt;exit&lt;/code&gt; back to the prompt. &lt;code&gt;start&lt;/code&gt; asks which IP may SSH in, defaulting to your current one, and runs &lt;code&gt;tofu apply&lt;/code&gt;. It never passes &lt;code&gt;-auto-approve&lt;/code&gt;, so you can &lt;strong&gt;read the plan&lt;/strong&gt; before you type &lt;code&gt;yes&lt;/code&gt;. It then starts the instances and regenerates &lt;code&gt;infra/ssh_config&lt;/code&gt;, because the edge gets a new public IP on every start. Finally it waits for each instance's first boot and prints &lt;code&gt;ready&lt;/code&gt; for each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;kubectl&lt;/code&gt; runs on the control plane&lt;/strong&gt; (&lt;code&gt;ssh control-plane&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;delete&lt;/code&gt; destroys the lab&lt;/strong&gt; when you're done or taking a long break.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;After &lt;code&gt;start&lt;/code&gt;, you choose how the cluster gets built.&lt;/strong&gt; &lt;code&gt;start&lt;/code&gt; brings up the machines, not Kubernetes. On a new lab, there's no cluster yet, and &lt;code&gt;start&lt;/code&gt; ends by listing your two ways to build it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Manual process&lt;/strong&gt;, recommended the first time, because you see every step: run &lt;code&gt;kubeadm init&lt;/code&gt; from a config file, install Cilium, then &lt;code&gt;kubeadm join&lt;/code&gt; the worker. &lt;a href="https://github.com/sean-mancho/k8s-hardening-lab/blob/ep01/infra/README.md#3-initialize-the-control-plane" rel="noopener noreferrer"&gt;Sections 3–5 of the build guide&lt;/a&gt; walk through each command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In one go&lt;/strong&gt;: type &lt;code&gt;init&lt;/code&gt; at the &lt;code&gt;cks-lab&amp;gt;&lt;/code&gt; prompt. It runs the same steps over SSH and skips any that are already done, so it's safe to rerun. Use it for rebuilds, or when you want to get straight to the hardening.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either way you end up with the same cluster. A stopped lab keeps its cluster, so later sessions need only &lt;code&gt;start&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/sean-mancho/k8s-hardening-lab/blob/ep01/infra/README.md#costs" rel="noopener noreferrer"&gt;cost breakdown&lt;/a&gt; and teardown are covered in &lt;code&gt;infra/README.md&lt;/code&gt;. Run &lt;code&gt;stop&lt;/code&gt; at the end of every session.&lt;/p&gt;




&lt;h2&gt;
  
  
  Accepted risks and alternatives
&lt;/h2&gt;

&lt;p&gt;A study cluster should be reliable rather than clever, so the lab accepts a few risks on purpose, and the capstone uses them as the attacker's starting points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open egress.&lt;/strong&gt; The edge's outbound rule is "all traffic" and its NAT accepts every protocol from the nodes. Post 5 tightens that at the edge and pairs it with the in-cluster version of the same control, an egress NetworkPolicy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convenience ports between nodes.&lt;/strong&gt; The NodePort range 30000–32767, ICMP and Cilium's Hubble port are open between the nodes so services and debugging work without fuss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No VPC flow logs.&lt;/strong&gt; There's no network-level record of who talked to whom.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host keys trusted on first use.&lt;/strong&gt; The repo shows &lt;a href="https://github.com/sean-mancho/k8s-hardening-lab/blob/ep01/infra/README.md#verifying-the-host-keys" rel="noopener noreferrer"&gt;how to verify them&lt;/a&gt; against each instance's console output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A forwarded SSH agent: the risk to watch.&lt;/strong&gt; Your agent is forwarded to the control plane so &lt;code&gt;ssh k8s-worker&lt;/code&gt; and &lt;code&gt;scp&lt;/code&gt; work from there. While you're connected, root on that host can use &lt;strong&gt;every key loaded in it&lt;/strong&gt;, not only the lab key, so a GitHub key or any other key in the same agent is exposed too. Load only the lab key while you work. &lt;code&gt;ssh-add -c&lt;/code&gt; makes each use ask for your confirmation, but it needs an &lt;code&gt;ssh-askpass&lt;/code&gt; program, which macOS doesn't include. Post 6 looks at &lt;code&gt;AllowAgentForwarding&lt;/code&gt; and the alternatives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;An alternative worth knowing:&lt;/strong&gt; inbound SSH itself could go entirely, replaced by EC2 Instance Connect Endpoint or SSM Session Manager. But SSM needs an IAM instance profile the lab doesn't have yet, and the nodes would still need the edge for egress, so a combined bastion/NAT keeps the lab to one box.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next up
&lt;/h2&gt;

&lt;p&gt;Once it's built, you have a private Kubernetes lab of your own to troubleshoot, break and rebuild as often as you like. Explore it, and get to know every component and its security implications before the series starts changing them.&lt;/p&gt;

&lt;p&gt;The next post, out Tuesday, November 10, scores this fresh cluster against the CIS Kubernetes Benchmark with kube-bench and verifies the platform binaries installed here. The &lt;a href="https://github.com/sean-mancho/k8s-hardening-lab/tree/ep01#how-the-series-builds-on-the-lab" rel="noopener noreferrer"&gt;repo README&lt;/a&gt; maps every post to the part of the lab it builds on.&lt;/p&gt;

&lt;p&gt;Follow the series to get it in your feed. Questions or improvements? Drop them in the comments, or open an issue on the repo.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>security</category>
      <category>aws</category>
      <category>opentofu</category>
    </item>
    <item>
      <title>I Passed the CKS. Now I'm Hardening My Own Cluster.</title>
      <dc:creator>sean-mancho</dc:creator>
      <pubDate>Thu, 22 Oct 2026 10:24:51 +0000</pubDate>
      <link>https://dev.to/seanmancho/i-passed-the-cks-now-im-hardening-my-own-cluster-em7</link>
      <guid>https://dev.to/seanmancho/i-passed-the-cks-now-im-hardening-my-own-cluster-em7</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I passed the CKS! It took a few tries, and every retake taught me something.&lt;/li&gt;
&lt;li&gt;It's simulation-based: real tasks in live clusters, no multiple choice. Open book doesn't buy you time, so pacing is everything.&lt;/li&gt;
&lt;li&gt;It leans hard on Linux and containers, not just Kubernetes. Hands-on time in a real cluster beats memorizing commands.&lt;/li&gt;
&lt;li&gt;Next up: I'm building my own lab cluster on AWS and working through every exam domain, one post a week.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frqgf1s23ww28n4zas47v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frqgf1s23ww28n4zas47v.png" alt="Certified Kubernetes Security Specialist (CKS) certificate issued to Sean Mancho by the Cloud Native Computing Foundation, October 2026" width="800" height="617"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I took it
&lt;/h2&gt;

&lt;p&gt;Honestly? I find security genuinely interesting. More and more runs on Kubernetes these days (AI model serving, event streaming, edge deployments) and new container vulnerabilities show up all the time. I wanted to understand how those workloads get locked down at the infrastructure and network layers.&lt;/p&gt;

&lt;p&gt;I also like using certifications as a study plan. They give me a clear target and keep me current on the tooling, the docs and the community around it. The CKS felt like the right way to go from knowing how Kubernetes runs to knowing how to secure it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the exam is really like
&lt;/h2&gt;

&lt;p&gt;The CKS is a two-hour, online, proctored exam, and it's simulation-based. There's no multiple choice to fall back on: every question is a real task you solve on the command line in a live cluster. You need the CKA first, and the cert is good for two years. Details are current as of October 2026; check the &lt;a href="https://training.linuxfoundation.org/certification/certified-kubernetes-security-specialist/" rel="noopener noreferrer"&gt;Linux Foundation CKS page&lt;/a&gt; for the latest.&lt;/p&gt;

&lt;p&gt;That format is why pacing matters so much: you get roughly six to eight minutes per task, so there's no time to wander. It's open book, so the Kubernetes docs (and the docs for the exam's tools) are fair game, but that doesn't buy you time. Finding something in the docs is easy. Finding it fast enough is the hard part. It's also tempting to sink twenty minutes into one stubborn problem and leave yourself scrambling through the rest of the exam. (More on that below.)&lt;/p&gt;

&lt;p&gt;What surprised me most was how much of it isn't Kubernetes. It leans just as hard on Linux and container fundamentals. Here are the six domains it covers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Domain&lt;/th&gt;
&lt;th&gt;Weight&lt;/th&gt;
&lt;th&gt;What it covers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cluster Setup&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;Network policies, CIS Benchmark review of etcd, kubelet and kube-apiserver, Ingress with TLS, protecting node metadata and endpoints, verifying platform binaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster Hardening&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;Restricting API access, RBAC, service account hygiene, keeping Kubernetes upgraded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System Hardening&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;Reducing the host OS attack surface, least-privilege IAM, limiting external network access, AppArmor and seccomp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minimize Microservice Vulnerabilities&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;Pod Security Standards, Secrets management, isolation and sandboxing (gVisor, Kata Containers), Pod-to-Pod encryption (Cilium, Istio)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supply Chain Security&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;Minimal base images, SBOMs, image signing and permitted registries, static analysis (Kubesec, KubeLinter)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitoring, Logging and Runtime Security&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;Behavioral analytics with Falco, container immutability at runtime, Kubernetes audit logs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Domains and weights are from the official curriculum; the tool examples are mine.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I studied
&lt;/h2&gt;

&lt;p&gt;If I had to pick one thing, it's hands-on time in a real cluster. Reading about a topic feels productive, but doing it over and over is what actually sticks. Minikube is fine for quick practice, but I built my own control plane and worker and secured them myself, which gave me a lot more to poke at. Nothing teaches hardening like hardening a cluster you built yourself.&lt;/p&gt;

&lt;p&gt;Everything else supported that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Video courses (KodeKloud, Udemy)&lt;/strong&gt; for the lectures. Great for the big picture and for giving your study plan some structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My own notes&lt;/strong&gt; to make it stick. After each lesson I summarized what I'd learned in my own words and sketched the architecture, so I understood how the pieces fit together. Any gap in the notes pointed straight at a gap in my understanding, and the notes became my go-to reference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practice exams (Killercoda, Killer.sh)&lt;/strong&gt; for timing. They're the closest thing to the real exam, both in difficulty and in time pressure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons from the retakes
&lt;/h2&gt;

&lt;p&gt;The CKS is genuinely hard, and a light understanding won't get you through it. It didn't get me through, either: it took me a few attempts to pass. I treated each one as feedback, not failure. Every attempt showed me where my studying was off and which skills had gaps, so I could sharpen exactly those before the next try.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pacing.&lt;/strong&gt; Remember that twenty-minute trap? Avoiding it was lesson one. By the end, I treated the clock as part of the exam: a time budget for every task, quick wins first, flag anything that stalls, and come back to it at the end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How layered it is.&lt;/strong&gt; One task can touch Kubernetes, the Linux host and the container runtime all at once. Kubernetes fundamentals are the base, but securing a cluster means working underneath it too. My gaps were in Linux and containers, so that's where I spent my time between retakes. It paid off: when a task stalls, those basics are what keep analysis paralysis away. If a service won't come up, I know to grep its logs with &lt;code&gt;journalctl&lt;/code&gt;. If I need a container ID, &lt;code&gt;crictl ps&lt;/code&gt; (or &lt;code&gt;docker ps&lt;/code&gt;, depending on the node) gets me there. Knowing my next move takes a lot of the anxiety out of the clock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Knowing which tool to reach for.&lt;/strong&gt; This was the big one. It stopped being about memorizing commands and became about how to approach a problem: where to look first and which tool fits the job. That changed how I used the docs, too. Instead of searching around for answers, I knew exactly what to look for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Passing felt great, but I didn't want everything I learned to just sit in my notes. So to keep building on it, I'm hardening my own lab cluster, built from scratch on AWS. The notes and code are written for AWS, but the Kubernetes side carries over to any cloud provider (or your own hardware) with a few adjustments. From there, I'll work through hands-on examples for each exam domain, one post every Tuesday. First the build and a kube-bench baseline, then a domain at a time, and at the end a capstone where I try to break in and see if I get caught.&lt;/p&gt;

&lt;p&gt;This series is also my way of keeping a record of the work, with every config, script and lesson learned in one place.&lt;/p&gt;

&lt;p&gt;The schedule is in the repo, and the code lands there with each post: &lt;a href="https://github.com/sean-mancho/k8s-hardening-lab" rel="noopener noreferrer"&gt;github.com/sean-mancho/k8s-hardening-lab&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Honorable mentions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://kubernetes.io/docs/home/" rel="noopener noreferrer"&gt;The Kubernetes docs&lt;/a&gt;.&lt;/strong&gt; My home base, both while studying and during the exam. The concept pages explain the &lt;em&gt;why&lt;/em&gt; behind each feature really thoroughly, and the task pages are full of YAML manifests you can copy as a starting template. That made them my quickest reference when I needed a NetworkPolicy or a SecurityContext in a hurry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/kelseyhightower/kubernetes-the-hard-way" rel="noopener noreferrer"&gt;Kubernetes the Hard Way&lt;/a&gt; by Kelsey Hightower.&lt;/strong&gt; It really makes you think about how a cluster fits together. There's no kubeadm or bootstrapping tool doing the work for you, so you set up every piece yourself: the certificates, etcd, the control plane and the kubelets. Once you've wired it up by hand, it's much clearer what kubeadm automates and where things can go wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See you next Tuesday, when we build the lab.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>security</category>
      <category>certification</category>
      <category>career</category>
    </item>
  </channel>
</rss>
