Set up a production-ready Kubernetes cluster using kubeadm with proper security hardening, RBAC configuration, and CNI networking. Includes worker node setup and verification steps.
Prerequisites
- At least 2GB RAM per node
- 2 CPU cores minimum
- Root or sudo access
- Network connectivity between nodes
What this solves
This tutorial helps you create a production-grade Kubernetes cluster using kubeadm with security best practices. You'll learn to set up control plane nodes, join worker nodes, configure networking, and implement security hardening measures including RBAC and network policies.
Step-by-step installation
Update system and install prerequisites
Start by updating your system and installing required packages for container runtime and networking.
sudo apt update && sudo apt upgrade -y
sudo apt install -y apt-transport-https ca-certificates curl gnupg lsb-release
Configure system prerequisites
Disable swap and configure kernel modules required for Kubernetes networking.
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab
overlay
br_netfilter
sudo modprobe overlay
sudo modprobe br_netfilter
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
sudo sysctl --system
Install containerd runtime
Install and configure containerd as the container runtime for Kubernetes.
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y containerd.io
sudo mkdir -p /etc/containerd
sudo containerd config default | sudo tee /etc/containerd/config.toml
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
sudo systemctl restart containerd
sudo systemctl enable containerd
Install Kubernetes components
Add the Kubernetes repository and install kubeadm, kubelet, and kubectl.
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/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.29/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt update
sudo apt install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
Initialize the control plane
Initialize the Kubernetes control plane with security-focused configuration.
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: v1.29.0
controlPlaneEndpoint: "203.0.113.10:6443"
networking:
serviceSubnet: "10.96.0.0/12"
podSubnet: "192.168.0.0/16"
apiServer:
extraArgs:
audit-log-maxage: "30"
audit-log-maxbackup: "3"
audit-log-maxsize: "100"
audit-log-path: "/var/log/audit.log"
enable-admission-plugins: "NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota,NodeRestriction"
controllerManager:
extraArgs:
bind-address: "0.0.0.0"
scheduler:
extraArgs:
bind-address: "0.0.0.0"
etcd:
local:
extraArgs:
listen-metrics-urls: "http://127.0.0.1:2381"
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
localAPIEndpoint:
advertiseAddress: "203.0.113.10"
bindPort: 6443
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
serverTLSBootstrap: true
protectKernelDefaults: true
sudo kubeadm init --config=/etc/kubernetes/kubeadm-config.yaml
Configure kubectl access
Set up kubectl configuration for the current user to interact with the cluster.
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Install Calico CNI
Deploy Calico as the Container Network Interface for pod networking and network policies.
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/tigera-operator.yaml
apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
name: default
spec:
calicoNetwork:
ipPools:
- blockSize: 26
cidr: 192.168.0.0/16
encapsulation: VXLANCrossSubnet
natOutgoing: Enabled
nodeSelector: all()
---
apiVersion: operator.tigera.io/v1
kind: APIServer
metadata:
name: default
spec: {}
kubectl create -f /tmp/calico-custom.yaml
Join worker nodes
Generate and use the join command to add worker nodes to your cluster.
kubeadm token create --print-join-command
Run the output command on each worker node:
sudo kubeadm join 203.0.113.10:6443 --token --discovery-token-ca-cert-hash sha256:
Configure RBAC security
Create restricted service accounts and roles following the principle of least privilege.
apiVersion: v1
kind: ServiceAccount
metadata:
name: restricted-user
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: default
subjects:
- kind: ServiceAccount
name: restricted-user
namespace: default
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
kubectl apply -f /tmp/rbac-config.yaml
Implement network policies
Create default network policies to restrict pod-to-pod communication.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: default
spec:
podSelector: {}
policyTypes:
- Ingress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-same-namespace
namespace: default
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: default
egress:
- to:
- namespaceSelector:
matchLabels:
name: default
kubectl apply -f /tmp/network-policy.yaml
Configure Pod Security Standards
Enable Pod Security Standards to enforce security policies at the namespace level.
kubectl label namespace default pod-security.kubernetes.io/enforce=restricted
kubectl label namespace default pod-security.kubernetes.io/audit=restricted
kubectl label namespace default pod-security.kubernetes.io/warn=restricted
Verify your setup
Check that all cluster components are running and nodes are ready.
kubectl get nodes -o wide
kubectl get pods -n kube-system
kubectl get pods -n calico-system
kubectl cluster-info
Test basic functionality by deploying a simple workload:
kubectl create deployment nginx-test --image=nginx:1.25
kubectl expose deployment nginx-test --port=80 --type=ClusterIP
kubectl get pods,svc
kubectl delete deployment nginx-test
kubectl delete service nginx-test
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Nodes stuck in NotReady | CNI not installed or misconfigured | Check CNI pods: kubectl get pods -n calico-system |
| kubeadm init fails | Swap enabled or ports blocked | Disable swap and check firewall rules for ports 6443, 2379-2380 |
| Pods stuck in Pending | Control plane has NoSchedule taint | Remove taint: kubectl taint nodes --all node-role.kubernetes.io/control-plane- |
| Network policies blocking traffic | Default deny-all policy active | Create specific allow policies for required communication |
| kubectl connection refused | Wrong kubeconfig or API server down | Check sudo systemctl status kubelet and verify kubeconfig |
Next steps
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Configuration
K8S_VERSION="1.29"
CONTROL_PLANE_IP="${1:-}"
POD_SUBNET="192.168.0.0/16"
SERVICE_SUBNET="10.96.0.0/12"
# Cleanup function
cleanup() {
echo -e "${RED}[ERROR] Installation failed. Cleaning up...${NC}"
systemctl stop kubelet containerd 2>/dev/null || true
kubeadm reset -f 2>/dev/null || true
exit 1
}
trap cleanup ERR
usage() {
echo "Usage: $0 <control-plane-ip>"
echo "Example: $0 203.0.113.10"
exit 1
}
# Validate arguments
if [ -z "$CONTROL_PLANE_IP" ]; then
usage
fi
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Please run as root or with sudo${NC}"
exit 1
fi
# Detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
PKG_UPGRADE="apt upgrade -y"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf check-update || true"
PKG_INSTALL="dnf install -y"
PKG_UPGRADE="dnf upgrade -y"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum check-update || true"
PKG_INSTALL="yum install -y"
PKG_UPGRADE="yum upgrade -y"
;;
*)
echo -e "${RED}Unsupported distribution: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Cannot detect distribution${NC}"
exit 1
fi
echo -e "${GREEN}[1/10] Updating system packages...${NC}"
$PKG_UPDATE
$PKG_UPGRADE
echo -e "${GREEN}[2/10] Installing prerequisites...${NC}"
if [ "$PKG_MGR" = "apt" ]; then
$PKG_INSTALL apt-transport-https ca-certificates curl gnupg lsb-release
else
$PKG_INSTALL curl gnupg2 software-properties-common device-mapper-persistent-data lvm2
fi
echo -e "${GREEN}[3/10] Configuring system prerequisites...${NC}"
# Disable swap
swapoff -a
sed -i '/ swap / s/^/#/' /etc/fstab
# Load kernel modules
modprobe overlay
modprobe br_netfilter
# Create modules-load configuration
cat > /etc/modules-load.d/k8s.conf << EOF
overlay
br_netfilter
EOF
# Configure sysctl parameters
cat > /etc/sysctl.d/k8s.conf << EOF
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sysctl --system > /dev/null
echo -e "${GREEN}[4/10] Installing containerd runtime...${NC}"
if [ "$PKG_MGR" = "apt" ]; then
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list
$PKG_UPDATE
$PKG_INSTALL containerd.io
else
if command -v dnf &> /dev/null; then
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
else
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
fi
$PKG_INSTALL containerd.io
fi
echo -e "${GREEN}[5/10] Configuring containerd...${NC}"
mkdir -p /etc/containerd
containerd config default > /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
chmod 644 /etc/containerd/config.toml
systemctl restart containerd
systemctl enable containerd
echo -e "${GREEN}[6/10] Installing Kubernetes components...${NC}"
if [ "$PKG_MGR" = "apt" ]; then
curl -fsSL https://pkgs.k8s.io/core:/stable:/v${K8S_VERSION}/deb/Release.key | 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:/v${K8S_VERSION}/deb/ /" > /etc/apt/sources.list.d/kubernetes.list
$PKG_UPDATE
$PKG_INSTALL kubelet kubeadm kubectl
apt-mark hold kubelet kubeadm kubectl
else
cat > /etc/yum.repos.d/kubernetes.repo << EOF
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v${K8S_VERSION}/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v${K8S_VERSION}/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF
$PKG_INSTALL kubelet kubeadm kubectl --disableexcludes=kubernetes
fi
systemctl enable kubelet
echo -e "${GREEN}[7/10] Creating kubeadm configuration...${NC}"
cat > /tmp/kubeadm-config.yaml << EOF
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: v${K8S_VERSION}.0
controlPlaneEndpoint: "${CONTROL_PLANE_IP}:6443"
networking:
serviceSubnet: "${SERVICE_SUBNET}"
podSubnet: "${POD_SUBNET}"
apiServer:
extraArgs:
audit-log-maxage: "30"
audit-log-maxbackup: "3"
audit-log-maxsize: "100"
audit-log-path: "/var/log/audit.log"
enable-admission-plugins: "NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota,NodeRestriction"
controllerManager:
extraArgs:
bind-address: "0.0.0.0"
scheduler:
extraArgs:
bind-address: "0.0.0.0"
etcd:
local:
extraArgs:
listen-metrics-urls: "http://127.0.0.1:2381"
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
localAPIEndpoint:
advertiseAddress: "${CONTROL_PLANE_IP}"
bindPort: 6443
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
serverTLSBootstrap: true
rotateCertificates: true
EOF
chmod 644 /tmp/kubeadm-config.yaml
echo -e "${GREEN}[8/10] Initializing Kubernetes control plane...${NC}"
kubeadm init --config=/tmp/kubeadm-config.yaml --upload-certs
echo -e "${GREEN}[9/10] Setting up kubectl for root user...${NC}"
mkdir -p /root/.kube
cp -f /etc/kubernetes/admin.conf /root/.kube/config
chmod 600 /root/.kube/config
echo -e "${GREEN}[10/10] Installing Calico CNI...${NC}"
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.0/manifests/calico.yaml
echo -e "${YELLOW}Waiting for cluster to be ready...${NC}"
sleep 30
# Verify installation
echo -e "${GREEN}Verifying installation...${NC}"
if kubectl get nodes &>/dev/null; then
echo -e "${GREEN}✓ kubectl is working${NC}"
else
echo -e "${RED}✗ kubectl is not working${NC}"
exit 1
fi
if systemctl is-active --quiet containerd; then
echo -e "${GREEN}✓ containerd is running${NC}"
else
echo -e "${RED}✗ containerd is not running${NC}"
exit 1
fi
if systemctl is-active --quiet kubelet; then
echo -e "${GREEN}✓ kubelet is running${NC}"
else
echo -e "${RED}✗ kubelet is not running${NC}"
exit 1
fi
echo -e "\n${GREEN}Kubernetes cluster initialized successfully!${NC}"
echo -e "\n${YELLOW}To join worker nodes, use the following commands:${NC}"
echo "1. Run this script on worker nodes (it will install prerequisites)"
echo "2. Use the kubeadm join command shown above"
echo -e "\n${YELLOW}To use kubectl from non-root user:${NC}"
echo "mkdir -p \$HOME/.kube"
echo "sudo cp -i /etc/kubernetes/admin.conf \$HOME/.kube/config"
echo "sudo chown \$(id -u):\$(id -g) \$HOME/.kube/config"
# Clean up temporary files
rm -f /tmp/kubeadm-config.yaml
Review the script before running. Execute with: bash install.sh