RKE2 (Rancher Kubernetes Engine 2) was built with security as a first-class concern. It ships with hardened defaults — no anonymous auth on the API server, audit logging enabled, etcd encryption by default on newer versions. But “secure defaults” and “fully CIS/STIG compliant” are different standards.
This guide covers the practical hardening steps for RKE2 on Rocky Linux 9 to get as close to CIS Kubernetes Benchmark and DoD STIG compliance as possible. This is based on running this setup in a homelab that mirrors production security requirements.
Prerequisites
Before hardening, you need a working RKE2 cluster. This guide assumes:
- Rocky Linux 9.x nodes
- RKE2 v1.32.x
- 3-node cluster (1 control plane, 2 workers)
- Access to modify
/etc/rancher/rke2/on all nodes
Rocky Linux 9 Base OS Hardening
CIS benchmarks start with the OS. Before touching Kubernetes:
# Install the SCAP Security Guide for automated checking
dnf install -y scap-security-guide
# Run CIS Level 1 benchmark scan
oscap xccdf eval \
--profile xccdf_org.ssgproject.content_profile_cis \
--results scan-results.xml \
--report scan-report.html \
/usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml
# Apply CIS remediation (CAUTION: test first)
# oscap xccdf generate fix ...
Key OS hardening steps that the scan will flag:
# 1. Disable unused filesystems
cat > /etc/modprobe.d/cis-hardening.conf << 'EOF'
install cramfs /bin/false
install freevxfs /bin/false
install jffs2 /bin/false
install hfs /bin/false
install hfsplus /bin/false
install squashfs /bin/false
install udf /bin/false
install usb-storage /bin/false
EOF
# 2. Secure /tmp with nodev, nosuid, noexec
systemctl enable tmp.mount
cat >> /etc/systemd/system/tmp.mount << 'EOF'
[Mount]
Options=mode=1777,strictatime,noexec,nodev,nosuid
EOF
# 3. Audit system calls
dnf install -y audit
systemctl enable --now auditd
# Add Kubernetes-specific audit rules
cat > /etc/audit/rules.d/kubernetes.rules << 'EOF'
-w /etc/rancher/rke2/ -p wa -k rke2-config
-w /var/lib/rancher/rke2/ -p wa -k rke2-data
-a always,exit -F arch=b64 -S execve -F path=/usr/local/bin/kubectl -k kubectl-usage
EOF
# 4. Disable core dumps
cat >> /etc/security/limits.conf << 'EOF'
* hard core 0
EOF
echo "kernel.core_pattern=|/bin/false" >> /etc/sysctl.d/99-cis.conf
# 5. Kernel hardening
cat > /etc/sysctl.d/99-kubernetes-cis.conf << 'EOF'
# Disable IPv4 forwarding for non-pod interfaces (RKE2 will set what it needs)
# Note: RKE2 sets net.ipv4.ip_forward = 1 — this is intentional
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.tcp_syncookies = 1
kernel.randomize_va_space = 2
EOF
sysctl --system
RKE2 Server Configuration
The main hardening happens in /etc/rancher/rke2/config.yaml:
# /etc/rancher/rke2/config.yaml (control plane nodes)
# CIS profile — enables a predefined set of hardened defaults
profile: cis # This sets many CIS benchmark settings automatically
# Audit logging to file (required for STIG)
kube-apiserver-arg:
# Authentication
- "anonymous-auth=false"
- "authentication-token-webhook-cache-ttl=0"
# Authorization
- "authorization-mode=Node,RBAC"
# Audit logging (STIG requirement)
- "audit-log-path=/var/lib/rancher/rke2/server/logs/audit.log"
- "audit-log-maxage=30"
- "audit-log-maxbackup=10"
- "audit-log-maxsize=100"
- "audit-policy-file=/etc/rancher/rke2/audit-policy.yaml"
# Admission controllers
- "enable-admission-plugins=NodeRestriction,PodSecurity,ServiceAccount"
# TLS settings
- "tls-min-version=VersionTLS12"
- "tls-cipher-suites=TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
# Secrets encryption
- "encryption-provider-config=/etc/rancher/rke2/encryption-config.yaml"
# Deny running privileged containers at namespace level
- "admission-control-config-file=/etc/rancher/rke2/admission-config.yaml"
# Request timeout
- "request-timeout=60s"
kube-controller-manager-arg:
- "terminated-pod-gc-threshold=10"
- "use-service-account-credentials=true"
- "service-account-private-key-file=/var/lib/rancher/rke2/server/tls/service.key"
- "bind-address=127.0.0.1" # Only listen on localhost (CIS 1.3.7)
kube-scheduler-arg:
- "bind-address=127.0.0.1" # Only listen on localhost (CIS 1.4.2)
kubelet-arg:
- "anonymous-auth=false"
- "authorization-mode=Webhook"
- "client-ca-file=/var/lib/rancher/rke2/server/tls/server-ca.crt"
- "read-only-port=0" # Disable unauthenticated read-only port (CIS 4.2.4)
- "protect-kernel-defaults=true" # Ensure kernel defaults are not overridden
- "event-qps=0" # Disable event rate limiting in some profiles
- "streaming-connection-idle-timeout=5m"
- "rotate-certificates=true"
- "seccomp-default=true" # Enable seccomp by default (requires k8s 1.27+)
- "make-iptables-util-chains=false" # Use iptables without modification chains
# etcd configuration
# etcd runs on control plane nodes; ensure it only listens on localhost or specific interface
etcd-arg:
- "listen-metrics-urls=http://127.0.0.1:2381" # Restrict metrics port
Audit Policy
# /etc/rancher/rke2/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Don't log read-only requests from system components
- level: None
users:
- system:kube-proxy
verbs:
- watch
resources:
- group: ""
resources:
- endpoints
- services
- services/status
# Don't log kubelet authentication/authorization decisions
- level: None
userGroups:
- system:nodes
verbs:
- get
resources:
- group: ""
resources:
- nodes
- nodes/status
# Log all pod exec/attach/portforward at Request level
- level: Request
verbs:
- create
resources:
- group: ""
resources:
- pods/exec
- pods/attach
- pods/portforward
# Log secret access at Metadata level (don't log secret contents)
- level: Metadata
resources:
- group: ""
resources:
- secrets
- configmaps
- group: authentication.k8s.io
resources:
- tokenreviews
# Log all other requests at Metadata level
- level: Metadata
omitStages:
- RequestReceived
Pod Security with Namespace Labels
Kubernetes Pod Security Admission replaced PodSecurityPolicies:
# Label namespaces with the appropriate security level
# Apply to all namespaces that don't need privileged access
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restricted
# For system namespaces that need privileged (kube-system, flux-system, etc.)
kubectl label namespace kube-system \
pod-security.kubernetes.io/enforce=privileged \
pod-security.kubernetes.io/audit=privileged \
pod-security.kubernetes.io/warn=privileged
For your own workloads in restricted namespaces, pods need to be compliant:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: production
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
- name: var-run
mountPath: /var/run
volumes:
- name: tmp
emptyDir: {}
- name: var-run
emptyDir: {}
Network Policies
Default deny all, then explicitly allow what’s needed:
# Default deny all ingress and egress in production
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
# Allow DNS (required for everything)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
Running kube-bench
Verify your hardening with kube-bench:
# Run kube-bench on control plane
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job-master.yaml
kubectl logs job/kube-bench-master | head -100
# Run on worker nodes
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job-node.yaml
kubectl logs job/kube-bench-node
Expected output for a hardened cluster:
== Summary master ==
42 checks PASS
8 checks FAIL
2 checks WARN
1 checks INFO
The remaining failures are typically intentional trade-offs (like etcd on the same nodes as the control plane, which is a homelab/small cluster compromise).
Continuous Compliance Monitoring
Set up ongoing compliance checking:
# Deploy kube-bench as a CronJob for regular scans
apiVersion: batch/v1
kind: CronJob
metadata:
name: kube-bench-scan
namespace: kube-system
spec:
schedule: "0 0 * * 0" # Weekly
jobTemplate:
spec:
template:
spec:
hostPID: true
nodeSelector:
node-role.kubernetes.io/control-plane: "true"
containers:
- name: kube-bench
image: aquasec/kube-bench:latest
command: ["kube-bench", "run", "--targets", "master,etcd"]
volumeMounts:
- name: var-lib-etcd
mountPath: /var/lib/etcd
readOnly: true
- name: var-lib-kubelet
mountPath: /var/lib/kubelet
readOnly: true
- name: etc-systemd
mountPath: /etc/systemd
readOnly: true
restartPolicy: Never
volumes:
- name: var-lib-etcd
hostPath:
path: /var/lib/rancher/rke2/server/db/etcd
- name: var-lib-kubelet
hostPath:
path: /var/lib/kubelet
- name: etc-systemd
hostPath:
path: /etc/systemd
Conclusion
Hardening RKE2 to CIS Benchmark compliance involves layers: the OS, the Kubernetes configuration, Pod Security, and Network Policies. The profile: cis setting in RKE2’s config does a lot of the heavy lifting, but the audit logging, encryption at rest, and admission controller configuration require explicit setup.
For a homelab that mirrors production security requirements, this configuration provides a solid baseline. Some STIG requirements (like multi-factor authentication for cluster admin) require additional infrastructure (identity providers, hardware tokens) that go beyond what’s covered here.
Run kube-bench regularly. The compliance landscape changes, and what passes today may not pass with a new benchmark version. Treat security compliance like any other operational metric: automate its measurement and alert on regressions.