A Secure Kubernetes Multi-Master Cluster Setup Guide
·25 min read·Kubernetes
Kubernetes Multi-Master Cluster Setup Guide
Purpose: This guide provides manual, step-by-step instructions for setting up a production-grade, multi-master Kubernetes cluster. It is designed for operators who want to understand what each step does and why it is necessary — without relying on Ansible or any automation tool.
OS Support: Ubuntu/Debian (apt) and CentOS/RHEL (yum/dnf)
Used by Cilium — must not overlap with host networks
Service CIDR
10.96.0.0/12
Default Kubernetes service range
Docker bridge
172.30.0.1/24
Isolated bridge for Docker
Cluster domain
cluster.local
Default Kubernetes internal DNS domain
2. Node Planning & Prerequisites
2.1 Node requirements
Before you begin, decide on your node layout. Here is a minimum recommended setup:
Node Role
Hostname
IP Address
RAM
CPU
Disk
Master 1
k8s-master-01
192.168.1.10
4 GB
2 vCPU
40 GB
Master 2
k8s-master-02
192.168.1.11
4 GB
2 vCPU
40 GB
Master 3
k8s-master-03
192.168.1.12
4 GB
2 vCPU
40 GB
Worker 1
k8s-worker-01
192.168.1.20
8 GB
4 vCPU
80 GB
Worker 2
k8s-worker-02
192.168.1.21
8 GB
4 vCPU
80 GB
Worker N
k8s-worker-NN
192.168.1.2X
8 GB
4 vCPU
80 GB
VIP
k8s-api.example.com
192.168.1.100
—
—
—
Key decisions to make before starting:
Virtual IP (VIP): Choose a free IP on your subnet that will float between masters. This is the address kubectl and workers will use to reach the API server.
Hostnames: Decide on a consistent naming scheme. All nodes must be able to resolve each other by hostname.
Kubernetes version: Pick a stable version (e.g., 1.29, 1.30, 1.31). All nodes must run the same version.
Network interface: Know the primary network interface name (e.g., eth0, enp1s0, ens192). You will need this for Keepalived.
2.2 What to have ready
SSH access to all nodes as a user with sudo privileges
All nodes can ping each other (network connectivity)
Internet access on all nodes (to download packages)
A DNS server or entries in /etc/hosts for name resolution (we will set this up)
3. Phase 0 — Cluster Cleanup (Nuke)
⚠️ Only run this if you are tearing down an existing cluster and starting fresh.
This will completely remove Kubernetes, Docker, containerd, and all their data.
3.1 Reset Kubernetes
Run this on every node (all masters and workers):
1
2
3
4
5
6
7
8
9
10
11
12
13
# Force reset Kubernetes — removes all pods, configs, and local datasudo kubeadm reset -f# Remove CNI configurationsudo rm-rf /etc/cni/net.d
# Remove kubeconfig from root and your usersudo rm-rf ~/.kube
rm-rf$HOME/.kube
# Remove Kubernetes package sourcessudo rm-f /etc/apt/sources.list.d/kubernetes.list # Debian/Ubuntusudo rm-f /etc/yum.repos.d/kubernetes.repo # CentOS/RHEL
3.2 Unhold and remove Kubernetes packages (Debian/Ubuntu)
1
2
3
4
5
6
# Unhold packages so they can be removedsudo apt-mark unhold kubeadm kubelet kubectl
# Remove Kubernetes binariessudo apt remove --purge-y kubeadm kubelet kubectl
sudo apt autoremove -y
# Remove the kubernetes sysctl config filesudo rm-f /etc/sysctl.d/kubernetes.conf
# Apply the changesudo sysctl --system
3.8 (Optional) Reboot
1
sudo reboot
Why clean up so thoroughly? A fresh install over a previous cluster can fail with cryptic errors. Certificates, configuration, and CNI state can conflict. It is safer to start clean.
4. Phase 1 — Hostname & Hosts File
Why: Kubernetes uses hostnames to identify nodes. Each node must have a unique, resolvable hostname. The /etc/hosts file ensures all nodes can find each other even without DNS.
4.1 Set hostname
Run on each node with its unique hostname:
1
2
3
4
# Replace with the actual hostname for this nodesudo hostnamectl set-hostname k8s-master-01 # On master 1sudo hostnamectl set-hostname k8s-master-02 # On master 2sudo hostnamectl set-hostname k8s-worker-01 # On worker 1, etc.
Verify it changed:
1
hostnamectl status
4.2 Configure /etc/hosts
Edit /etc/hosts on every node to include ALL nodes and the VIP. This way every node can resolve every other node without a DNS server.
1
sudo vi /etc/hosts
Add entries like this (adapt IPs and hostnames to your environment):
1
2
3
4
5
6
7
8
9
10
11
12
# Kubernetes cluster nodes
192.168.1.10 k8s-master-01
192.168.1.11 k8s-master-02
192.168.1.12 k8s-master-03
192.168.1.20 k8s-worker-01
192.168.1.21 k8s-worker-02
# Kubernetes VIP (used as API endpoint)
192.168.1.100 k8s-api.example.com
# Local hostname mapping
127.0.1.1 k8s-master-01 # On each node, this should be its own hostname
Also ensure 127.0.1.1 points to the node’s own hostname (this is needed for correct name resolution):
1
2
# Ensure 127.0.1.1 points to the current node's hostname (NOT localhost)echo"127.0.1.1 $(hostname)" | sudo tee-a /etc/hosts
Note: If you have a working DNS server, you can skip the /etc/hosts entries and use DNS A records instead. But /etc/hosts is simpler and more reliable for small clusters.
5. Phase 2 — OS Prerequisites
Why: Kubernetes and Docker require specific kernel parameters, modules, and settings to function correctly. This phase prepares the operating system on ALL nodes (masters and workers).
Kubernetes does not fully support SELinux enforcing mode. Set it to permissive:
1
2
3
4
5
6
7
8
9
# Temporarily setsudo setenforce 0
# Permanently set in configsudo sed-i's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config
sudo sed-i's/^SELINUX=enforcing/SELINUX=permissive/' /etc/sysconfig/selinux
# Verify
getenforce # Should show "Permissive"
5.4 Disable swap
Why: The Kubernetes kubelet requires swap to be disabled. With swap enabled, the kubelet will fail to start. Swap can cause unpredictable performance for pods.
1
2
3
4
5
6
7
8
# Disable swap immediatelysudo swapoff -a# Remove or comment out swap entries in /etc/fstab so it stays off after rebootsudo sed-i'/ swap / s/^\(.*\)$/#\1/' /etc/fstab
# Verify
free -m# Swap should show 0
5.5 Load required kernel modules
Why: Kubernetes networking relies on br_netfilter for bridge-netfilter communication, and overlay for container filesystems. If you use Ceph/Rook storage, you also need rbd and ceph modules.
# Load modules immediatelysudo modprobe overlay
sudo modprobe br_netfilter
sudo modprobe bridge
# Create a config file so they load on bootcat<<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
bridge
EOF
# If using Ceph/Rook, also load these:sudo modprobe rbd
sudo modprobe ceph
cat<<EOF | sudo tee /etc/modules-load.d/rbd.conf
rbd
EOF
cat<<EOF | sudo tee /etc/modules-load.d/ceph.conf
ceph
EOF
# Verify modules are loaded
lsmod | grep-E"br_netfilter|overlay|bridge"
5.6 Configure sysctl for Kubernetes networking
Why: These sysctl settings enable IP forwarding (required for pod-to-pod communication across nodes) and bridge-netfilter (required for iptables rules to apply to bridged traffic, which is how Kubernetes Services work).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cat<<EOF | sudo tee /etc/sysctl.d/kubernetes.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF
# Also set in /etc/sysctl.conf for persistencesudo sed-i'/^net.bridge.bridge-nf-call-iptables=/d' /etc/sysctl.conf
echo"net.bridge.bridge-nf-call-iptables=1" | sudo tee-a /etc/sysctl.conf
sudo sed-i'/^net.bridge.bridge-nf-call-ip6tables=/d' /etc/sysctl.conf
echo"net.bridge.bridge-nf-call-ip6tables=1" | sudo tee-a /etc/sysctl.conf
# Apply sysctl settingssudo sysctl --system# Verify
sysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables net.ipv4.ip_forward
5.7 Configure inotify limits (for large clusters)
Why: Kubernetes and many applications (e.g., file watchers) use inotify. The default limits are too low for production clusters.
# Global systemd limitssudo sed-i's/^#DefaultLimitNOFILE=/DefaultLimitNOFILE=999999/' /etc/systemd/system.conf
sudo sed-i's/^#DefaultLimitNOFILE=/DefaultLimitNOFILE=999999/' /etc/systemd/user.conf
# Docker service overridesudo mkdir-p /etc/systemd/system/docker.service.d
cat<<EOF | sudo tee /etc/systemd/system/docker.service.d/override.conf
[Service]
LimitNOFILE=999999
EOF
# Kubelet service overridesudo mkdir-p /etc/systemd/system/kubelet.service.d
cat<<EOF | sudo tee /etc/systemd/system/kubelet.service.d/override.conf
[Service]
LimitNOFILE=999999
EOF
# Containerd service overridesudo mkdir-p /etc/systemd/system/containerd.service.d
cat<<EOF | sudo tee /etc/systemd/system/containerd.service.d/override.conf
[Service]
LimitNOFILE=999999
EOF
# Reload systemdsudo systemctl daemon-reexec
5.10 Disable firewalls
Why: Kubernetes manages its own networking rules via iptables/nftables. Having a separate firewall (firewalld, ufw) can conflict and cause connectivity issues.
⚠️ Security note: In a production environment, you should configure firewall rules that align with Kubernetes requirements instead of fully disabling the firewall. The required ports are: TCP 6443 (API server), 2379-2380 (etcd), 10250 (kubelet), 30000-32767 (NodePort services). However, for initial setup, disabling the firewall avoids complexity.
Why: The Docker daemon configuration isolates the Docker bridge network, disables iptables management (Kubernetes will handle that), and sets log rotation to prevent disks from filling up.
Why: This is the core step — it creates the first control plane node. The kubeadm init command bootstraps the cluster: it generates certificates, starts the control plane components (api-server, controller-manager, scheduler), and configures etcd.
7.1 Prepare containerd (on all nodes)
1
2
3
4
5
6
7
8
9
10
# Remove any existing containerd config and regeneratesudo rm-f /etc/containerd/config.toml
sudo containerd config default | sudo tee /etc/containerd/config.toml
# Enable SystemdCgroupsudo sed-i's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
# Restart containerd and kubeletsudo systemctl restart containerd
sudo systemctl restart kubelet
7.2 Create kubeadm configuration
On the first master node only, create a kubeadm config file:
What is controlPlaneEndpoint? This is the address (hostname or IP + port) that all nodes will use to reach the API server. In a multi-master cluster, this should be the VIP or the first master’s IP. Using a hostname here is better because it allows the VIP to change in the future.
7.3 Initialize the cluster
1
2
3
4
5
6
7
# Initialize certificates CA phase firstsudo kubeadm init phase certs ca
# Then initialize the clustersudo kubeadm init \--config=/etc/kubernetes/kubeadm-config.yaml \--upload-certs 2>&1 | tee cluster_initialized.log
What does --upload-certs do? It uploads the control plane certificates to the cluster so that additional master nodes can automatically download them when they join.
Why: By default, kubectl uses ~/.kube/config. The admin.conf file contains the cluster CA certificate and admin credentials.
7.5 Install Cilium CNI
Why: Cilium is the Container Network Interface (CNI) plugin that provides pod networking, network policies, and load balancing. We use Cilium version 1.16.4.
Save these commands! You will use them in Phases 5 and 6. The token expires after 2 hours — if it expires, generate a new one with the same commands.
8. Phase 5 — Join Additional Master Nodes
Why: For high availability, you need at least 3 master nodes. This ensures that if one master fails, the cluster can still operate (etcd requires a majority: 2 out of 3).
8.1 On each additional master node
On each additional master (master-02, master-03, …), run the master join command you saved from Phase 7.7.
What does --control-plane do? It tells kubeadm to install control plane components (api-server, controller-manager, scheduler, etcd) on this node, making it a master.
Tip: If you get errors, try adding --ignore-preflight-errors=all to bypass non-critical warnings.
9.3 Verify from a master
1
kubectl get nodes -o wide
You should see all nodes listed with status Ready.
10. Phase 7 — High Availability with Keepalived + HAProxy
Why: Without HA, if the first master fails, the API server becomes unreachable. HAProxy load-balances traffic across all healthy masters, and Keepalived provides a floating Virtual IP (VIP) that automatically moves to a surviving master.
# Get the list of master IPs# In this example: 192.168.1.10, 192.168.1.11, 192.168.1.12cat<<EOF | sudo tee /etc/haproxy/haproxy.cfg
global
log /dev/log local0
maxconn 2000
user haproxy
group haproxy
defaults
log global
mode tcp
timeout connect 10s
timeout client 1m
timeout server 1m
frontend kube-apiserver
bind 192.168.1.100:6443 # VIP:port
default_backend kube-apiserver-backend
backend kube-apiserver-backend
mode tcp
server master-1 192.168.1.10:6443 check
server master-2 192.168.1.11:6443 check
server master-3 192.168.1.12:6443 check
EOF
How HAProxy works: It listens on the VIP:6443 and forwards traffic to the real API servers on each master. The check option means HAProxy will automatically remove a master from the pool if it fails.
10.5 Start and enable Keepalived and HAProxy
1
2
3
4
5
6
7
8
sudo systemctl enable keepalived
sudo systemctl start keepalived
sudo systemctl enable haproxy
sudo systemctl start haproxy
# Verify they are runningsudo systemctl status keepalived
sudo systemctl status haproxy
10.6 Verify the VIP is assigned
1
2
3
4
# Check which master currently owns the VIP
ip addr show | grep 192.168.1.100
# You should see the VIP on the MASTER node
10.7 Update the kubeadm ConfigMap with the VIP
On the first master, update the cluster configuration to use the VIP/hostname:
1
2
3
4
5
6
7
8
9
10
11
# Get the current ClusterConfiguration
kubectl get cm kubeadm-config -n kube-system -ojsonpath='{.data.ClusterConfiguration}'> /tmp/current-config.yaml
# Update controlPlaneEndpoint to use the VIP hostnamesed-i"s/controlPlaneEndpoint: .*/controlPlaneEndpoint: k8s-api.example.com:6443/" /tmp/current-config.yaml
# Patch the ConfigMap
kubectl patch configmap kubeadm-config -n kube-system --patch"{\"data\":{\"ClusterConfiguration\":\"$(cat /tmp/current-config.yaml | sed':a;N;$!ba;s/\n/\\n/g')\"}}"# Clean uprm /tmp/current-config.yaml
10.8 Update kubeconfig files on ALL nodes
On master nodes
1
2
3
4
5
# Update all kubeconfig files to use the VIP hostnamesudo sed-i's|server: https://.*:6443|server: https://k8s-api.example.com:6443|' /etc/kubernetes/admin.conf
sudo sed-i's|server: https://.*:6443|server: https://k8s-api.example.com:6443|' /etc/kubernetes/controller-manager.conf
sudo sed-i's|server: https://.*:6443|server: https://k8s-api.example.com:6443|' /etc/kubernetes/scheduler.conf
sudo sed-i's|server: https://.*:6443|server: https://k8s-api.example.com:6443|'$HOME/.kube/config
On worker nodes
1
2
3
# Update kubelet configs to use the VIP hostnamesudo sed-i's|server: https://.*:6443|server: https://k8s-api.example.com:6443|' /etc/kubernetes/kubelet.conf
sudo sed-i's|server: https://.*:6443|server: https://k8s-api.example.com:6443|' /var/lib/kubelet/kubeconfig
10.9 Restart kubelet on ALL nodes
1
sudo systemctl restart kubelet
10.10 Regenerate API server certificates with SANs
On the first master, regenerate certificates to include the VIP and all master IPs as Subject Alternative Names (SANs):
# Backup existing certificatessudo cp /etc/kubernetes/pki/apiserver.crt /etc/kubernetes/pki/apiserver.crt.bak-$(date +%Y%m%d%H%M%S)sudo cp /etc/kubernetes/pki/apiserver.key /etc/kubernetes/pki/apiserver.key.bak-$(date +%Y%m%d%H%M%S)# Remove old certificatessudo rm-f /etc/kubernetes/pki/apiserver.crt /etc/kubernetes/pki/apiserver.key
# Create certificate config with SANscat<<EOF | sudo tee /root/kubeadm-cert-config.yaml
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: stable
controlPlaneEndpoint: "k8s-api.example.com:6443"
apiServer:
certSANs:
- "192.168.1.100" # VIP
- "k8s-api.example.com" # VIP hostname
- "10.96.0.1" # Kubernetes service IP
- "kubernetes"
- "kubernetes.default"
- "kubernetes.default.svc"
- "kubernetes.default.svc.cluster.local"
- "localhost"
- "127.0.0.1"
- "192.168.1.10" # Master 1 IP
- "192.168.1.11" # Master 2 IP
- "192.168.1.12" # Master 3 IP
EOF
# Regenerate certificatessudo kubeadm init phase certs apiserver --config /root/kubeadm-cert-config.yaml
# Verify the new certificate includes the VIP
openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text | grep-A1"Subject Alternative Name"
Why do we need certSANs? The API server certificate must list all valid names/IPs that clients use to connect. Without the VIP in the SANs, TLS verification will fail when connecting through the VIP.
10.11 Restart kubelet and verify from a master
On the first master:
1
2
3
4
5
6
7
8
9
10
11
12
# Restart kubeletsudo systemctl restart kubelet
# Wait for API server to be readysleep 10
# Test connectivity via the VIP
kubectl get nodes --server=https://k8s-api.example.com:6443
# If that works, test VIP failover by temporarily stopping keepalived on the master# sudo systemctl stop keepalived# Then check that the VIP moves to another master
11. Phase 8 — Certificate Management
Why: Kubernetes certificates expire (typically after 1 year). You may also need to add new IPs or hostnames to the certificate SANs as your cluster grows.
11.1 Check certificate expiry
1
2
# Check expiry of all certificatessudo kubeadm certs check-expiration
11.2 Renew all certificates
1
2
3
4
5
# Renew all certificatessudo kubeadm certs renew all
# Restart control plane componentssudo systemctl restart kubelet
11.3 Update certificates with new SANs (for HA)
If you need to add new IPs/hostnames to the API server certificate (e.g., after adding a new master), follow the steps in Section 10.10.
11.4 Full certificate regeneration with kubeadm
If the certificates are problematic, you can fully regenerate them:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# On all masters:# 1. Backup old certificatessudo cp-r /etc/kubernetes/pki /etc/kubernetes/pki.bak-$(date +%Y%m%d%H%M%S)# 2. Remove the apiserver cert and keysudo rm-f /etc/kubernetes/pki/apiserver.crt /etc/kubernetes/pki/apiserver.key
# 3. Regenerate with your configsudo kubeadm init phase certs apiserver --config /root/kubeadm-cert-config.yaml
# 4. Restart kubeletsudo systemctl restart kubelet
# 5. On the first master only — update the admin kubeconfigsudo kubeadm init phase kubeconfig all --control-plane-endpoint k8s-api.example.com:6443
12. Phase 9 — Reset Worker Nodes (for Re-joining)
Why: If a worker node becomes corrupted, or you need to reinstall it, you must completely clean it before re-joining.
# Cluster status
kubectl cluster-info
kubectl get nodes -o wide
kubectl get pods -A
kubectl get svc -A# Cilium status
cilium status
cilium connectivity test# Certificate expirysudo kubeadm certs check-expiration
# HAProxy statussudo systemctl status haproxy
sudo haproxy -f /etc/haproxy/haproxy.cfg -c# Check config# Keepalived statussudo systemctl status keepalived
ip addr show | grep <VIP> # Check which node has the VIP# System checks
free -m# Memorydf-h# Diskuptime# How long runningsudo sysctl net.ipv4.ip_forward # Verify IP forwarding
14.2 Common issues and solutions
Symptom
Likely Cause
Solution
kubeadm init fails with “port 6443 already in use”
Previous cluster not cleaned
Run sudo kubeadm reset -f and retry
Node shows NotReady
CNI not installed or containerd issue
Check kubectl get pods -n kube-system, verify containerd is running
Check ip link show for correct interface, check firewall
kubeadm join token expired
Token lifetime exceeded
Generate a new token with kubeadm token create --print-join-command
Pods stuck in ContainerCreating
CNI not ready or network issues
Check Cilium pods, verify br_netfilter module
kubelet constantly restarting
Not yet joined to cluster
This is normal until kubeadm join or kubeadm init is run
14.3 Node port range
Kubernetes uses ports 30000–32767 for NodePort services. Ensure these are open if you need external access to services.
14.4 Required ports reference
Port
Component
Description
6443
kube-apiserver
Kubernetes API (HTTPS)
2379-2380
etcd
etcd server/client
10250
kubelet
Kubelet API
10259
kube-scheduler
Scheduler health
10257
kube-controller-manager
Controller manager health
30000-32767
NodePorts
Service NodePort range
8472
Cilium
VXLAN overlay (if used)
4244
Cilium
Hubble relay
Document Version: 1.0
Last Updated: 2026-07-18
This guide replaces automation (Ansible) with detailed manual steps so operators understand what each command does and why it is needed. Adapt IP addresses, hostnames, interface names, and versions to match your environment.
This documentation provides step-by-step instructions for setting up a Kubernetes 29 cluster, installing necessary components such as Cilium, Rook Ceph storage, and Crunchy PGO,...
This document provides a step-by-step guide to resolve a reconciliation loop issue in the Istio CNI plugin when used alongside Cilium as the primary Container Network Interface ...