K8s Cluster Deployment Troubleshooting
This guide is compiled from a real troubleshooting session on a KubeKey-deployed KubeSphere environment. It covers complete diagnostic paths and resolutions for 6 categories of issues: NFS service startup failure, NFS Provisioner scheduling failure, repeated Worker node NotReady, scripts failing to execute on noexec mounts, missing sudo permission configuration, and missing node labels.
Issue 1: nfs-server Service Startup Failure (unit is masked)
Symptoms
Starting the NFS service reports:
failed to restart nfs-server.service
unit nfs-server.service is maskedCause
masked is a more thorough state than disable; it means the service was explicitly blocked by the system administrator to prevent it being started by any means. Masking is typically implemented by creating a symlink pointing to /dev/null.
Solution
# 1. Unmask the service
sudo systemctl unmask nfs-server.service
# 2. Start the service
sudo systemctl restart nfs-server.service
# 3. Enable on boot (optional)
sudo systemctl enable nfs-server.serviceAdditional Checks
If the service still fails to start after unmasking, check whether other dependent services are also masked:
# Check for symlinks pointing to /dev/null
ls -l /etc/systemd/system/ | grep nfs
# Check related dependencies
systemctl status nfs-mountd rpc-statdThe NFS service depends on components such as nfs-mountd and rpc-statd. If any of them is similarly masked, unmask it too.
Issue 2: NFS Provisioner Scheduling Failure
Symptoms
kubectl describe pod -n kube-system nfs-subdir-external-provisioner-xxxOutput:
Conditions:
PodScheduled False
Events:
Warning FailedScheduling
0/2 nodes are available:
1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: },
1 node(s) had untolerated taint {node.kubernetes.io/unreachable: }
preemption: 0/2 nodes are available
Node-Selectors: rise.io/manager=trueCause
Two independent causes are stacked:
- The master node has the
control-planetaint and the Pod has no matching toleration, so it cannot be scheduled. - The worker node has the
unreachabletaint (caused by the node being NotReady), so the Pod cannot be scheduled either. - The Pod's Node-Selector requires the
rise.io/manager=truelabel, but the node lacks it.
Solution
Step 1: Fix the worker node NotReady (see Issue 3)
Once the node returns to Ready, the unreachable taint disappears automatically.
Step 2: Label the node
# Run on the master
kubectl label node 110236p.corp.scaniagroup.cn rise.io/manager=true
# Verify the label
kubectl get node 110236p.corp.scaniagroup.cn --show-labels
# Check whether the Pod is scheduled normally
kubectl get pod -n kube-system | grep nfsNotes
rise.io/manager=true is a label used by KubeKey or storage components to designate the management node, pinning the NFS Provisioner to a specific node.
Issue 3: Repeated NotReady on Worker Node Caused by Port 8081 Occupation
Symptoms
The status of Worker node 110236p.corp.scaniagroup.cn keeps flipping between Ready and NotReady.
kubectl describe node shows:
Conditions:
Type Status Reason
Ready Unknown NodeStatusUnknown
MemoryPressure Unknown Kubelet stopped posting node status
DiskPressure Unknown Kubelet stopped posting node status
PIDPressure Unknown Kubelet stopped posting node status
Taints:
node.kubernetes.io/unreachable:NoExecute
node.kubernetes.io/unreachable:NoSchedule
Lease:
Failed to get lease: leases.coordination.k8s.io "..." not foundKey kubelet log errors:
E0914 ... kubelet_node_status.go] "Unable to register node with API server"
err="Post \"https://127.0.0.1:6443/api/v1/nodes\": dial tcp 127.0.0.1:6443: connect: connection refused"
E0914 ... controller.go:145] "Failed to ensure lease exists, will retry"
err="Get \"https://127.0.0.1:6443/apis/coordination.k8s.io/v1/...\":
dial tcp 127.0.0.1:6443: connect: connection refused"Root Cause Analysis
Failure chain:
McAfee endpoint security software occupies port 8081
↓
HAProxy static Pod cannot bind 8081, crashes on startup
↓
Local 127.0.0.1:6443 proxy unavailable
↓
kubelet cannot reach API Server → node NotReady
↓
Node gets the unreachable taint → other Pods cannot be scheduledOne additional issue was also found:
- HAProxy config file permission problem: the host config file is owned by LDAP user
gqu1vxwith 640 permissions. The in-container HAProxy process runs as non-root and fails to read it withPermission denied.
Diagnostics
Step 1: Observe from the cluster side
kubectl get nodes
kubectl describe node 110236p.corp.scaniagroup.cnStep 2: SSH to the node and check kubelet
systemctl status kubelet
journalctl -u kubelet --no-pager -n 100Step 3: Locate the API Server connectivity issue
grep server /etc/kubernetes/kubelet.confExpected: in a KubeKey HA deployment, this should be https://127.0.0.1:6443.
Step 4: Check the local HAProxy status
crictl ps -a | grep haproxy
crictl logs <haproxy-container-id>Finding 1: Config file permission problem
[ALERT] (1) : config : Could not open configuration file
/usr/local/etc/haproxy/haproxy.cfg : Permission deniedls -l /etc/haproxy/haproxy.cfg
# -rwxr-xr-x. 1 gqulvx ot010005-scan iausers 1096 Sep 14 15:06 haproxy.cfgFinding 2: Port conflict
[ALERT] (1) : Binding [/usr/local/etc/haproxy/haproxy.cfg:24]
for frontend healthz: cannot bind socket (Address in use) for [0.0.0.0:8081]
[ALERT] (1) : [haproxy.main()] Some protocols failed to start their listeners! Exiting.netstat -anput | grep 8081
# tcp6 0 0 :::8081 :::* LISTEN 3384/macmnsvcSolution
Fix 1: Config file permissions
chown root:root /etc/haproxy/haproxy.cfg
chmod 644 /etc/haproxy/haproxy.cfgNote: do NOT change permissions on
/etc/kubernetes/manifests/haproxy.yaml. That is the static Pod manifest read by kubelet and is unrelated to the in-container process permission issue.
Fix 2: Resolve the port conflict
Change the health-check port in the host config file:
sed -i 's/bind :8081/bind :8082/' /etc/haproxy/haproxy.cfgAlso update the probe port in /etc/kubernetes/manifests/haproxy.yaml:
containers:
- name: haproxy
image: ...
livenessProbe:
httpGet:
path: /healthz
port: 8082 # must match haproxy.cfg
readinessProbe:
httpGet:
path: /healthz
port: 8082 # must match haproxy.cfgKey principle: the HAProxy healthz bind port must match the kubelet probe target port. If one changes, the other must change with it.
Verification
# 1. Check port listeners
ss -tlnp | grep -E '8082|6443'
# 2. Simulate a health check
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8082/healthz
# Expected output: 200
# 3. Verify the 6443 proxy
curl -k https://127.0.0.1:6443/healthz
# Expected output: ok
# 4. Check container status
crictl ps | grep haproxy
# Expected: Running
# 5. Observe node status on the master
kubectl get nodes -wKubeKey HA Architecture Notes
In a KubeKey HA deployment, every node runs a local HAProxy static Pod:
kubelet → 127.0.0.1:6443 (local HAProxy) → master:6443 (real API Server)- Design intent: kubelet does not need to know the real master IP; it always uses the local loopback address, so when the master changes, only the HAProxy config needs updating.
- Side effect: once the local HAProxy fails, the node's kubelet immediately loses connectivity and the node turns
NotReady. Always check local HAProxy status first when troubleshooting.
Issue 4: Scripts Cannot Execute on the /tmp Partition with noexec
Symptoms
Executing ./run.sh as root reports:
bash: ./run.sh: Permission deniedBut bash ./run.sh works fine.
Cause
Difference between ./run.sh and bash ./run.sh:
| Method | Needs x permission | Needs r permission | Uses shebang |
|---|---|---|---|
./run.sh | ✅ required | depends | ✅ required |
bash ./run.sh | ❌ not needed | ✅ required | ❌ not needed |
Even if the file has execute permission and the shebang is correct (#!/usr/bin/env bash), the kernel still refuses to execute it if the file resides on a noexec mount (e.g. /tmp).
Diagnostics
# 1. Confirm file permissions
ls -l ./run.sh
# 2. Confirm the mount options of the partition containing the file
findmnt -T ./run.sh -o TARGET,SOURCE,FSTYPE,OPTIONS
# 3. Check whether noexec is present
mount | grep -E "$(df --output=target run.sh | tail -1)"If noexec appears in OPTIONS, this is the cause.
Solution
Option A: Temporary remount (takes effect immediately, lost on reboot)
sudo mount -o remount,exec /tmpOption B: Permanent change (edit /etc/fstab)
# Back up first
sudo cp /etc/fstab /etc/fstab.bak
# Edit fstab: remove noexec or replace with exec for the /tmp line
sudo vi /etc/fstabFrom:
/dev/vdb1 /tmp ext4 rw,noexec,relatime 0 2To:
/dev/vdb1 /tmp ext4 rw,exec,relatime 0 2Option C: Move the script out of /tmp (safest)
cp /tmp/run.sh /root/run.sh
cd /root && ./run.shIf compliance requires /tmp to stay noexec, this option is recommended.
Notes
Even with x permission, if the script's first line lacks a shebang, ./run.sh falls back to /bin/sh, which can cause syntax incompatibilities.
Other causes of Permission denied include: filesystems without execute support (vfat/ntfs/exfat), ACL restrictions, SELinux/AppArmor interception, and noexec mounts inside containers.
Issue 5: sudo User Missing sudoers Configuration Causes Errors
Symptoms
KubeKey deployment reports:
Sorry, user gqu1vx is not allowed to execute '/bin/bash -c echo greetings,kubekey' as root on master, process exited with status 1Cause
This error means sudo itself works, but the user does not match any passwordless sudo rule.
For LDAP/AD users, the username in the sudoers file must exactly match the login name returned by id or getent passwd.
If SSSD is configured with use_fully_qualified_names = True, the recognized username may be gqu1vx@your.domain.com instead of the short name gqu1vx.
Diagnostics
# 1. Confirm the username sudo actually recognizes
id
whoami
# 2. Confirm whether the sudoers rule matches
sudo -l -U gqu1vx
# 3. Check sudoers file syntax
sudo visudo -c -f /etc/sudoers.d/kubekeySolution
Option A: Configure with the short name
echo "gqu1vx ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/kubekey
sudo chmod 440 /etc/sudoers.d/kubekeyOption B: Configure with the fully qualified name (if the system resolves users via FQDN)
echo "gqu1vx@your.domain.com ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/kubekeyOption C: Grant via group (more robust)
# Confirm the user's groups first
id gqu1vx
# Assuming the user is in the wheel or sudo group
echo "%wheel ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/kubekeyNote: the group name must be prefixed with
%.
Verification
# Non-interactive test: succeeds without a password
sudo -n true && echo "passwordless sudo configured" || echo "configuration not effective"
# Validate sudoers syntax
sudo visudo -c -f /etc/sudoers.d/kubekey
# Should output parsed OKAdditional Checks
Check whether /etc/sudoers includes /etc/sudoers.d/:
grep includedir /etc/sudoersShould output something like #includedir /etc/sudoers.d. If not, use visudo to append the rule directly at the end of the main file.
Check LDAP user resolution:
id gqu1vx
getent passwd gqu1vxIf both commands return empty, the problem is in the LDAP client configuration (/etc/nsswitch.conf, /etc/sssd/sssd.conf, etc.), not in sudoers itself.
Why KubeKey Requires Passwordless sudo
KubeKey executes remote commands with sudo -E over a non-interactive SSH channel. When sudo requires a password, the prompt cannot be passed through, causing sudo: 3 incorrect password attempts even with a correct password. The official and community consensus is therefore to configure passwordless sudo for the deployment user in /etc/sudoers on the target node.
Issue 6: Components Fail to Schedule Because the rise.io/manager=true Label Is Missing
Symptoms
Pods of some components (e.g. NFS Provisioner) stay Pending. kubectl describe pod shows:
Node-Selectors: rise.io/manager=true
Events:
Warning FailedScheduling
0/2 nodes are available: 1 node(s) didn't match Pod's node affinity/selectorCause
The Pod's Node-Selector requires nodes to carry the rise.io/manager=true label, but no node in the cluster satisfies this condition.
This label is typically used by KubeKey or storage components to designate the management node, pinning specific components to one node.
Solution
Run on the master node:
# Label the target node
kubectl label node 110236p.corp.scaniagroup.cn rise.io/manager=true
# If the label exists with a different value, use --overwrite
kubectl label node 110236p.corp.scaniagroup.cn rise.io/manager=true --overwriteVerification
# Show all node labels
kubectl get node 110236p.corp.scaniagroup.cn --show-labels
# Show only this label's value
kubectl get node 110236p.corp.scaniagroup.cn -o jsonpath='{.metadata.labels.rise\.io/manager}'
# Expected output: true
# Check whether the Pod is scheduled normally
kubectl get pod -n kube-system | grep nfs
# Expected: Pending → RunningNotes
If other Pods later require this label too, labeling once is enough — all Pods that need it will match. If the label is supposed to be added automatically by KubeKey at a specific stage, also investigate why it was not added (e.g. interrupted deployment flow, an addon not executed).
Appendix A: Common Pitfalls Quick Reference
| Pitfall | Description |
|---|---|
| Wrong file edited | haproxy.yaml (static Pod manifest) and haproxy.cfg (HAProxy config) are two different files |
| Port changed in one place only | After changing HAProxy's bind port, the YAML probe port must change too |
| LDAP user permissions | LDAP user UID/GID may not match inside the container, causing file read failures |
| Security software interference | McAfee/Trellix etc. often fight with K8s components for ports and hook syscalls |
| Residual taint | After the node returns to Ready, the unreachable taint does not disappear immediately; wait for the control plane to process it |
| Script execute permission | ./run.sh needs x permission, bash ./run.sh only needs r permission; noexec mounts refuse execution |
| sudo username matching | The LDAP user's short name / fully qualified name must match what the system recognizes |
| Missing label | If the label required by Node-Selector is missing, Pods stay Pending forever |
Appendix B: Worker Node NotReady Quick Checklist
When a Worker node is NotReady, check in this order:
- [ ]
kubectl describe node <node>— check Conditions and Taints - [ ]
systemctl status kubelet— confirm kubelet process state - [ ]
journalctl -u kubelet --no-pager | grep -i error— check key errors - [ ]
grep server /etc/kubernetes/kubelet.conf— confirm the API Server address - [ ]
crictl ps -a | grep haproxy— check local HAProxy status - [ ]
crictl logs <haproxy-id>— check HAProxy logs - [ ]
ls -l /etc/haproxy/haproxy.cfg— check config file permissions - [ ]
netstat -anput | grep <port>— check port occupation - [ ]
ss -tlnp | grep 6443— confirm the local proxy listener - [ ]
curl -k https://127.0.0.1:6443/healthz— verify proxy connectivity
Appendix C: Complete Failure Chain
McAfee occupies port 8081
↓
HAProxy fails to bind port → startup failure
↓
Local 127.0.0.1:6443 proxy unavailable
↓
kubelet cannot reach API Server → node NotReady
↓
Node gets the unreachable taint
↓
Pods such as NFS Provisioner cannot be scheduled
↓
Also: node lacks the rise.io/manager=true label → scheduling fails furtherDocument Info
| Item | Value |
|---|---|
| Document version | v2.0 |
| Applicable environment | KubeSphere / Kubernetes cluster deployed by KubeKey |
| Issues covered | 6 categories |
| Last updated | 2026-09-15 |