Skip to main content

API Server Should Enable The DenyServiceExternalIPs

More Info:

Verifies that the DenyServiceExternalIPs admission plugin is enabled. This plugin blocks use of Service external IPs, mitigating a known man-in-the-middle vector.

Risk Level

Medium

Address

Security

Compliance Standards

  • CIS Kubernetes

Triage and Remediation

Remediation

Manual Steps
  1. On every control plane node, back up the API server manifest before editing:

    sudo cp /etc/kubernetes/manifests/kube-apiserver.yaml /etc/kubernetes/manifests/kube-apiserver.yaml.bak
  2. On every control plane node, open the API server manifest for editing:

    sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml
  3. In the spec.containers[0].command section, locate any existing --disable-admission-plugins= flag.

    • Remove ServiceAccount from its comma-separated list if present, ensuring it is not listed.
    • If the flag becomes empty, remove the flag line entirely.
    • Save and exit the editor.
      (Editing this static pod manifest will cause the kube-apiserver pod to restart automatically.)
  4. On every control plane node, confirm that the kube-apiserver pod has restarted and is running:

    sudo crictl ps | grep kube-apiserver

    (Use docker ps instead if Docker is the container runtime.)

  5. On any machine with access to the node, verify the running kube-apiserver process no longer disables ServiceAccount via --disable-admission-plugins:

    /bin/ps -ef | grep kube-apiserver | grep -v grep

    Inspect the command line output and confirm that either --disable-admission-plugins is absent, or if present, its value does not contain ServiceAccount.

Using kubectl

kubectl cannot modify kube-apiserver process flags or the static pod manifest at /etc/kubernetes/manifests/kube-apiserver.yaml on control plane nodes, so this finding cannot be fixed via the Kubernetes API. Make the required changes directly on each control plane node’s host configuration as described in the Manual Steps section.

Automation
#!/usr/bin/env bash
#
# Automation: Enable DenyServiceExternalIPs admission plugin on all control plane nodes
#
# Usage:
# 1) From any machine with SSH access to control plane nodes, create an inventory file:
# cat > controlplane_hosts.txt <<EOF
# 10.0.0.11
# 10.0.0.12
# 10.0.0.13
# EOF
# 2) Run:
# ./fix_apiserver_deny_service_external_ips.sh controlplane_hosts.txt
#
# This script:
# - Connects to each control plane node over SSH.
# - Edits /etc/kubernetes/manifests/kube-apiserver.yaml to ensure
# DenyServiceExternalIPs is present in --enable-admission-plugins and NOT present
# in --disable-admission-plugins.
# - Creates backup copies before changes.
# - Relies on the kubelet to restart the kube-apiserver static pod.
# - Verifies via `ps` that the plugin settings are correct.
#
# Requirements on the machine running this script:
# - bash, ssh, sed, grep
# - SSH key or passwordless sudo on target nodes (script uses sudo on remote).
#
# Runs on: any machine with SSH access to control plane nodes.
# Remote edits on: every control plane node.

set -euo pipefail

INVENTORY_FILE="${1:-}"

if [[ -z "${INVENTORY_FILE}" || ! -f "${INVENTORY_FILE}" ]]; then
echo "Usage: $0 controlplane_hosts.txt"
echo "controlplane_hosts.txt should contain one hostname/IP per line for each control plane node."
exit 1
fi

SSH_OPTS="-o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"

remote_fix_node() {
local NODE="$1"

echo "==== [${NODE}] Processing control plane node ===="

ssh ${SSH_OPTS} "${NODE}" 'bash -s' <<'EOF'
set -euo pipefail

APISERVER_MANIFEST="/etc/kubernetes/manifests/kube-apiserver.yaml"
BACKUP_DIR="/etc/kubernetes/manifests/backup_deny_service_externalips"
TIMESTAMP="$(date +%Y%m%d%H%M%S)"

if [[ ! -f "${APISERVER_MANIFEST}" ]]; then
echo " [WARN] kube-apiserver manifest not found at ${APISERVER_MANIFEST}; skipping."
exit 0
fi

sudo mkdir -p "${BACKUP_DIR}"

BACKUP_FILE="${BACKUP_DIR}/kube-apiserver.yaml.${TIMESTAMP}"
sudo cp "${APISERVER_MANIFEST}" "${BACKUP_FILE}"
echo " [INFO] Backup created at ${BACKUP_FILE}"

TMP_FILE="$(mktemp)"
sudo cp "${APISERVER_MANIFEST}" "${TMP_FILE}"

# Ensure DenyServiceExternalIPs is enabled and not disabled.

# 1) Remove DenyServiceExternalIPs from any existing --disable-admission-plugins value.
if grep -qE -- '--disable-admission-plugins=' "${TMP_FILE}"; then
sudo sed -i \
-e 's/\(--disable-admission-plugins=[^-]*\)DenyServiceExternalIPs,\{0,1\}/\1/g' \
-e 's/\(--disable-admission-plugins=[^-]*\),\{0,1\}DenyServiceExternalIPs/\1/g' \
"${TMP_FILE}"
fi

# 2) Ensure --enable-admission-plugins includes DenyServiceExternalIPs.
if grep -qE -- '--enable-admission-plugins=' "${TMP_FILE}"; then
# If flag exists but does not yet contain DenyServiceExternalIPs, append it.
if ! grep -qE -- '--enable-admission-plugins=[^"]*DenyServiceExternalIPs' "${TMP_FILE}"; then
sudo sed -i \
-e 's/\(--enable-admission-plugins=[^-"]*\)"/\1,DenyServiceExternalIPs"/' \
-e "s/\(--enable-admission-plugins=[^-']*\)'/\1,DenyServiceExternalIPs'/" \
-e 's/\(--enable-admission-plugins=[^ ]*\)$/\1,DenyServiceExternalIPs/' \
"${TMP_FILE}" || true
fi
else
# No --enable-admission-plugins flag; add it alongside other args.
# Try to append as a new - --enable-admission-plugins=... line in YAML args list.
if grep -q '^\s*- kube-apiserver' "${TMP_FILE}"; then
# Static pod style manifest; append line under existing args if present.
if grep -q '^\s*- --' "${TMP_FILE}"; then
sudo awk '
/- --/ && !added {
print $0
print " - --enable-admission-plugins=DenyServiceExternalIPs"
added=1
next
}
{ print $0 }
END {
if (!added) {
print " - --enable-admission-plugins=DenyServiceExternalIPs"
}
}
' "${TMP_FILE}" | sudo tee "${TMP_FILE}.new" >/dev/null
sudo mv "${TMP_FILE}.new" "${TMP_FILE}"
else
# Fallback: just append a generic line near the command args.
echo " - --enable-admission-plugins=DenyServiceExternalIPs" | sudo tee -a "${TMP_FILE}" >/dev/null
fi
else
# Very old-style manifest or different layout: append a generic arg.
echo " - --enable-admission-plugins=DenyServiceExternalIPs" | sudo tee -a "${TMP_FILE}" >/dev/null
fi
fi

# Install updated manifest (this will cause kubelet to restart the kube-apiserver static pod).
sudo mv "${TMP_FILE}" "${APISERVER_MANIFEST}"
echo " [INFO] Updated ${APISERVER_MANIFEST}; kubelet will restart kube-apiserver."

# Wait for kube-apiserver process to restart and pick up new flags.
echo " [INFO] Waiting for kube-apiserver to reflect new flags..."
RETRIES=30
SLEEP_SECONDS=10

for i in $(seq 1 "${RETRIES}"); do
if ps -ef | grep kube-apiserver | grep -v grep >/dev/null 2>&1; then
# Verify DenyServiceExternalIPs is enabled and not disabled.
if ps -ef | grep kube-apiserver | grep -v grep | grep -q -- '--enable-admission-plugins=[^ ]*DenyServiceExternalIPs'; then
if ! ps -ef | grep kube-apiserver | grep -v grep | grep -q -- '--disable-admission-plugins=[^ ]*DenyServiceExternalIPs'; then
echo " [OK] DenyServiceExternalIPs is enabled and not disabled on kube-apiserver."
exit 0
else
echo " [WARN] DenyServiceExternalIPs still appears in --disable-admission-plugins."
fi
else
echo " [WARN] DenyServiceExternalIPs not yet present in --enable-admission-plugins."
fi
else
echo " [INFO] kube-apiserver process not detected yet."
fi
sleep "${SLEEP_SECONDS}"
done

echo " [ERROR] Timed out waiting for kube-apiserver to show correct DenyServiceExternalIPs flags."
exit 1
EOF

echo "==== [${NODE}] Done ===="
}

while IFS= read -r NODE; do
[[ -z "${NODE}" || "${NODE}" =~ ^# ]] && continue
remote_fix_node "${NODE}"
done < "${INVENTORY_FILE}"

echo "All listed control plane nodes processed."