[{"data":1,"prerenderedAt":519},["ShallowReactive",2],{"\u002Fblog\u002Flabfromzero-2":3,"\u002Fblog\u002Flabfromzero-2-surround":508},{"id":4,"title":5,"authors":6,"badge":7,"body":9,"date":497,"description":498,"extension":499,"image":500,"meta":502,"navigation":503,"path":504,"seo":505,"stem":506,"__hash__":507},"posts\u002F1.blog\u002F4.LabFromZero-2\u002Findex.md","LabFromZero Part 2 - Network, k8s, HSM",null,{"label":8},"Network, HSM, Kubernetes",{"type":10,"value":11,"toc":479},"minimark",[12,17,29,33,36,62,67,119,123,133,140,144,152,160,163,170,200,205,209,218,381,388,391,396,400,403,407,432,439,443,446,457,464,467,470,475],[13,14,16],"h2",{"id":15},"from-network-to-cluster","From Network to Cluster",[18,19,20,21,28],"p",{},"This post condenses the foundational layers that turn bare hardware into a running, trusted Kubernetes cluster. It draws from the ",[22,23,27],"a",{"href":24,"rel":25},"https:\u002F\u002Fcodeberg.org\u002Ftessellative\u002FLabFromZero",[26],"nofollow","LabFromZero"," sections 1.1-networking, 1.2-k8s-nodes, 1.3-k8s-setup, and 1.4-YubiHSM.",[13,30,32],{"id":31},"networking-dns","Networking & DNS",[18,34,35],{},"The lab lives on its own subnet so it can be isolated from the rest of the LAN.",[37,38,39,47,53],"ul",{},[40,41,42,43],"li",{},"External LAN: ",[44,45,46],"code",{},"10.0.0.0\u002F16",[40,48,49,50],{},"Lab network: ",[44,51,52],{},"10.10.0.0\u002F16",[40,54,55,56,61],{},"Connectivity: ",[22,57,60],{"href":58,"rel":59},"https:\u002F\u002Fcodeberg.org\u002Ftessellative\u002FLabFromZero\u002Fsrc\u002Fbranch\u002Fmaster\u002F1.1-networking#routing-configuration",[26],"static routes"," (chosen for simplicity over BGP or pure NAT)",[63,64,66],"h3",{"id":65},"infra-subnet-plan-10100016","Infra subnet plan (10.10.0.0\u002F16)",[68,69,70,83],"table",{},[71,72,73],"thead",{},[74,75,76,80],"tr",{},[77,78,79],"th",{},"Range",[77,81,82],{},"Purpose",[84,85,86,95,103,111],"tbody",{},[74,87,88,92],{},[89,90,91],"td",{},"10.10.0.0\u002F18",[89,93,94],{},"Static \u002F node addresses",[74,96,97,100],{},[89,98,99],{},"10.10.64.0\u002F18",[89,101,102],{},"Static \u002F load-balancer addresses",[74,104,105,108],{},[89,106,107],{},"10.10.128.0\u002F18",[89,109,110],{},"Dynamic hosts \u002F VMs (DHCP)",[74,112,113,116],{},[89,114,115],{},"10.10.192.0\u002F18",[89,117,118],{},"Reserved",[63,120,122],{"id":121},"initial-dns-records-labfromzerointernal","Initial DNS records (labfromzero.internal)",[124,125,130],"pre",{"className":126,"code":128,"language":129},[127],"language-text","control001.labfromzero.internal     A     10.10.0.11\nworker001.labfromzero.internal      A     10.10.0.101\nrouter.labfromzero.internal         A     10.10.0.1\ngateway.labfromzero.internal        A     10.10.64.1\n\nidp.labfromzero.internal            CNAME gateway.labfromzero.internal\nheadlamp.labfromzero.internal       CNAME gateway.labfromzero.internal\ngit.labfromzero.internal            CNAME gateway.labfromzero.internal\ngrafana.labfromzero.internal        CNAME gateway.labfromzero.internal\nprometheus.labfromzero.internal     CNAME gateway.labfromzero.internal\nopenbao.labfromzero.internal        CNAME gateway.labfromzero.internal\nhsm.labfromzero.internal            CNAME gateway.labfromzero.internal\nflux-webhook.labfromzero.internal   CNAME gateway.labfromzero.internal\n# ... additional CNAMEs as services appear\n","text",[44,131,128],{"__ignoreMap":132},"",[18,134,135,139],{},[136,137,138],"strong",{},"IaC note",": These static mappings and the MikroTik route configuration are excellent candidates for Ansible or Terraform \u002F OpenTofu.",[13,141,143],{"id":142},"bare-metal-nodes-luks-tpm2-and-storage","Bare-Metal Nodes – LUKS + TPM2 and Storage",[63,145,147,148,151],{"id":146},"control-node-control001","Control node (",[44,149,150],{},"control001",")",[18,153,154,159],{},[22,155,158],{"href":156,"rel":157},"https:\u002F\u002Fcodeberg.org\u002Ftessellative\u002FLabFromZero\u002Fsrc\u002Fbranch\u002Fmaster\u002F1.2-k8s-nodes\u002FWORKER_NODE.md#disk-layout",[26],"Install Ubuntu LTS 26.04"," with the proper disk layout that uses LVM on LUKS. TPM2 auto-unlock is enrolled with PCR 7 (Secure Boot state).",[18,161,162],{},"After any firmware or dbx update, re-enroll the TPM slot!",[63,164,166,167,151],{"id":165},"worker-node-worker001","Worker node (",[44,168,169],{},"worker001",[37,171,172,180,188],{},[40,173,174,179],{},[22,175,178],{"href":176,"rel":177},"https:\u002F\u002Fcodeberg.org\u002Ftessellative\u002FLabFromZero\u002Fsrc\u002Fbranch\u002Fmaster\u002F1.2-k8s-nodes\u002FWORKER_NODE.md#zfs-raidz-2",[26],"Root filesystem on LUKS"," (same TPM2 pattern).",[40,181,182,183,187],{},"Data pool: 4×2 TB NVMe in ",[22,184,186],{"href":176,"rel":185},[26],"ZFS RAID-Z2 double parity",".",[40,189,190,195,196,199],{},[22,191,194],{"href":192,"rel":193},"https:\u002F\u002Fcodeberg.org\u002Ftessellative\u002FLabFromZero\u002Fsrc\u002Fbranch\u002Fmaster\u002F1.2-k8s-nodes\u002FWORKER_NODE.md#zfs-encryption-for-libvirt-vm-storage",[26],"Optional encrypted ZFS dataset"," for libvirt\u002FVM storage, with the key sealed via ",[44,197,198],{},"systemd-creds"," and a small oneshot unit that loads it at boot.",[18,201,202,204],{},[136,203,138],{},": Node OS installation and the LUKS\u002FTPM enrolment steps are still largely manual. Once the nodes are reachable, Ansible can lock down SSH, install packages, distribute the Root CA, and prepare kubeadm prerequisites.",[13,206,208],{"id":207},"kubernetes-cluster-bootstrap","Kubernetes Cluster Bootstrap",[18,210,211,212,217],{},"On the control node the ",[22,213,216],{"href":214,"rel":215},"https:\u002F\u002Fcodeberg.org\u002Ftessellative\u002FLabFromZero\u002Fsrc\u002Fbranch\u002Fmaster\u002F1.3-k8s-setup#kubeadm-control001",[26],"kubernetes init configuration should look like",":",[124,219,223],{"className":220,"code":221,"language":222,"meta":132,"style":132},"language-sh shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","NODE_IP=\"$(hostname -I | awk '{print $1}')\"\nNODE_NAME=\"$(hostname)\"\ncat > \u002Froot\u002Fkubeadm.yaml \u003C\u003CEOF\napiVersion: kubeadm.k8s.io\u002Fv1beta4\nkind: InitConfiguration\nskipPhases:\n  - addon\u002Fkube-proxy\nlocalAPIEndpoint:\n  advertiseAddress: ${NODE_IP}\n  bindPort: 6443\n---\napiVersion: kubeadm.k8s.io\u002Fv1beta4\nkind: ClusterConfiguration\nnetworking:\n  serviceSubnet: 10.96.0.0\u002F12\n  podSubnet: 10.244.0.0\u002F16\napiServer:\n  certSANs:\n    - ${NODE_NAME}\n    - ${NODE_IP}\n---\napiVersion: kubelet.config.k8s.io\u002Fv1beta1\nkind: KubeletConfiguration\nserverTLSBootstrap: true\nEOF\nkubeadm init --config \u002Froot\u002Fkubeadm.yaml\n","sh",[44,224,225,233,239,245,251,257,263,269,275,281,287,293,298,304,310,316,322,328,334,340,346,351,357,363,369,375],{"__ignoreMap":132},[226,227,230],"span",{"class":228,"line":229},"line",1,[226,231,232],{},"NODE_IP=\"$(hostname -I | awk '{print $1}')\"\n",[226,234,236],{"class":228,"line":235},2,[226,237,238],{},"NODE_NAME=\"$(hostname)\"\n",[226,240,242],{"class":228,"line":241},3,[226,243,244],{},"cat > \u002Froot\u002Fkubeadm.yaml \u003C\u003CEOF\n",[226,246,248],{"class":228,"line":247},4,[226,249,250],{},"apiVersion: kubeadm.k8s.io\u002Fv1beta4\n",[226,252,254],{"class":228,"line":253},5,[226,255,256],{},"kind: InitConfiguration\n",[226,258,260],{"class":228,"line":259},6,[226,261,262],{},"skipPhases:\n",[226,264,266],{"class":228,"line":265},7,[226,267,268],{},"  - addon\u002Fkube-proxy\n",[226,270,272],{"class":228,"line":271},8,[226,273,274],{},"localAPIEndpoint:\n",[226,276,278],{"class":228,"line":277},9,[226,279,280],{},"  advertiseAddress: ${NODE_IP}\n",[226,282,284],{"class":228,"line":283},10,[226,285,286],{},"  bindPort: 6443\n",[226,288,290],{"class":228,"line":289},11,[226,291,292],{},"---\n",[226,294,296],{"class":228,"line":295},12,[226,297,250],{},[226,299,301],{"class":228,"line":300},13,[226,302,303],{},"kind: ClusterConfiguration\n",[226,305,307],{"class":228,"line":306},14,[226,308,309],{},"networking:\n",[226,311,313],{"class":228,"line":312},15,[226,314,315],{},"  serviceSubnet: 10.96.0.0\u002F12\n",[226,317,319],{"class":228,"line":318},16,[226,320,321],{},"  podSubnet: 10.244.0.0\u002F16\n",[226,323,325],{"class":228,"line":324},17,[226,326,327],{},"apiServer:\n",[226,329,331],{"class":228,"line":330},18,[226,332,333],{},"  certSANs:\n",[226,335,337],{"class":228,"line":336},19,[226,338,339],{},"    - ${NODE_NAME}\n",[226,341,343],{"class":228,"line":342},20,[226,344,345],{},"    - ${NODE_IP}\n",[226,347,349],{"class":228,"line":348},21,[226,350,292],{},[226,352,354],{"class":228,"line":353},22,[226,355,356],{},"apiVersion: kubelet.config.k8s.io\u002Fv1beta1\n",[226,358,360],{"class":228,"line":359},23,[226,361,362],{},"kind: KubeletConfiguration\n",[226,364,366],{"class":228,"line":365},24,[226,367,368],{},"serverTLSBootstrap: true\n",[226,370,372],{"class":228,"line":371},25,[226,373,374],{},"EOF\n",[226,376,378],{"class":228,"line":377},26,[226,379,380],{},"kubeadm init --config \u002Froot\u002Fkubeadm.yaml\n",[18,382,383,384,387],{},"Join the worker with the token and CA hash printed by ",[44,385,386],{},"kubeadm init",". Because many platform services run on the control node, the control-plane taint is removed.",[18,389,390],{},"Install Gateway API CRDs and a local-path provisioner so workloads have storage. Later on consider deploying a proper distributed storage solution such as Longhorn, or an external storage appliance like NetApp \u002F TrueNAS Scale \u002F Synology NAS. This Lab environment features a 10 Gbit LAN so a network storage solution is feasible!",[18,392,393,395],{},[136,394,138],{},": The kubeadm configuration and the join process can be templated with Ansible or something like Kubespray can be used for the setup and day-2 operation!",[13,397,399],{"id":398},"yubihsm-and-openbao-auto-unseal","YubiHSM and OpenBao Auto-Unseal",[18,401,402],{},"The YubiHSM 2 provides a hardware root of trust for OpenBao’s seal key.",[63,404,406],{"id":405},"connector-on-the-control-node","Connector on the control node",[37,408,409,412,418,425],{},[40,410,411],{},"Install the YubiHSM SDK connector on the control node",[40,413,414,415],{},"Place the TLS cert and key generated earlier under ",[44,416,417],{},"\u002Fetc\u002Fyubihsm\u002F",[40,419,420,421,424],{},"Configure ",[44,422,423],{},"\u002Fetc\u002Fyubihsm-connector.yaml"," to listen on the lab network and use the certificate",[40,426,427,428,431],{},"Start and enable the ",[44,429,430],{},"yubihsm-connector"," service",[18,433,434],{},[22,435,438],{"href":436,"rel":437},"https:\u002F\u002Fcodeberg.org\u002Ftessellative\u002FLabFromZero\u002Fsrc\u002Fbranch\u002Fmaster\u002F1.4-YubiHSM#installation-and-configuration",[26],"Installation steps in detail",[63,440,442],{"id":441},"hsm-key-setup","HSM key setup",[18,444,445],{},"Using the yubihsm-shell set up the YubiHSM:",[37,447,448,451,454],{},[40,449,450],{},"Create a new admin authentication key and delete the default weak key.",[40,452,453],{},"Create a dedicated OpenBao authentication key (limited capabilities).",[40,455,456],{},"Generate the RSA-4096 seal key that OpenBao will use for auto-unseal.",[18,458,459],{},[22,460,463],{"href":461,"rel":462},"https:\u002F\u002Fcodeberg.org\u002Ftessellative\u002FLabFromZero\u002Fsrc\u002Fbranch\u002Fmaster\u002F1.4-YubiHSM#hsm-setup",[26],"Setup steps in detail",[18,465,466],{},"Store the resulting PINs in KeePassXC and inject the OpenBao PIN into the cluster as a Kubernetes secret.",[18,468,469],{},"Later OpenBao is deployed via GitOps and configured with HSM auto-unseal so the unseal key never exists in software.",[18,471,472,474],{},[136,473,138],{},": The HSM ceremony is deliberately offline\u002Fmanual. The subsequent OpenBao deployment and its HSM configuration belong in GitOps manifests.",[476,477,478],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":132,"searchDepth":235,"depth":235,"links":480},[481,482,486,492,493],{"id":15,"depth":235,"text":16},{"id":31,"depth":235,"text":32,"children":483},[484,485],{"id":65,"depth":241,"text":66},{"id":121,"depth":241,"text":122},{"id":142,"depth":235,"text":143,"children":487},[488,490],{"id":146,"depth":241,"text":489},"Control node (control001)",{"id":165,"depth":241,"text":491},"Worker node (worker001)",{"id":207,"depth":235,"text":208},{"id":398,"depth":235,"text":399,"children":494},[495,496],{"id":405,"depth":241,"text":406},{"id":441,"depth":241,"text":442},"2026-10-10","From network to cluster with HSM","md",{"src":501},"\u002Fblog\u002FLabFromZero\u002Frouter2.webp",{},true,"\u002Fblog\u002Flabfromzero-2",{"title":5,"description":498},"1.blog\u002F4.LabFromZero-2\u002Findex","B1swwJwKNjeKr67s46Ot-KfD791u7VwaQfefobA4_xI",[509,514],{"title":510,"path":511,"stem":512,"description":513,"children":-1},"LabFromZero Part 1 - Hardware, PKI, Security","\u002Fblog\u002Flabfromzero-1","1.blog\u002F3.LabFromZero-1\u002Findex","Foundations of a lab environment",{"title":515,"path":516,"stem":517,"description":518,"children":-1},"LabFromZero Part 3 - Cluster Bootstraping and GitOps","\u002Fblog\u002Flabfromzero-3","1.blog\u002F5.LabFromZero-3\u002Findex","Platform GitOps bootstrapping for the lab environment",1791905103952]