Harden Linux Nodes for Kubernetes

Audit and harden kubeadm Linux nodes by reducing services, securing SSH and Kubernetes files, mapping firewall paths, patching safely, and validating readiness.

Published

Updated

Read time 19 min read

Reviewed byDeepak Prasad

Linux node hardening for Kubernetes with host firewall rules, SSH access controls, and protected kubelet and container runtime paths
Tested on Rocky Linux 10.2 (Red Quartz) — kubeadm control plane and worker nodes
Package kubectl 1.36.3
kubelet 1.36.3
containerd 2.2.5
Cilium CNI (cilium DaemonSet v1.19.6)
cilium CLI v0.19.6
Applies to RHEL, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream, Fedora
Kubernetes cluster
Cert prep CKS
Lab environment Multi-node kubeadm cluster with containerd, Cilium, and SSH node access — install Kubernetes with kubeadm
Privilege root on cluster nodes for package, service, firewall, and permission changes; cluster-admin kubectl from the control plane or a separate admin workstation—not from the worker being drained
Scope Node OS hardening assessment and checklist for kubeadm nodes: inventory, package and service reduction, SSH and sudo, SELinux and swap checks, host firewall paths matched to the CNI, Kubernetes file and runtime socket permissions, and safe patch workflow with cordon and drain. Commands target RHEL-family paths; principles apply on other Linux distributions with different package and firewall tools. Does not cover kubelet HTTPS API configuration, Pod SecurityContext, seccomp or AppArmor profiles, Kubernetes RBAC, admission policy, compliance certification, or a full kernel-hardening guide.
Related guides Kubernetes security architecture

A compromised Linux node can expose every workload on that host, mounted volumes, ServiceAccount tokens, and the container runtime socket. This checklist inventories a kubeadm lab control-plane node and worker running Cilium, walks through tested hardening steps where commands are shown, and gives you inspection points for categories you apply in your own change windows.

Use a separate administration workstation or the control-plane node for kubectl commands. Do not use the worker being drained as the administration host. This lab runs kubectl from k8s-cp; OS commands run over SSH on the node being hardened. Expected durations are noted for steps that take more than a minute.

This lab successfully ran Cilium 1.19.6 with Kubernetes 1.36.3. Kubernetes 1.36 is not listed in the Cilium 1.19.6 guaranteed compatibility matrix, so verify the supported version combination before reproducing this outside the lab.

IMPORTANT
This guide hardens the operating system under Kubernetes—packages, services, SSH, firewall, and file modes. It does not configure kubelet authentication webhooks, Pod securityContext, or RBAC. For kubelet API exposure, see protect node metadata and kubelet endpoints.

Define the node security boundary

The node is more than the kubelet process. Host compromise may expose:

  • Running workloads and their process namespaces
  • Mounted volumes and hostPath data
  • ServiceAccount tokens mounted into Pods
  • The container runtime socket (containerd.sock or equivalent)
  • Kubelet client certificates and kubeconfig files
  • CNI and CSI plugin credentials on disk
  • Cloud instance metadata and node IAM roles
  • The host kernel and any privileged containers

Kubernetes policy limits what a stolen API token can do cluster-wide. A root-equivalent foothold on the node can bypass that boundary by reading files, creating containers directly through the runtime socket, or reading node-local credentials. Node hardening shrinks that blast radius before you tune workload controls.


Inventory the node

Record a baseline on every control-plane and worker node before you change anything. SSH to the node and capture OS identity:

bash
hostname
grep PRETTY_NAME /etc/os-release
uname -r
kubelet --version
containerd --version | head -1
output
k8s-cp
PRETTY_NAME="Rocky Linux 10.2 (Red Quartz)"
6.12.0-211.16.1.el10_2.0.1.x86_64
Kubernetes v1.36.3
containerd containerd.io 2.2.5 1.el10_2

List listening TCP sockets to see what faces the network:

bash
ss -lntp

On the lab control plane, 6443 (API server), 10250 (kubelet), 10256 (kube-proxy health), and 22 (SSH) listen on all interfaces. etcd listens on 192.168.56.108:2379 and 2380. Scheduler and controller-manager ports bind to 127.0.0.1 only—a healthier pattern than wide exposure.

Count installed packages and note compilers you may remove later:

bash
rpm -qa | wc -l
rpm -q gcc make 2>&1
output
693
package gcc is not installed
package make is not installed

Store inventory in a change ticket: OS version, kernel, enabled repositories, sudoers, SSH effective settings, firewall zone, CNI mode, and Kubernetes paths under /etc/kubernetes and /var/lib/kubelet.


Reduce installed packages

Remove software that expands the attack surface without supporting cluster operations:

  • Compilers and build toolchains on production nodes
  • Legacy remote-access agents you no longer use
  • Desktop environments and debug servers
  • Duplicate package repositories pointing at untrusted mirrors

Retain tools your runbooks require—tcpdump for incident response, chrony for clock sync, and the container runtime packages themselves.

On this lab control plane, gcc and make are already absent. Before you run dnf remove on a worker, confirm no host agent or monitoring installer still needs the package. Uninstall in a maintenance window and reboot only when the package list changes kernel modules.


Disable unused services

SSH to the node you are hardening. List running and enabled units:

bash
systemctl --type=service --state=running --no-pager | head -15
bash
systemctl list-unit-files --state=enabled --type=service --no-pager | head -15

On the control plane, expected long-running services include kubelet, containerd, firewalld, chronyd, and sshd. The lab worker also had httpd enabled from other tutorials—a candidate to disable when that node is Kubernetes-only.

Before you systemctl disable --now a service, document why it listens on the network. Ask whether Kubernetes, storage, monitoring, or LDAP still needs it. Disabling firewalld or chronyd on a node causes different failures than removing httpd; validate cluster health after each disable.

On worker01, disable Apache when the node no longer serves HTTP outside the cluster:

bash
systemctl disable --now httpd

Confirm the unit stays off across reboot:

bash
systemctl is-enabled httpd
output
disabled
bash
systemctl is-active httpd
output
inactive

The worker no longer starts httpd at boot. Re-enable only if a workload or runbook still needs it on that host.


Secure administrative access

Inspect effective SSH settings—the file comments may hide defaults:

bash
sshd -T | grep -E 'permitrootlogin|passwordauthentication|pubkeyauthentication'
output
permitrootlogin yes
pubkeyauthentication yes
passwordauthentication yes

This lab still allows root login and password authentication. Production hardening typically moves to:

  • Dedicated admin users in the wheel or sudo group
  • Key-based SSH only (PasswordAuthentication no)
  • PermitRootLogin prohibit-password or no
  • Source restrictions in firewalld rich rules or security-group equivalents
  • Session recording for break-glass accounts

Apply SSH changes through a drop-in under /etc/ssh/sshd_config.d/ so distribution updates do not overwrite your edits. This example caps authentication attempts without touching root login yet:

bash
cat > /etc/ssh/sshd_config.d/99-glc-hardening.conf <<'EOF'
# GoLinuxCloud node hardening example
MaxAuthTries 4
EOF

Validate syntax before reload—a bad file can lock you out on the next restart:

bash
sshd -t && echo "sshd configuration valid"
output
sshd configuration valid

Reload SSH and confirm the effective value:

bash
systemctl reload sshd
bash
sshd -T | grep maxauthtries
output
maxauthtries 4

Change SSH in one step, keep a console session open, and test from a second terminal before you close your path in. Shared root passwords and permanent PermitRootLogin yes fail most security reviews.


Limit sudo access

Separate who may patch the OS, change firewalld, restart kubelet, or run kubectl with cluster-admin credentials.

Example sudo split (illustrative—adapt to your org):

Role Typical rights
Platform patcher dnf update, reboot
Kubernetes admin kubectl, edit /etc/kubernetes on control plane
Runtime troubleshooter crictl, journalctl -u kubelet
Network engineer firewall-cmd, routing changes

List custom sudoers files before you add new ones:

bash
ls -la /etc/sudoers.d/

An empty directory means only the main /etc/sudoers rules apply. Prefer visudo fragments per team instead of handing out unrestricted root.


Inspect SELinux and swap

On RHEL-family nodes, confirm SELinux mode and swap before you treat the host as production-ready.

bash
getenforce
output
Permissive

This node does not yet meet the intended SELinux hardening target. Investigate denials and test Kubernetes and Cilium under Enforcing before marking this category complete.

SELinux should normally stay Enforcing on hardened nodes. Permissive or Disabled modes remove a major containment layer; fix policy violations rather than leaving the host permissive indefinitely.

Check whether swap is active:

bash
swapon --show

An empty result means no swap devices are enabled on this worker. When swap is present, memory-backed Secret data can reach persistent swap storage under unsupported configurations. Do not assume swap is safe or automatically blocked—verify kubelet swap settings and kernel support for your Kubernetes version before you leave swap enabled.


Configure host firewall paths

The Cilium validation steps later in this section use the Cilium CLI (cilium), which is separate from the in-cluster Cilium DaemonSet. Install it on your administration host before you run cilium status or cilium connectivity test—see Cilium CLI installation.

On k8s-cp, confirm the client version matches the lab:

bash
cilium version --client
output
cilium-cli: v0.19.6 compiled with go1.26.4 on linux/amd64
cilium image (default): v1.19.5
cilium image (stable): v1.19.6

Map required ports per node role before you remove rules. The lab uses firewalld on Rocky Linux with Cilium in VXLAN tunnel mode.

The host-firewall procedure must match the installed CNI. Cilium defaults to 8472/udp for VXLAN and 6081/udp for Geneve, but the tunnel port is configurable. Confirm the effective configuration from the cilium-config ConfigMap before you open host ports. A firewall-cmd --reload can disrupt CNI-managed rules; validate the Cilium data plane after every reload on nodes where firewalld stays enabled.

From k8s-cp (or your separate admin workstation), read the tunnel settings:

bash
kubectl -n kube-system get cm cilium-config -o jsonpath='tunnel-protocol={.data.tunnel-protocol}{"\n"}tunnel-port={.data.tunnel-port}{"\n"}'
output
tunnel-protocol=vxlan
tunnel-port=

An empty tunnel-port means Cilium uses the default VXLAN port 8472/udp. If your cluster sets a custom tunnel-port, open that UDP port instead.

Control-plane ingress (this cluster):

Port / protocol Purpose
6443/tcp Kubernetes API
2379-2380/tcp etcd client and peer (restrict to control-plane peers)
10250/tcp kubelet API
10256/tcp kube-proxy health endpoint
30000-32767/tcp NodePort Services (TCP) in the default NodePort range
30000-32767/udp NodePort Services (UDP) in the default NodePort range when used
22/tcp SSH (restrict sources)

Do not list 10257/tcp or 10259/tcp as required network ingress when the controller-manager and scheduler bind to 127.0.0.1 only, as on this lab.

Cilium tunnel ports (open only the mode you use):

Port / protocol When required
8472/udp Cilium VXLAN encapsulation
6081/udp Cilium Geneve encapsulation

Optional Cilium health connectivity:

Port / protocol Purpose
4240/tcp or ICMP Optional inter-node cilium-health checks

Cilium continues operating without these health paths, but connectivity-health reporting becomes incomplete.

Worker ingress (subset):

Port / protocol Purpose
10250/tcp kubelet API from control plane
10256/tcp kube-proxy health endpoint
Cilium tunnel port 8472/udp for VXLAN or 6081/udp for Geneve
30000-32767/tcp Default NodePort range if used
30000-32767/udp Default NodePort UDP range if used
22/tcp SSH (restrict sources)

SSH to each Cilium node and review the active zone before you change ports:

bash
firewall-cmd --list-all
output
public (active)
  services: cockpit dhcpv6-client ntp ssh
  ports: 1313/tcp 6443/tcp 2379-2380/tcp 10250/tcp 10259/tcp 10257/tcp 10256/tcp 30000-32767/tcp 4789/udp 5473/tcp 179/tcp

The zone still lists Calico-era ports (4789, 179, 5473) and localhost-only controller ports from an earlier setup pass. This cluster runs Cilium VXLAN, so add 8472/udp and remove stale entries on every node.

Apply the CNI correction on each node (about one minute per node including reload). Remove 1313/tcp if nothing on the node listens there—this lab had that port left over from an earlier Hugo preview with no current listener:

bash
firewall-cmd --permanent --add-port=8472/udp
firewall-cmd --permanent --remove-port=4789/udp
firewall-cmd --permanent --remove-port=179/tcp
firewall-cmd --permanent --remove-port=5473/tcp
firewall-cmd --permanent --remove-port=10257/tcp
firewall-cmd --permanent --remove-port=10259/tcp
firewall-cmd --permanent --remove-port=1313/tcp
firewall-cmd --permanent --remove-service=cockpit
firewall-cmd --reload
firewall-cmd --list-services
firewall-cmd --list-ports

Control-plane sample after reload:

output
dhcpv6-client ntp ssh
2379-2380/tcp 6443/tcp 10250/tcp 10256/tcp 30000-32767/tcp 8472/udp

A worker should not normally list control-plane-only entries such as 6443/tcp or 2379-2380/tcp. On worker01 after the same CNI correction (this lab also still has LDAP tutorial ports):

output
dhcpv6-client ldap ssh
1636/tcp 3490/tcp 3590/tcp 10250/tcp 10256/tcp 30000-32767/tcp 8472/udp

Removing cockpit from the services list is what proves that service was dropped—--list-ports alone cannot show a removed service.

Node readiness alone does not prove cross-node Pod networking survived a firewall change. From k8s-cp, confirm Cilium is healthy (about 30 seconds):

bash
cilium status --wait

Relevant output:

output
/¯¯\
 /¯¯\__/¯¯\    Cilium:             OK
 \__/¯¯\__/    Operator:           OK
 /¯¯\__/¯¯\    Envoy DaemonSet:    OK
 \__/¯¯\__/    Hubble Relay:       disabled
    \__/       ClusterMesh:        disabled

DaemonSet              cilium                   Desired: 2, Ready: 2/2, Available: 2/2
...
Cluster Pods:          7/7 managed by Cilium
Helm chart version:    1.19.6

Run a basic Pod networking check

Use an isolated namespace so Istio sidecars do not interfere, and clean up synchronously so reruns do not hit name collisions:

bash
kubectl create namespace node-hardening-test
kubectl label namespace node-hardening-test \
  istio-injection=disabled --overwrite
kubectl -n node-hardening-test run net-a --image=nginx:alpine \
  --restart=Never --port=80
kubectl -n node-hardening-test wait --for=condition=Ready pod/net-a \
  --timeout=90s
IP=$(kubectl -n node-hardening-test get pod net-a \
  -o jsonpath='{.status.podIP}')
kubectl -n node-hardening-test run net-b --rm -i --restart=Never \
  --image=busybox:1.36 --timeout=60s -- wget -qO- --timeout=5 \
  http://${IP} | head -2
kubectl delete namespace node-hardening-test --wait=true
output
<!DOCTYPE html>
<html>

This confirms that Pod-IP networking still works on the schedulable worker. It does not validate inter-node VXLAN because both test Pods may run on the same node in a single-worker lab. Cilium's inter-host connectivity suite is what exercises the overlay tunnel between nodes—--single-node intentionally skips those cases.

For deeper Cilium validation, cilium connectivity test deploys a temporary test namespace and runs many scenarios. Expect 8–15 minutes for a full run on a two-node cluster, or 5–8 minutes with --single-node on a single-worker lab. Always pass an explicit suite timeout:

bash
cilium connectivity test --single-node --timeout 8m

If external HTTPS or DNS egress is restricted in your lab, the suite may hang on pod-to-world scenarios—fix cluster DNS upstream forwarders or scope tests with --test filters. A full multi-node run also needs enough schedulable disk on every node; a control plane under disk pressure will evict test Pods.


Restrict outbound access

Inventory destinations nodes must reach before you block egress:

  • API server https://<cp>:6443 from workers
  • DNS resolvers and NTP
  • OS package mirrors or internal patch servers
  • Container image registries (registry.k8s.io, mirrors)
  • Storage backends (NFS, iSCSI, cloud APIs)
  • Monitoring and log collectors

Blanket deny-all egress breaks image pulls and webhook callbacks. Document allowed FQDNs or CIDRs per node role, implement in host firewall or network policy at the hypervisor layer, and watch for ImagePullBackOff or NodeNotReady after each change.


Protect Kubernetes files

Sensitive paths on kubeadm nodes include:

text
/etc/kubernetes/          — admin kubeconfig, PKI, manifests
/var/lib/kubelet/       — kubelet config, pod state
/var/lib/etcd/          — etcd data (control plane)
/etc/cni/net.d/         — CNI configuration
/run/containerd/        — runtime socket

SSH to the node and audit ownership and modes.

Run on every node:

bash
ls -ld /etc/kubernetes /var/lib/kubelet /etc/cni/net.d
ls -l /var/lib/kubelet/config.yaml

Run on control-plane nodes only:

bash
ls -ld /var/lib/etcd
ls -l /etc/kubernetes/admin.conf /etc/kubernetes/pki/ca.key

Control-plane sample:

output
drwxr-xr-x. 5 root root 4096 Jul 23 00:02 /etc/kubernetes
drwxr-xr-x. 10 root root 4096 Jul 28 03:20 /var/lib/kubelet
drwx------. 3 root root 4096 Jul 27 22:21 /var/lib/etcd
-rw-------. 1 root root 5638 /etc/kubernetes/admin.conf
-rw-------. 1 root root 1679 /etc/kubernetes/pki/ca.key
-rw-r--r--. 1 root root 1118 /var/lib/kubelet/config.yaml

admin.conf and ca.key are already 600. The kubelet configuration was world-readable (644)—kube-bench flags that on CIS check 4.1.9.

Tighten the kubelet config mode on the control plane:

bash
chmod 600 /var/lib/kubelet/config.yaml
stat -c '%a %n' /var/lib/kubelet/config.yaml
output
600 /var/lib/kubelet/config.yaml

Confirm kubelet keeps running:

bash
systemctl is-active kubelet
output
active

Repeat on workers. The lab worker already had 600 on /var/lib/kubelet/config.yaml from an earlier CIS remediation pass.


Protect runtime sockets

The container runtime socket grants host-level container control. On this lab:

bash
ls -l /run/containerd/containerd.sock
output
srw-rw----. 1 root root 0 Jul 27 22:10 /run/containerd/containerd.sock

Mode 660 with group root limits access to root-equivalent users. Anyone who can write to the socket can pull images and create privileged containers.

Reduce risk by:

  • Keeping mode 660 or tighter and avoiding broad group membership
  • Denying remote access to CRI endpoints unless encrypted and authenticated
  • Preventing workload users from joining the docker or containerd groups

Socket hardening pairs with node metadata and kubelet endpoint controls at the Kubernetes layer.


Patch nodes safely

OS patches on Kubernetes nodes follow a rolling pattern. From k8s-cp:

bash
kubectl cordon worker01
output
node/worker01 cordoned

Cordon marks the node unschedulable while existing Pods keep running:

bash
kubectl get node worker01
output
NAME       STATUS                     ROLES    AGE    VERSION
worker01   Ready,SchedulingDisabled   <none>   5h8m   v1.36.3

List workloads still bound to the node before you drain:

bash
kubectl get pods -A -o wide --field-selector spec.nodeName=worker01
output
NAMESPACE      NAME                              READY   STATUS    RESTARTS   AGE     IP               NODE
istio-system   istio-cni-node-cntjf              1/1     Running   0          5h48m   192.168.1.67     worker01
istio-system   istiod-68b8c6d945-57sd2           1/1     Running   0          5h50m   192.168.1.140    worker01
kube-system    cilium-envoy-cvm65                1/1     Running   0          7h9m    192.168.56.109   worker01
kube-system    cilium-fdkjr                      1/1     Running   0          5h43m   192.168.56.109   worker01
kube-system    coredns-589f44dc88-bg4dm          1/1     Running   0          5h45m   192.168.1.21     worker01
kube-system    coredns-589f44dc88-dc4j9          1/1     Running   0          5h45m   192.168.1.203    worker01
kube-system    kube-proxy-xnmv9                  1/1     Running   0          14h     192.168.56.109   worker01

Drain evicts workloads before you reboot. The --delete-emptydir-data flag permanently removes Pod emptyDir data—confirm no application still needs that ephemeral storage. PodDisruptionBudgets can block eviction until enough replicas run elsewhere. Do not start OS patching or reboot until kubectl drain exits successfully. Expect 1–5 minutes for drain on a lightly loaded worker; PDB conflicts can extend that.

kubectl drain waits indefinitely by default. Use an explicit --timeout so a PDB-blocked drain exits instead of retrying forever.

First, run a short diagnostic drain to surface the PDB conflict:

bash
kubectl drain worker01 \
  --ignore-daemonsets \
  --delete-emptydir-data \
  --timeout=60s
output
node/worker01 already cordoned
Warning: ignoring DaemonSet-managed Pods: istio-system/istio-cni-node-cntjf, kube-system/cilium-envoy-cvm65, kube-system/cilium-fdkjr, kube-system/kube-proxy-xnmv9
evicting pod istio-system/istiod-68b8c6d945-6k9nl
error when evicting pods/"istiod-68b8c6d945-6k9nl" -n "istio-system" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
...
There are pending pods in node "worker01" when an error occurred: error when evicting pods/"istiod-68b8c6d945-6k9nl" -n "istio-system": global timeout reached: 1m0s
error: unable to drain node "worker01" due to error: ... global timeout reached: 1m0s, continuing command...

The command exits non-zero after the timeout when the PDB remains unsatisfied. That drain did not finish—do not patch or reboot while drain still reports errors.

This single-worker lab cannot demonstrate a PDB-respecting, no-downtime drain unless another schedulable node is available. Add another worker or explicitly accept workload downtime.

To complete drain on this lab, scaling istiod to zero directly removes its Pod and explicitly accepts an Istio control-plane outage; it does not make the eviction satisfy the PDB. Preserve the original replica count before you scale:

bash
ISTIOD_REPLICAS=$(
  kubectl get deployment istiod -n istio-system \
    -o jsonpath='{.spec.replicas}'
)

kubectl scale deployment istiod -n istio-system --replicas=0

Re-run drain with a bounded timeout and confirm it exits successfully:

bash
kubectl drain worker01 \
  --ignore-daemonsets \
  --delete-emptydir-data \
  --timeout=5m
output
node/worker01 already cordoned
Warning: ignoring DaemonSet-managed Pods: istio-system/istio-cni-node-cntjf, kube-system/cilium-envoy-cvm65, kube-system/cilium-fdkjr, kube-system/kube-proxy-xnmv9
node/worker01 drained

Only after node/worker01 drained appears may you patch with dnf upgrade on the node over SSH and reboot if the kernel changed. After reboot, wait for the node to report Ready (up to five minutes).

On worker01 over SSH:

bash
systemctl is-active kubelet containerd
output
active
active

On k8s-cp:

bash
kubectl wait --for=condition=Ready node/worker01 --timeout=5m
output
node/worker01 condition met

Uncordon when the node is healthy:

bash
kubectl uncordon worker01
output
node/worker01 uncordoned

Confirm the node is Ready and schedulable. Uncordoning permits future scheduling but does not automatically rebalance existing Pods back to this node.

bash
kubectl get node worker01
output
NAME       STATUS   ROLES    AGE   VERSION
worker01   Ready    <none>   15h   v1.36.3

Restore scaled-down workloads when maintenance ends:

bash
kubectl scale deployment istiod -n istio-system \
  --replicas="${ISTIOD_REPLICAS}"

kubectl rollout status deployment/istiod \
  -n istio-system --timeout=5m
output
deployment "istiod" successfully rolled out
bash
kubectl get pods -n istio-system -o wide
output
NAME                      READY   STATUS    RESTARTS   AGE   IP              NODE
istio-cni-node-cntjf      1/1     Running   0          7h    192.168.1.67    worker01
istiod-68b8c6d945-6k9nl   1/1     Running   0          10m   192.168.1.133   worker01

Repeat one node at a time. For control-plane nodes, follow your HA runbook—etcd quorum and API availability matter more than on a single worker. See upgrade a Kubernetes cluster for version skew rules that also apply during long maintenance windows.

If you copied admin.conf to a worker during an earlier lab pass, remove it when you finish—admin.conf grants cluster-admin access and should stay on the control plane or a dedicated admin workstation:

bash
rm -f /root/.kube/config   # on the worker, not on k8s-cp

Validate the hardened node

After each control category, rerun a short checklist.

On the hardened node over SSH:

bash
ss -lntp | grep -E ':22|:6443|:10250'
systemctl is-active kubelet containerd firewalld

On k8s-cp (or your separate admin workstation):

bash
kubectl get nodes

Control-plane sample:

output
LISTEN 0 4096 *:10250 *:* users:(("kubelet",...))
LISTEN 0 4096 *:6443 *:* users:(("kube-apiserver",...))
active
active
active
NAME       STATUS   ROLES           AGE     VERSION
k8s-cp     Ready    control-plane   5h10m   v1.36.3
worker01   Ready    <none>          5h8m    v1.36.3

Recheck file modes on paths you changed, SSH login from an admin jump host, and Pod networking with a test workload if you altered firewall or CNI ports. Run kube-bench periodically to catch permission drift.


Hardening quick reference

Use this table to correlate inspection commands with expected results during study or audits:

Category Inspect Expected result
Services systemctl list-unit-files --state=enabled Only required node services enabled
SSH sshd -T Approved key, root, and password settings
Firewall/CNI Port list and cilium-config tunnel mode Only required paths reachable for your CNI mode
Kubernetes files stat on /etc/kubernetes and /var/lib/kubelet Root ownership and restrictive modes
Runtime socket ls -l /run/containerd/containerd.sock No broad write access
Maintenance cordon, drain, wait, uncordon Drain exits with node/<name> drained before patch; node returns Ready

Troubleshooting

Symptom Likely cause Fix
NodeNotReady after firewall change Node cannot reach the API server on 6443, or required CNI traffic is blocked Check journalctl -u kubelet, API connectivity, and the active CNI tunnel port
kubectl logs, exec, or port-forward fails API server cannot reach kubelet on 10250 Allow 10250/tcp from the control-plane addresses
ImagePullBackOff after egress lockdown Registry unreachable Allow HTTPS to registries and DNS
SSH lockout after hardening Password auth disabled without keys Use console access; restore PasswordAuthentication temporarily
kubelet active but API denies node status Clock skew or cert expiry Sync chronyd; renew certificates per PKI guide
Pods stuck on cordoned node Drain not run before maintenance kubectl drain with appropriate flags
Drain blocked by PDB Eviction would violate PodDisruptionBudget Scale replicas, adjust the PDB, or drain another node
cilium connectivity test hangs External DNS/HTTPS blocked or suite timeout too low Fix CoreDNS forwarders; use --timeout 8m or --single-node; scope with --test
kube-bench FAIL on kubelet config mode World-readable config.yaml chmod 600 /var/lib/kubelet/config.yaml

What's Next


References


Summary

You mapped the Linux node as a security boundary that holds workloads, tokens, runtime sockets, and platform credentials—not just the kubelet binary. The inventory pass recorded OS versions, listening ports, Cilium tunnel mode, package count, SSH effective settings, and permission modes on /etc/kubernetes and /var/lib/kubelet before any change.

Tested steps included disabling an unused httpd service, applying an SSH drop-in validated with sshd -t, aligning firewalld ports with Cilium VXLAN on every node, and running a basic Pod-IP check in an isolated namespace after reload. The patch workflow listed Pods on the node, showed a PDB-blocked drain with --timeout=60s on this single-worker cluster, scaled istiod to accept downtime while preserving the original replica count, completed drain with node/worker01 drained, then restored the Deployment with rollout status. Run kubectl from the control plane or a separate admin workstation—not from the worker being drained.

Match host firewall rules to your CNI tunnel mode—this lab uses Cilium VXLAN on 8472/udp, not Calico ports left from an earlier zone. Blocking kubelet port 10250 from the control plane breaks logs, exec, and port-forwarding; it does not normally flip node readiness, which kubelet reports through its outbound API connection. Pair this checklist with API server controls and periodic kube-bench scans so the OS and Kubernetes layers stay aligned.


Frequently Asked Questions

1. Does hardening the Linux node replace Kubernetes RBAC or NetworkPolicy?

No. Node OS hardening reduces host compromise risk and limits what an attacker can reach on the operating system. RBAC, admission control, and NetworkPolicy still govern API access and Pod traffic. Use both layers—host controls for the node boundary and Kubernetes policy for workloads.

2. Which ports must stay open on a Kubernetes worker node?

At minimum, workers need kubelet HTTPS on 10250 from the control plane, the default NodePort TCP and UDP ranges (30000-32767 unless your cluster customizes them) if you use NodePort Services, and CNI ports that match your plugin mode—Cilium VXLAN uses 8472/udp and Geneve uses 6081/udp, not both. SSH for administration should be restricted by source network. Map your CNI and storage requirements before you tighten host firewall rules.

3. Can I patch a Kubernetes node without draining workloads?

You can reboot a node without draining only when no Pods run there or when you accept workload disruption. The safe sequence is cordon, drain, patch, reboot if required, validate kubelet and container runtime, then uncordon. Skipping drain risks killing Pods without graceful termination.

4. Why are file permissions on kubeconfig and kubelet config important?

Kubeconfig files and kubelet configuration contain credentials or settings that change node behavior. World-readable modes let any local user read cluster credentials or learn authentication settings. CIS benchmarks and kube-bench flag modes such as 644 on /var/lib/kubelet/config.yaml; tighten to 600 owned by root.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)