Kubernetes Security Hardening in Practice: RBAC Permissions and Cluster Protection Configuration
Harden a Kubernetes cluster across five layers: least-privilege RBAC, ServiceAccount management, Pod Security Standards, NetworkPolicy isolation, and etcd encryption. Includes kubectl verification commands and common troubleshooting.
Kubernetes security hardening is core to protecting cloud-native environments. This article delivers a ready-to-apply Kubernetes cluster hardening configuration across five layers: RBAC access control, ServiceAccount management, Pod security standards, network policy isolation, and etcd encryption. Each item includes verification commands to help teams reduce the risk of cluster compromise and lateral movement.
Applicable Scenarios
- Security baseline configuration before a Kubernetes cluster goes live or during MLPS remediation
- Multi-team shared clusters that need isolated permissions and resource boundaries
- Audits that found cluster-admin abuse, privileged containers, or similar risks
- Containerized workloads that must meet Pod Security Standards and NetworkPolicy compliance
Prerequisites
- Kubernetes cluster already deployed (this tutorial uses Kubernetes 1.28+ as the example)
- Cluster admin privileges (kubeconfig pointing to an admin context)
- kubectl CLI installed
How It Works
Kubernetes defense in depth has four layers: authentication and authorization (who can call the API), Pod runtime security (what identity and capabilities containers run with), network isolation (whether workloads can reach each other), and data encryption (whether sensitive data stored in etcd is encrypted at rest). In most real intrusions, attackers enter a Pod via supply-chain or application vulnerabilities, then use an over-privileged ServiceAccount to call kubelet/API Server and take over the cluster. Least-privilege RBAC and banning privileged containers are therefore the two highest-priority hardening steps.
Procedure
1. Audit and Tighten RBAC Permissions
`# List all accounts bound to cluster-admin (tighten immediately)
kubectl get clusterrolebindings -o custom-columns=NAME:.metadata.name,SUBJECTS:.subjects[*].name | grep -B1 -i admin
# Check whether the default ServiceAccount has high-risk permissions
kubectl get clusterrolebindings -o json | jq '.items[] | select(.subjects[]?.kind==\"Group\" and .subjects[]?.name==\"system:serviceaccounts\")'`
Create least-privilege Role and RoleBinding for the application (namespace-scoped):
`# Create a read-only Role
cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: app-dev
name: app-readonly
rules:
- apiGroups: [\"\"]
resources: [\"pods\", \"services\", \"configmaps\"]
verbs: [\"get\", \"list\", \"watch\"]
EOF
# Bind to a specified user
kubectl create rolebinding app-readonly-binding --role=app-readonly --user=alice@example.com --namespace=app-dev`
2. Tighten ServiceAccount and Automount
`# Disable automatic mounting of API credentials on the default ServiceAccount
kubectl patch serviceaccount default -n app-dev -p '{\"automountServiceAccountToken\": false}'
# Explicitly assign a dedicated ServiceAccount to each workload
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: app-dev
automountServiceAccountToken: false
EOF`
In the Deployment spec, set `serviceAccountName: app-sa`. Only components that truly need to call the Kubernetes API (such as Operators) should receive dedicated credentials and a mounted token.
3. Enable Pod Security Standards
Use Pod Security Admission (PSA) to force Pods to follow baseline/restricted standards:
`# Enable the restricted (strictest) standard on the app-dev namespace; start with warn to observe
kubectl label --overwrite ns app-dev pod-security.kubernetes.io/enforce=restricted
kubectl label --overwrite ns app-dev pod-security.kubernetes.io/audit=restricted
kubectl label --overwrite ns app-dev pod-security.kubernetes.io/warn=restricted`
Add security context to workloads (restricted requirements):
`securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile:
type: RuntimeDefault
capabilities:
drop: [\"ALL\"]
allowPrivilegeEscalation: false`
4. Configure NetworkPolicy Default Deny
`# Default-deny all ingress/egress traffic
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: app-dev
spec:
podSelector: {}
policyTypes: [\"Ingress\", \"Egress\"]
EOF
# Allow only the frontend to reach backend port 8080
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: app-dev
spec:
podSelector:
matchLabels: { app: backend }
policyTypes: [\"Ingress\"]
ingress:
- from:
- podSelector: { matchLabels: { app: frontend } }
ports:
- protocol: TCP
port: 8080
EOF`
Prerequisite: the cluster must run a CNI plugin that supports NetworkPolicy (Calico, Cilium, Weave, etc.); otherwise policies will not take effect.
5. Enable etcd Encryption at Rest
Enable `--encryption-provider-config` in the API Server startup flags:
`# Generate an encryption key (use a KMS provider in production)
head -c 32 /dev/urandom | base64
# Create the encryption configuration file encryption-config.yaml
cat > /etc/kubernetes/encryption-config.yaml <<EOF
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources: [\"secrets\"]
providers:
- aescbc:
keys:
- name: key1
secret: <base64 key generated in the previous step>
- identity: {}
EOF`
After adding `--encryption-provider-config=/etc/kubernetes/encryption-config.yaml` to kube-apiserver, restart the API Server and re-encrypt all secrets:
`kubectl get secrets --all-namespaces -o json | kubectl replace -f -`
6. Audit Critical API Server Flags
`# Confirm anonymous requests are disabled and RBAC authorization is enabled
kubectl get --raw /api/v1/namespaces/kube-system/configmaps/kube-apiserver-leader-elect 2>/dev/null
# Recommended flags (set when deploying the control plane)
# --authorization-mode=RBAC
# --anonymous-auth=false
# --enable-admission-plugins=NodeRestriction,PodSecurity,ServiceAccount`
Configuration Verification
`# 1. Verify RBAC is in effect: try deleting a Pod with the read-only account (should be rejected)
kubectl --as=alice@example.com -n app-dev delete pod nginx-demo
# Output: Error from server (Forbidden): ... indicates authorization is in effect
# 2. Inspect the Pod's actual security context
kubectl get pod -n app-dev -o jsonpath='{.items[*].spec.containers[*].securityContext}'
# 3. Verify etcd encryption
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key get /registry/secrets/app-dev/mysecret | head -c 200
# 4. NetworkPolicy in effect: access from a non-allowlisted Pod to the backend should time out
kubectl exec -n app-dev frontend-pod -- curl -m 3 http://backend-svc:8080`
If the secret value in etcd starts with the prefix `k8s:enc:aescbc:v1:key1:`, encryption is in effect.
FAQ
FAQ 1: Existing Pods Fail to Schedule or Error After Enabling the Restricted Standard?
Pod Security is admission control; Pods that do not meet restricted are rejected. First check the rejection reason with `kubectl describe pod xxx -n app-dev` (or `kubectl get events`). Then fill in securityContext per restricted requirements (runAsNonRoot, capabilities drop ALL, seccompProfile, allowPrivilegeEscalation: false). For Pods with special needs (such as network-plugin DaemonSets that require NET_ADMIN), downgrade that namespace to baseline, or add the `pod-security.kubernetes.io/audit=baseline` annotation and assess risk. Gray-release in warn/audit mode first, then switch to enforce.
FAQ 2: Services Cannot Reach Each Other After Configuring NetworkPolicy?
A default-deny NetworkPolicy blocks all traffic not explicitly allowed. Troubleshooting order: first confirm the CNI supports NetworkPolicy (`kubectl get networkpolicies` listing policies is a basic sign it is working); second, check whether policyTypes only declares the needed direction (do not write Egress if you only want to block Ingress); third, confirm selector labels exactly match actual Pod labels (`kubectl get pods --show-labels`); fourth, cross-namespace access requires `namespaceSelector`, and the declared target port must match the actual port. When adding allow policies, note that policies are OR'd together; a new policy does not override existing allows.
Summary
Kubernetes security hardening follows least privilege and defense in depth: tighten RBAC admin permissions, disable ServiceAccount automount, ban privileged containers with Pod Security, default-deny with NetworkPolicy, and encrypt etcd at rest. The five layers are independent and complementary; if one layer is breached, the others still limit lateral movement. Bake these settings into base images and Helm templates, add policy checks in CI, and periodically re-baseline the cluster with tools such as `kube-bench` to stay compliant.