API Server Should Use Strong Cryptographic Ciphers
More Info:
Verifies that --tls-cipher-suites is restricted to strong cipher suites so the API server does not negotiate weak or deprecated TLS ciphers.
Risk Level
Medium
Address
Security
Compliance Standards
- CIS Kubernetes
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
Back up the existing manifest (run on every control plane node):
sudo cp -a /etc/kubernetes/manifests/kube-apiserver.yaml /etc/kubernetes/manifests/kube-apiserver.yaml.bak.$(date +%F-%H%M%S) -
Edit the API server manifest to set strong ciphers (run on every control plane node):
Open the file with a text editor:sudo vi /etc/kubernetes/manifests/kube-apiserver.yamlIn the
command:orargs:list forkube-apiserver, add or replace the existing--tls-cipher-suitesflag so it is exactly:- --tls-cipher-suites=TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256Save and exit.
Operational impact: because this is a static pod manifest under/etc/kubernetes/manifests, the kubelet will automatically restart the API server with the new configuration. -
Confirm the API server pod has restarted (run on any machine with
kubectlaccess):kubectl -n kube-system get pods -l component=kube-apiserver -o wideEnsure the
READYstatus is1/1and theAGEreflects a recent restart compared to the time of your edit. -
Verify the running process uses only the configured strong ciphers (run on every control plane node):
/bin/ps -ef | grep kube-apiserver | grep -v grep | tr ' ' '\n' | grep -- '--tls-cipher-suites'Confirm the output exactly matches:
--tls-cipher-suites=TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
Using kubectl
kubectl cannot modify the API server’s host-level configuration or its static pod manifest at /etc/kubernetes/manifests/kube-apiserver.yaml on the control plane nodes. To remediate this finding, follow the guidance in the Manual Steps section to edit the manifest directly on each control plane node.
Automation
#!/usr/bin/env bash
#
# Harden kube-apiserver TLS cipher suites on all control plane nodes.
#
# Usage:
# 1) Create an inventory file with one control-plane node per line, e.g.:
# /root/controlplanes.txt
# Each line must be usable by ssh (hostname or IP).
# 2) Run:
# bash harden_apiserver_ciphers.sh /root/controlplanes.txt
#
# Requirements:
# - Passwordless SSH (or ssh-agent) from this machine to each control-plane node.
# - Script must be run from a machine that can SSH into control-plane nodes.
# - Remote nodes must store kube-apiserver manifest at:
# /etc/kubernetes/manifests/kube-apiserver.yaml
set -euo pipefail
INVENTORY_FILE="${1:-}"
if [[ -z "$INVENTORY_FILE" || ! -f "$INVENTORY_FILE" ]]; then
echo "Usage: $0 /path/to/controlplane_inventory.txt" >&2
exit 1
fi
# Desired cipher suite list (exactly as per benchmark remediation)
DESIRED_CIPHERS="TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"
REMOTE_MANIFEST="/etc/kubernetes/manifests/kube-apiserver.yaml"
BACKUP_DIR="/etc/kubernetes/manifests/backup-tls-ciphers"
while IFS= read -r NODE || [[ -n "$NODE" ]]; do
[[ -z "$NODE" ]] && continue
echo "=== Processing control-plane node: $NODE ==="
ssh "$NODE" "set -euo pipefail
if [[ ! -f '$REMOTE_MANIFEST' ]]; then
echo 'ERROR: $REMOTE_MANIFEST not found on node; skipping' >&2
exit 1
fi
mkdir -p '$BACKUP_DIR'
# Backup only once per unique content (idempotent-ish)
TS=\$(date +%Y%m%d%H%M%S)
SUM=\$(sha256sum '$REMOTE_MANIFEST' | awk '{print \$1}')
BACKUP_FILE='$BACKUP_DIR'/kube-apiserver.yaml.\$TS.\$SUM
if ! ls '$BACKUP_DIR'/kube-apiserver.yaml.*.\$SUM >/dev/null 2>&1; then
cp '$REMOTE_MANIFEST' \"\$BACKUP_FILE\"
echo 'Created backup: '\$BACKUP_FILE
else
echo 'Matching backup already exists for current manifest hash'
fi
# Work on a temporary file
TMP_MANIFEST=\$(mktemp)
cp '$REMOTE_MANIFEST' \"\$TMP_MANIFEST\"
# Remove any existing --tls-cipher-suites entries (idempotent)
sed -i '/--tls-cipher-suites=/d' \"\$TMP_MANIFEST\"
# Insert the required flag under the kube-apiserver container args.
# This assumes standard kubeadm-style static pod with YAML 'args:' list.
# If an args list already exists, append; otherwise, create one.
if grep -q '^- kube-apiserver' \"\$TMP_MANIFEST\"; then
# Ensure args: key exists for the kube-apiserver container
if ! awk '
\$1 == \"-\" && \$2 == \"name:kube-apiserver\" {found=1}
/args:/ && found==1 {print; exit 0}
' \"\$TMP_MANIFEST\" >/dev/null 2>&1; then
# Try generic insertion of args: list under the kube-apiserver container
awk '
/name: kube-apiserver/ && !added {
print
print \" args:\"
added=1
next
}
{print}
' \"\$TMP_MANIFEST\" > \"\${TMP_MANIFEST}.new\" && mv \"\${TMP_MANIFEST}.new\" \"\$TMP_MANIFEST\"
fi
# Now append the cipher suites argument under args:
# We add it only if not already present with the exact desired list.
if ! grep -q \"--tls-cipher-suites=$DESIRED_CIPHERS\" \"\$TMP_MANIFEST\"; then
awk -v ciphers=\"$DESIRED_CIPHERS\" '
/name: kube-apiserver/ {in_apiserver=1}
in_apiserver && /args:/ {
print
print \" - --tls-cipher-suites=\" ciphers
in_apiserver=0
next
}
{print}
' \"\$TMP_MANIFEST\" > \"\${TMP_MANIFEST}.new\" && mv \"\${TMP_MANIFEST}.new\" \"\$TMP_MANIFEST\"
fi
fi
# Move updated manifest into place (this restarts the kube-apiserver static pod)
cp \"\$TMP_MANIFEST\" '$REMOTE_MANIFEST'
rm -f \"\$TMP_MANIFEST\"
echo 'Updated $REMOTE_MANIFEST with strong TLS cipher suites; kube-apiserver static pod will restart.'
# Verification (adapted from audit command)
# Allow some time for kube-apiserver to restart
for i in \$(seq 1 30); do
if /bin/ps -ef | grep kube-apiserver | grep -v grep >/dev/null 2>&1; then
break
fi
sleep 2
done
echo 'Current kube-apiserver process and cipher suites flag:'
/bin/ps -ef | grep kube-apiserver | grep -v grep | sed -e 's/--tls-cipher-suites/\\n --tls-cipher-suites/g'
if /bin/ps -ef | grep kube-apiserver | grep -v grep | grep -- '--tls-cipher-suites=$DESIRED_CIPHERS' >/dev/null 2>&1; then
echo 'VERIFIED: kube-apiserver is running with the expected --tls-cipher-suites value.'
exit 0
else
echo 'WARNING: kube-apiserver process does not show the exact expected --tls-cipher-suites value.' >&2
exit 1
fi
" || {
echo "Node $NODE: remediation or verification failed" >&2
}
echo
done < "$INVENTORY_FILE"