OneLinersCommand workbench
Guides
Containers & Kubernetes / Networking & DNS / Security

Set up a Kubernetes cluster on Ubuntu with kubeadm

Build a single-control-plane Kubernetes cluster with containerd, an explicit Pod network, a joined worker, verification, and a complete reset path.

90 min10 stepsChanges system stateRevision 2
Save or explore
Save to collectionCreate a collection in the sidebar first.
0 of 10 steps completed
Goal

Create a reproducible Kubernetes learning or small self-managed cluster without hiding the host, network, runtime, or recovery requirements.

Supported environments
  • Ubuntu Server 24.04 LTS
  • Kubernetes 1.36.x
  • containerd 1.7+, 2.x
Prerequisites
  • Recovery access Keep console access to every node before changing swap, kernel networking, or the container runtime.
  • Node capacity Provide at least 2 CPUs and 2 GiB RAM for the control plane, plus one or more Linux workers.nproc && free --gibi
  • Network plan Choose stable node addresses and a Pod CIDR that does not overlap host, VPN, or site networks.ip route show
  • Clean hosts Use nodes that are not already running another Kubernetes distribution or a conflicting CNI.
Operating boundary

OneLiners never runs these steps or stores secrets. Review placeholders, versions, current state, and change-control requirements before using a command.

Full guide

What you will build

System
  • A two-node Kubernetes 1.36 learning or small self-managed cluster on Ubuntu 24.04 with one control plane, one worker, containerd, and a single pinned Flannel CNI release.
  • Hosts with deliberate swap, kernel forwarding, cgroup, package-version, port, route, and Pod CIDR decisions instead of hidden installer defaults.
  • A protected cluster-admin kubeconfig, short-lived bootstrap join material, readiness evidence, a workload and DNS smoke test, and a rehearsable teardown path.
Observable outcome
  • kubeadm preflight succeeds on every node, containerd and kubelet use compatible systemd cgroups, and packages are held at an intentional supported minor release.
  • The API server is reachable only through the planned control-plane endpoint, one CNI owns Pod networking, CoreDNS becomes Running, and every intended node reports Ready.
  • A disposable multi-node workload resolves a ClusterIP service and exchanges traffic before the cluster is accepted; credentials, reset limitations, upgrades, backup, and high-availability gaps are documented.

Architecture

How the parts fit together

kubeadm writes static control-plane Pod manifests and a local stacked etcd member on the control-plane node. Kubelet asks containerd through CRI to run Pods, Flannel supplies one CNI network over the non-overlapping 10.244.0.0/16 Pod CIDR, and CoreDNS supplies service discovery. Workers bootstrap through a short-lived token and CA pin, while administrators use a tightly protected kubeconfig. This design is intentionally not highly available.

Control-plane hostRuns kube-apiserver, scheduler, controller manager, stacked etcd, kubelet, containerd, and CNI components.
Worker hostRuns kubelet, containerd, kube-proxy, CNI, and scheduled application Pods.
containerdImplements the CRI runtime with a systemd cgroup hierarchy compatible with kubelet.
kubeadmPerforms preflight, certificate bootstrap, control-plane initialization, node join, upgrade coordination, and best-effort reset.
FlannelProvides the selected single Pod network for the declared 10.244.0.0/16 range.
Cluster credentialsSeparate bootstrap-token node enrollment from long-lived cluster-admin kubeconfig authorization.
  1. Inventory every node and prove unique host identity, stable routing, required ports, DNS/time, capacity, cgroup v2, and non-overlapping networks.
  2. Disable swap under an explicit policy, load bridge/overlay modules, enable forwarding, and configure containerd consistently.
  3. Install kubeadm, kubelet, and kubectl from the repository for exactly one Kubernetes minor release, then hold them.
  4. Initialize the single control plane with the agreed Pod CIDR and CRI socket, protect generated credentials, and install exactly one pinned CNI manifest.
  5. Wait for control-plane networking and CoreDNS, generate a fresh time-limited join command, and enroll one prepared worker at a time.
  6. Prove node readiness, system Pods, scheduling, service discovery, and traffic; then record backup, upgrade, capacity, monitoring, and recovery obligations.

Assumptions

  • The nodes are dedicated Ubuntu Server 24.04 LTS hosts with unique hostnames, machine IDs, MAC addresses, and stable default-route addresses.
  • The control-plane endpoint and every Kubernetes/Flannel port are allowed only between the intended nodes and administrator networks; the API is not casually exposed to the internet.
  • Host, VPN, service, and Pod networks do not overlap. The example reserves 10.244.0.0/16 for Pods and must be changed consistently when that range conflicts.
  • The hosts use cgroup v2, which Kubernetes 1.36 expects by default, and containerd is the only CRI runtime or its socket is selected explicitly.
  • Nodes can reach signed Ubuntu and pkgs.k8s.io repositories plus approved image registries, or an independently designed air-gapped mirror is available.
  • A single control plane is accepted as a failure domain for learning or small noncritical use; production availability needs multiple control-plane nodes, a stable endpoint, and tested etcd recovery.

Key concepts

CRI
The Kubernetes Container Runtime Interface used by kubelet to manage Pod sandboxes and containers through containerd.
CNI
The Container Network Interface implementation that assigns Pod addresses and connects Pod traffic; a cluster uses one primary Pod network.
Pod CIDR
An address range reserved for Pods that must not overlap node, service, VPN, or routed site networks.
cgroup driver
The resource-control manager used by kubelet and the runtime; on systemd hosts both should use systemd semantics.
Bootstrap token
A short-lived secret used with CA public-key pinning to authenticate a node during kubeadm join.
Static Pod
A node-local Pod definition watched directly by kubelet; kubeadm places control-plane manifests under /etc/kubernetes/manifests.
Node Ready
A condition indicating kubelet reports healthy node status; it does not by itself prove application networking or durable control-plane recovery.

Before you copy

Values used in this guide

{{controlPlaneAddress}}

Stable node address advertised by the control-plane API on the cluster network.

Example: 10.20.0.10
{{bootstrapToken}}secret

Short-lived kubeadm discovery and TLS bootstrap token generated immediately before joining a worker.

Example: abcdef.0123456789abcdef
{{caHash}}

SHA-256 hash pin of the cluster CA public key used by discovery.

Example: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

Security and production boundaries

  • `/etc/kubernetes/admin.conf` grants cluster-admin; `/etc/kubernetes/super-admin.conf` bypasses normal authorization through system:masters. Never publish either file.
  • Bootstrap tokens are bearer-like enrollment credentials. Generate them only when needed, limit lifetime, transmit through approved channels, and delete unused tokens.
  • Restrict TCP/6443 to administrator and node networks. Also account for kubelet TCP/10250, control-plane ports, NodePort policy, and the selected CNI's traffic.
  • Do not pipe mutable remote scripts into a shell. Use signed package repositories and a pinned, reviewed CNI release manifest whose provenance is recorded.
  • Kubernetes RBAC, NetworkPolicy support, Pod security controls, secrets encryption, audit logging, image policy, backups, and workload hardening remain separate production controls.
  • A node is a privileged trust boundary. Root compromise of a control-plane node exposes cluster credentials and potentially etcd data.

Stop before continuing if

  • Stop when any hostname, machine ID, MAC, route, clock, runtime endpoint, or Pod CIDR conflicts between nodes.
  • Do not initialize until required ports are restricted correctly and the chosen CNI version supports the Kubernetes release and Pod CIDR.
  • Do not rerun kubeadm init after a failure without understanding the phase and using documented reset or recovery behavior.
  • Do not join workers while CoreDNS or the CNI remains unhealthy; multiplying nodes obscures the original network fault.
  • Do not accept the cluster when nodes are merely Ready but scheduling, service DNS, cross-node traffic, backup, monitoring, or upgrade planning is unproven.
  • Do not use this single-control-plane topology for a workload whose availability or data durability requires control-plane redundancy.
01

verification

Inspect every node before changing it

read-only

Confirm the OS, CPU, memory, route, unique hostname, and current swap state on the control plane and every worker.

Why this step matters

A complete cross-node inventory prevents bootstrap from embedding duplicate identity, wrong routes, insufficient capacity, clock skew, or overlapping address ranges into cluster state.

What to understand

Record OS, kernel, cgroup version, hostname, machine-id, MAC addresses, CPU, memory, swap, routes, DNS, time synchronization, listeners, and firewall state on every node.

Verify bidirectional node connectivity and the official port matrix: API 6443, kubelet 10250, control-plane 2379-2380/10257/10259, plus CNI-specific traffic.

Compare the proposed 10.244.0.0/16 Pod range with every host, service, VPN, cloud, and site route. An overlap can produce intermittent failures that look like CNI bugs.

System changes

  • No intended change; reads hardware, identity, network, swap, cgroup, time, and service state on every node.

Syntax explained

hostnamectl
Shows the node hostname, operating system, kernel, and architecture.
nproc / free --gibi
Reports allocatable CPU count and memory in practical units.
ip route show
Displays route selection used for node address auto-detection.
swapon --show
Enumerates currently active swap devices.
Command
hostnamectl && nproc && free --gibi && ip route show && swapon --show
Example output / evidence
Operating System: Ubuntu 24.04 LTS
4
Mem: 7.7Gi
Default via 10.20.0.1 dev ens18
NAME TYPE SIZE USED PRIO

Checkpoint: Approve every node

hostnamectl && systemd-detect-virt && stat -fc %T /sys/fs/cgroup && ip route && swapon --show

Continue whenEach node is unique, uses cgroup2fs, has stable intended routes, adequate capacity, synchronized time, and no conflicting CIDR.

Stop whenIdentity duplicates, cgroup v1, route ambiguity, clock skew, insufficient capacity, active unplanned services, or address overlap exists.

If this step fails

Two cloned nodes have the same machine-id.

Likely causeA VM image was cloned without regenerating host identity.

Safe checks
  • cat /etc/machine-id
  • ip -brief link

ResolutionRegenerate identity using the Ubuntu image process before Kubernetes installation; never join duplicate nodes.

Security notes

  • Keep control-plane and kubelet ports off public networks; document every permitted source and direction.

Alternatives

  • Generate this inventory through reviewed configuration management when it captures the same per-node evidence.

Stop conditions

  • Do not alter any node until the complete topology and recovery access are approved.
02

command

Disable swap and preserve the prior fstab

caution

The default kubeadm preflight rejects active swap. Save the original file before disabling current and boot-time swap.

Why this step matters

The selected kubeadm 1.36 baseline rejects active swap, so preserving and explicitly disabling every swap source makes kubelet resource behavior predictable and rollback possible.

What to understand

The command archives fstab before editing and disables current swap. Inspect zram, cloud-init, and systemd-generated swap separately.

The sed expression comments fstab swap rows with an audit marker; review the diff before reboot rather than assuming every row matched correctly.

Reboot a non-control-plane host during validation and confirm swap remains absent before repeating the change across nodes.

System changes

  • Archives `/etc/fstab`, disables active swap, and comments boot-time swap entries.

Syntax explained

cp --archive
Preserves the original fstab metadata for exact rollback.
swapoff --all
Disables all currently active swap devices.
sed ... swap
Comments fstab rows whose filesystem type is swap and labels the change.
Command
sudo cp --archive /etc/fstab /etc/fstab.before-kubernetes && sudo swapoff --all && sudo sed -i '/[[:space:]]swap[[:space:]]/ s/^/# oneliners: /' /etc/fstab
Example output / evidence
swapon --show returns no rows

Checkpoint: Prove swap remains disabled

swapon --show && grep -nE '^[^#].*[[:space:]]swap[[:space:]]' /etc/fstab || true

Continue whenNeither command reports an active or uncommented boot-time swap entry.

Stop whenSwap remains active, memory headroom is unsafe, fstab diff affects non-swap mounts, or another generator will recreate swap.

If this step fails

Swap returns after reboot.

Likely causezram, cloud-init, a systemd unit, or an unmodified fstab entry recreates it.

Safe checks
  • swapon --show
  • systemctl list-units --type=swap
  • systemctl list-unit-files | grep -i zram

ResolutionDisable the actual provider under the approved policy and repeat a reboot check before kubeadm.

Security notes

  • Disabling swap can expose memory pressure; capacity and eviction design must replace it, not denial of the risk.

Alternatives

  • Use Kubernetes swap support only through a separately validated kubelet policy supported by the chosen release.

Stop conditions

  • Stop if node capacity cannot safely operate without swap or the prior fstab cannot be restored.
03

config

Load bridge networking and forwarding settings

caution

Persist the kernel modules and sysctls required for bridged Pod traffic, then verify the applied values.

Why this step matters

Overlay networking and bridged packet visibility depend on persistent kernel modules and forwarding values that must survive reboot consistently across every node.

What to understand

overlay supports common container snapshotters; br_netfilter exposes bridged IPv4/IPv6 traffic to netfilter hooks used by cluster networking.

IPv4 forwarding permits routed Pod traffic. The bridge sysctls must exist only after br_netfilter is loaded.

Review existing sysctl ownership to avoid two automation systems fighting over the same keys.

System changes

  • Creates module-load and sysctl drop-ins, loads two modules, enables bridge netfilter hooks, and enables IPv4 forwarding.

Syntax explained

/etc/modules-load.d/kubernetes.conf
Requests overlay and br_netfilter on future boots.
modprobe
Loads the modules immediately for current validation.
sysctl --system
Reloads system sysctl configuration in precedence order.
net.ipv4.ip_forward=1
Allows the host to route IPv4 traffic between interfaces.
File /etc/sysctl.d/99-kubernetes-cri.conf
Configuration
printf 'overlay\nbr_netfilter\n' | sudo tee /etc/modules-load.d/kubernetes.conf && sudo modprobe overlay && sudo modprobe br_netfilter && printf 'net.bridge.bridge-nf-call-iptables = 1\nnet.bridge.bridge-nf-call-ip6tables = 1\nnet.ipv4.ip_forward = 1\n' | sudo tee /etc/sysctl.d/99-kubernetes-cri.conf && sudo sysctl --system
Example output / evidence
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1

Checkpoint: Verify networking prerequisites

lsmod | grep -E '^(overlay|br_netfilter)' && sysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables net.ipv4.ip_forward

Continue whenBoth modules are loaded and all three values equal 1 on every node.

Stop whenA value is overridden, a module is unavailable, or host firewall policy does not account for forwarded Pod traffic.

If this step fails

The bridge sysctl path does not exist.

Likely causebr_netfilter failed to load or the running kernel lacks the module.

Safe checks
  • sudo modprobe br_netfilter
  • dmesg | tail -n 50

ResolutionInstall or boot a supported Ubuntu kernel, load the module, then apply and verify the sysctls.

Security notes

  • Forwarding expands the host data path; enforce node and CNI policy instead of assuming default-deny input covers forwarding.

Alternatives

  • Use the exact prerequisites documented by another selected CNI when it does not use bridged overlay behavior.

Stop conditions

  • Do not bootstrap with inconsistent module or forwarding state across nodes.
04

command

Install and configure containerd

caution

Generate a known runtime configuration, select the systemd cgroup driver, and verify the service before installing Kubernetes.

Why this step matters

Kubelet and containerd must agree on CRI availability and the systemd cgroup hierarchy before cluster certificates and node state are created.

What to understand

Generate a complete packaged default configuration instead of maintaining an undocumented fragment, then change only the cgroup setting.

Kubernetes 1.36 expects cgroup v2 by default; containerd and kubelet should use systemd semantics on Ubuntu.

Verify the service, socket, effective config, and CRI response. A running process alone does not prove a usable CRI plugin.

System changes

  • Installs containerd, writes `/etc/containerd/config.toml`, selects SystemdCgroup, and restarts the runtime.

Syntax explained

containerd config default
Emits a complete default configuration for the installed runtime version.
SystemdCgroup = true
Delegates container cgroup lifecycle through systemd.
systemctl restart containerd
Applies the generated runtime configuration.
systemctl is-active
Returns success only when systemd considers the runtime active.
Command
sudo apt update && sudo apt install --yes containerd && sudo install -d -m 0755 /etc/containerd && containerd config default | sudo tee /etc/containerd/config.toml >/dev/null && sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml && sudo systemctl restart containerd && systemctl is-active containerd
Example output / evidence
active

Checkpoint: Prove CRI and cgroups

systemctl is-active containerd && sudo ctr version && sudo grep -n 'SystemdCgroup = true' /etc/containerd/config.toml

Continue whencontainerd is active, client/server versions respond, and the effective runtime stanza uses SystemdCgroup.

Stop whenCRI is disabled, the socket is missing, runtime logs show errors, or cgroup mode differs.

If this step fails

containerd starts but kubeadm cannot find a CRI endpoint.

Likely causeThe CRI plugin is disabled, configuration schema differs, or multiple runtime sockets confuse detection.

Safe checks
  • sudo ctr plugins ls | grep cri
  • sudo journalctl -u containerd --since '10 minutes ago' --no-pager
  • sudo ls -l /run/containerd/containerd.sock

ResolutionRestore a valid version-appropriate config with CRI enabled and pass the explicit containerd socket to kubeadm.

Security notes

  • Restrict write access to runtime configuration and socket; membership or root access to the runtime is host-equivalent privilege.

Alternatives

  • Use another Kubernetes-supported CRI runtime with its official systemd-cgroup configuration.

Stop conditions

  • Do not install cluster state until CRI health and cgroup agreement are proven.
05

command

Install kubelet, kubeadm, and kubectl

caution

Use the versioned Kubernetes package repository, then hold the node packages so ordinary upgrades cannot introduce unsupported version skew.

Why this step matters

A repository scoped to one Kubernetes minor and explicit package holds prevent ordinary host upgrades from creating unsupported control-plane, kubelet, and kubeadm version skew.

What to understand

pkgs.k8s.io repositories are minor-version-specific. A future minor upgrade requires repository and lifecycle changes, not a casual package update.

Install the repository signing key into a dedicated keyring and reference it with signed-by; never use apt-key or disable verification.

Inspect package candidates and installed client versions, then hold all three packages until a documented kubeadm upgrade procedure unholds them.

System changes

  • Adds the Kubernetes 1.36 APT keyring and repository, installs kubelet/kubeadm/kubectl, and marks them held.

Syntax explained

curl -fsSL
Fails on HTTP errors while retrieving the repository signing key over TLS.
gpg --dearmor
Converts the armored public key into an APT keyring file.
signed-by=
Scopes repository trust to the dedicated Kubernetes keyring.
apt-mark hold
Prevents unattended package movement outside the cluster upgrade sequence.
Command
sudo install -m 0755 -d /etc/apt/keyrings && curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.36/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg && echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.36/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list && sudo apt update && sudo apt install --yes kubelet kubeadm kubectl && sudo apt-mark hold kubelet kubeadm kubectl
Example output / evidence
kubelet set on hold.
kubeadm set on hold.
kubectl set on hold.

Checkpoint: Pin the intended release

kubeadm version -o short && kubectl version --client && apt-mark showhold | grep -E '^(kubeadm|kubelet|kubectl)$'

Continue whenTools report v1.36.x and all three package names appear in the hold list.

Stop whenRepository signature fails, candidates span the wrong minor, versions differ unexpectedly, or packages are not held.

If this step fails

APT has no kubeadm candidate.

Likely causeThe repository URL, architecture, keyring, DNS, or minor path is wrong.

Safe checks
  • apt-cache policy kubeadm
  • sudo apt update

ResolutionCorrect the official v1.36 repository definition and keyring; do not fall back to the frozen legacy repository.

Security notes

  • Package holds require an explicit patch cadence; forgotten holds leave security fixes unapplied.

Alternatives

  • Mirror the same signed packages internally when supply-chain and availability policy requires it.

Stop conditions

  • Do not mix Kubernetes minor releases or continue after repository verification errors.
06

warning

Initialize the control plane

caution

Run this only on the intended control-plane node. The Pod CIDR must match the selected CNI and must not overlap any existing network.

Why this step matters

Control-plane initialization creates PKI, stacked etcd, static Pods, kubelet configuration, and cluster state, so network and recovery decisions must be final first.

What to understand

Run only on the intended control-plane node. The 10.244.0.0/16 value is coupled to the Flannel manifest and must not overlap another route.

kubeadm performs preflight and then writes sensitive files under `/etc/kubernetes` plus etcd state under `/var/lib/etcd`.

Capture output securely because it includes a bootstrap join command; protect super-admin.conf separately as break-glass material.

System changes

  • Creates cluster PKI, kubeconfigs, static control-plane manifests, stacked etcd state, kubelet bootstrap state, and a bootstrap token.

Syntax explained

kubeadm init
Runs preflight and initializes a new control plane on this node.
--pod-network-cidr=10.244.0.0/16
Declares the Pod address pool expected by the selected Flannel configuration.
Command
sudo kubeadm init --pod-network-cidr=10.244.0.0/16
Example output / evidence
Your Kubernetes control-plane has initialized successfully!
You should now deploy a Pod network to the cluster.

Checkpoint: Accept control-plane initialization

sudo KUBECONFIG=/etc/kubernetes/admin.conf kubectl get --raw=/readyz?verbose

Continue whenkubeadm reports success and the local authenticated API readiness checks pass.

Stop whenAny preflight is ignored without justification, init fails mid-phase, API readiness fails, or the Pod CIDR is wrong.

If this step fails

kubeadm init times out waiting for the control plane.

Likely causeKubelet, containerd, cgroups, image pulls, node address selection, or required ports prevent static Pods from becoming healthy.

Safe checks
  • sudo crictl --runtime-endpoint unix:///run/containerd/containerd.sock ps -a
  • sudo journalctl -u kubelet --since '15 minutes ago' --no-pager
  • sudo ss -lntp | grep 6443

ResolutionDiagnose the first failing component; preserve evidence and use documented reset only when abandoning the partial cluster.

Security notes

  • Never post init output, admin.conf, super-admin.conf, certificates, or bootstrap tokens in public logs.

Alternatives

  • Use a reviewed kubeadm configuration file when stable endpoints, certificate SANs, dual stack, or reproducible advanced settings are required.

Stop conditions

  • Do not rerun init over partial state or bypass preflight errors to force success.
07

command

Configure kubectl for the administrator

caution

Copy the generated admin kubeconfig with restrictive ownership. Treat this file as a cluster-admin credential and never publish it.

Why this step matters

Copying admin.conf with owner-only permissions enables administration without running kubectl as root while recognizing that the credential grants complete cluster control.

What to understand

The copied certificate belongs to kubeadm:cluster-admins and is bound to cluster-admin; use it for commissioning, not broad distribution.

Keep super-admin.conf offline as tightly controlled break-glass material because system:masters bypasses normal authorization.

Verify context, API endpoint, file mode, and authenticated identity before applying the CNI.

System changes

  • Creates `$HOME/.kube/config` containing a cluster-admin client certificate and endpoint configuration.

Syntax explained

cp -i
Prompts before replacing an existing kubeconfig.
chown $(id -u):$(id -g)
Assigns the current administrator ownership.
chmod 600
Restricts kubeconfig reading and modification to its owner.
Command
mkdir -p "$HOME/.kube" && sudo cp -i /etc/kubernetes/admin.conf "$HOME/.kube/config" && sudo chown "$(id -u):$(id -g)" "$HOME/.kube/config" && chmod 600 "$HOME/.kube/config"
Example output / evidence
kubectl config current-context
kubernetes-admin@kubernetes

Checkpoint: Protect cluster-admin access

stat -c '%a %U:%G %n' "$HOME/.kube/config" && kubectl auth whoami && kubectl config view --minify

Continue whenMode is 600, the expected administrator identity appears, and the endpoint matches the intended API.

Stop whenAn existing context was overwritten unexpectedly, permissions are broad, identity is wrong, or endpoint is public/unplanned.

If this step fails

kubectl reports connection refused.

Likely causeThe context endpoint is wrong, API static Pod is unhealthy, firewall blocks it, or KUBECONFIG selects another file.

Safe checks
  • kubectl config view --minify
  • sudo ss -lntp | grep 6443
  • printf '%s\n' "$KUBECONFIG"

ResolutionSelect the intended config and repair API health or network policy; do not disable TLS verification.

Security notes

  • Never store admin.conf in source control, shared home directories, images, or unrestricted backup artifacts.

Alternatives

  • Generate individual kubeconfigs and bind least-privilege RBAC after commissioning.

Stop conditions

  • Do not apply cluster resources through an unverified context or identity.
08

command

Install one Pod network

caution

Apply the pinned Flannel v0.28.4 manifest for the chosen 10.244.0.0/16 Pod CIDR. Do not install a second CNI on the same cluster.

Why this step matters

Installing one pinned CNI release completes Pod networking and allows CoreDNS to run without trusting a mutable latest URL or layering competing datapaths.

What to understand

The manifest is pinned to Flannel v0.28.4. Download, inspect, and archive the exact manifest in controlled environments before applying it.

The manifest expects 10.244.0.0/16 unless reviewed and changed consistently with kubeadm configuration.

Observe the kube-flannel namespace, DaemonSet rollout, node routes, CoreDNS, and image pulls before joining workers.

System changes

  • Creates the kube-flannel namespace, RBAC, configuration, service account, and node DaemonSet for the pinned CNI release.

Syntax explained

kubectl apply -f
Reconciles the objects declared in the selected manifest.
v0.28.4 URL
Pins review and deployment to a specific upstream Flannel release.
10.244.0.0/16
Must match both kubeadm cluster configuration and the CNI network configuration.
Command
kubectl apply -f https://github.com/flannel-io/flannel/releases/download/v0.28.4/kube-flannel.yml
Example output / evidence
namespace/kube-flannel created
daemonset.apps/kube-flannel-ds created

Checkpoint: Prove one healthy CNI

kubectl rollout status daemonset/kube-flannel-ds -n kube-flannel --timeout=180s && kubectl wait -n kube-system --for=condition=Ready pod -l k8s-app=kube-dns --timeout=180s

Continue whenFlannel is available on the control-plane node and every CoreDNS Pod becomes Ready.

Stop whenThe manifest source/version is unverified, another CNI exists, DaemonSet fails, or CoreDNS remains unhealthy.

If this step fails

Flannel Pod enters CrashLoopBackOff.

Likely causePod CIDR mismatch, missing kernel prerequisites, inaccessible image, interface selection, firewall, or stale prior CNI state.

Safe checks
  • kubectl logs -n kube-flannel daemonset/kube-flannel-ds --all-containers --tail=100
  • kubectl describe pod -n kube-flannel -l app=flannel
  • ip route show

ResolutionCorrect the single root cause and restart the rollout; do not install another CNI beside it.

Security notes

  • Archive the reviewed manifest and monitor CNI security releases; Flannel alone does not enforce NetworkPolicy.

Alternatives

  • Select a supported NetworkPolicy-capable CNI before initialization when policy enforcement is required.

Stop conditions

  • Do not join workers until CNI and CoreDNS are healthy on the control plane.
09

command

Join each worker with a fresh token

caution

Run the generated join command on one prepared worker at a time. The token is a credential; generate a new command if it has expired and do not store it in shell history longer than necessary.

Why this step matters

A fresh short-lived token and pinned CA hash enroll exactly one prepared worker without disabling discovery verification or reusing stale bootstrap credentials.

What to understand

Generate `kubeadm token create --print-join-command --ttl` immediately before enrollment and transmit it through an approved channel.

Run the command on one worker at a time, confirm its unique identity and expected API address, then observe CSR and node status.

Delete unused tokens after enrollment; the CA hash is not secret, but the bootstrap token is.

System changes

  • Creates worker kubelet PKI, kubeconfig, CNI/runtime state, Node registration, and bootstrap authentication records.

Syntax explained

kubeadm join host:6443
Bootstraps this host against the selected API endpoint.
--token
Supplies the time-limited discovery and TLS bootstrap secret.
--discovery-token-ca-cert-hash sha256:
Pins the cluster CA public key and prevents blind trust of an endpoint.
Command
Fill variables0/3 ready

Values stay on this page and are never sent or saved.

sudo kubeadm join {{controlPlaneAddress}}:6443 --token {{bootstrapToken}} --discovery-token-ca-cert-hash sha256:{{caHash}}
Example output / evidence
This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.

Checkpoint: Accept one worker

kubectl get nodes -o wide && sudo kubeadm token list

Continue whenThe uniquely named worker appears and progresses to Ready; only expected unexpired tokens remain.

Stop whenNode identity is duplicated, the endpoint/hash is unexpected, join partially fails, or a token is exposed.

If this step fails

The token is invalid or expired.

Likely causeIts TTL elapsed, it was deleted, or the token text was copied incorrectly.

Safe checks
  • sudo kubeadm token list
  • kubectl get csr

ResolutionGenerate a new short-lived join command on the control plane and delete abandoned tokens; never use unsafe discovery flags.

Security notes

  • Treat token values as secrets and avoid persistent shell history, tickets, screenshots, and chat transcripts.

Alternatives

  • Use a kubeadm JoinConfiguration file delivered through an approved secret channel for reproducible advanced enrollment.

Stop conditions

  • Do not add another worker until the current join either succeeds cleanly or is reset and explained.
10

verification

Wait for nodes and system Pods

read-only

Do not deploy workloads until every intended node is Ready and CoreDNS plus the CNI are running.

Why this step matters

Node Ready and Running system Pods are necessary but insufficient evidence, so final acceptance must include scheduling, DNS, service endpoints, and cross-node traffic.

What to understand

Inspect node roles, versions, internal addresses, conditions, taints, system Pods, restart counts, and events before deploying a test workload.

Create a disposable multi-replica web workload spread across nodes, expose it internally, and resolve/curl it from a temporary client Pod.

Delete the smoke namespace afterward and confirm all remaining system Pods are healthy.

System changes

  • Reads cluster health; a full acceptance drill may temporarily create and then delete an isolated smoke-test namespace and workloads.

Syntax explained

get nodes -o wide
Shows readiness, role, version, internal address, OS, kernel, and runtime.
get pods --all-namespaces
Inventories system and workload Pod readiness plus restart counts.
--field-selector
Can filter out Running/Succeeded states to focus on unhealthy Pods.
Command
kubectl get nodes -o wide && kubectl get pods --all-namespaces
Example output / evidence
NAME       STATUS   ROLES           VERSION
control-1  Ready    control-plane   v1.36.x
worker-1   Ready    <none>          v1.36.x
kube-system  coredns-...  1/1  Running

Checkpoint: Accept the cluster data path

kubectl wait --for=condition=Ready nodes --all --timeout=180s && kubectl get pods -A -o wide

Continue whenEvery intended node is Ready, system Pods are healthy, and a documented smoke test proves DNS and service traffic.

Stop whenAny node, system Pod, DNS lookup, endpoint, cross-node request, or cleanup step fails.

If this step fails

Nodes are Ready but test Pods remain Pending.

Likely causeControl-plane taints, insufficient worker resources, image pulls, selectors, PVCs, or admission policy prevent scheduling.

Safe checks
  • kubectl describe pod -n smoke
  • kubectl get events -n smoke --sort-by=.lastTimestamp

ResolutionCorrect the explicit scheduler or admission reason; do not remove control-plane isolation merely to hide missing worker capacity.

Security notes

  • Use non-sensitive images and data in smoke tests; avoid granting them host access or unnecessary service-account privileges.

Alternatives

  • Run an organization-owned conformance or synthetic test suite that proves the same control and data paths.

Stop conditions

  • Do not call the cluster ready based only on kubeadm success text.

Finish line

Verification checklist

Node readinesskubectl wait --for=condition=Ready nodes --all --timeout=180sEvery registered node reports condition met.
System Podskubectl get pods --all-namespaces --field-selector=status.phase!=Running,status.phase!=SucceededReturns no unhealthy Pods.
Cluster APIkubectl cluster-infoReports reachable control-plane and CoreDNS endpoints.

Recovery guidance

Common problems and safe checks

kubeadm preflight rejects active swap.

Likely causeA swap partition, file, zram device, or systemd generator remains active or returns after reboot.

Safe checks
  • swapon --show
  • findmnt --types swap
  • grep -nE '[[:space:]]swap[[:space:]]' /etc/fstab

ResolutionIdentify every swap provider, preserve its prior configuration, disable it according to the approved node policy, reboot if needed, and rerun preflight.

kubelet repeatedly restarts before or after initialization.

Likely causeBefore init this may be expected; afterward common causes include swap, cgroup mismatch, an unreachable runtime socket, missing configuration, or certificate failure.

Safe checks
  • systemctl status kubelet --no-pager
  • journalctl -u kubelet --since '15 minutes ago' --no-pager
  • sudo crictl --runtime-endpoint unix:///run/containerd/containerd.sock info

ResolutionUse the first concrete kubelet/runtime error, correct that host prerequisite, and avoid deleting PKI or state speculatively.

CoreDNS stays Pending after kubeadm init.

Likely causeNo CNI is installed yet, the CNI DaemonSet is unhealthy, the Pod CIDR conflicts, required kernel features are missing, or image pulls fail.

Safe checks
  • kubectl get pods -A -o wide
  • kubectl get daemonset -n kube-flannel
  • kubectl describe pod -n kube-system -l k8s-app=kube-dns

ResolutionRepair the single selected CNI and host networking first; do not install a second CNI as a workaround.

A worker cannot join the API server.

Likely causeTCP/6443 routing or filtering fails, the token expired, the CA hash is wrong, time is skewed, or node identity duplicates another host.

Safe checks
  • nc -vz 10.20.0.10 6443
  • sudo kubeadm token list
  • timedatectl status

ResolutionRestore the restricted network path, generate a fresh join command on the control plane, verify the CA pin, and reset only the failed worker if partial state blocks retry.

Nodes are Ready but a test service cannot resolve or communicate across nodes.

Likely causeCoreDNS, kube-proxy, CNI routes, host firewall, MTU, reverse-path filtering, or overlapping networks break the data path.

Safe checks
  • kubectl get pods -A -o wide
  • kubectl get svc,endpointslices -A
  • ip route show && sudo nft list ruleset

ResolutionTrace DNS, service endpoint, and Pod-to-Pod layers separately; correct the owning layer instead of disabling all firewalling.

After the procedure

Alternatives and next steps

Consider these alternatives

  • Use a managed Kubernetes service when control-plane availability, upgrades, etcd recovery, and infrastructure integration should be provider responsibilities.
  • Use Cluster API, Kubespray, or approved configuration management when repeatable multi-node lifecycle automation is required beyond kubeadm's bootstrap scope.
  • Use a different CNI when NetworkPolicy, eBPF datapaths, BGP, encryption, Windows nodes, or observability requirements are not met by Flannel.
  • Use a local single-node distribution only for development when learning kubeadm host and lifecycle behavior is not the objective.

Operate it safely

  • Back up and restore-test application data plus etcd using the topology's supported procedure; copying admin.conf is not a cluster backup.
  • Add metrics, logs, API audit policy, certificate-expiry checks, node-pressure alerts, image/runtime monitoring, and CNI health signals.
  • Define quarterly patch and minor-version upgrades that follow supported skew, control-plane-first sequencing, worker drain, CNI upgrades, and rollback evidence.
  • Create least-privilege administrator and automation credentials instead of distributing admin.conf.
  • Add NetworkPolicy with a CNI that enforces it, Pod security admission, secrets encryption at rest, image provenance policy, resource requests/limits, and namespace boundaries.
  • Move critical workloads to a highly available control plane with a stable endpoint, multiple failure domains, tested etcd recovery, and capacity for node maintenance.

Reference

Frequently asked questions

Is kubeadm a production installer?

It is a supported bootstrap building block, not a complete production platform. Availability, infrastructure, storage, ingress, policy, monitoring, upgrades, and recovery remain operator responsibilities.

Why is CoreDNS Pending before Flannel?

Cluster DNS Pods require a functional Pod network. Pending before CNI can be expected; remaining Pending afterward signals a scheduling or network problem.

Can I install two CNI plugins?

Not as two competing primary Pod networks. Use exactly one supported design unless a documented multi-network architecture deliberately introduces secondary attachments.

Does kubeadm reset restore the original host?

No. Reset is best effort and does not automatically remove every CNI interface, iptables/IPVS rule, kubeconfig, package, sysctl, module, or application volume.

Recovery

Rollback

Reset Kubernetes state on workers first, then the control plane, and restore the host settings only after the cluster is no longer needed.

  1. Drain and delete workload nodes from the control plane when the cluster contains data that must be preserved.
  2. Run sudo kubeadm reset on each worker and then on the control-plane node.
  3. Remove the CNI interfaces and Kubernetes directories identified by kubeadm reset; do not delete unrelated network configuration.
  4. Restore /etc/fstab.before-kubernetes, remove the Kubernetes sysctl and module files, and reboot or re-enable the previous swap policy.
  5. Remove package holds only when intentionally uninstalling or changing Kubernetes versions.

Evidence

Sources and review

Verified 2026-07-24Review due 2026-10-22
Install kubeadmofficialCreate a cluster with kubeadmofficialKubernetes container runtimesofficialFlannel upstream projectofficialKubernetes ports and protocolsofficialkubeadm resetofficial