Skip to main content

Kube-Proxy Metrics Service Bound To Localhost

More Info:​

Binding the kube-proxy metrics service to localhost prevents the metrics endpoint from being reachable across the network. This limits exposure of operational data to the local node only.

Risk Level​

Medium

Address​

Security

Compliance Standards​

  • CIS Kubernetes

Triage and Remediation​

Remediation​

Manual Steps
  1. Identify how kube-proxy is started (systemd vs. manifest)

    • Run on: every worker node
    /bin/ps -fC kube-proxy
    • From the output, note whether kube-proxy is started via:
      • a systemd unit referencing a config file (e.g. /etc/kubernetes/proxy.conf), or
      • a static pod manifest (e.g. /etc/kubernetes/manifests/kube-proxy.yaml).
    • The remaining steps assume the config file /etc/kubernetes/proxy.conf is in use; if not, adjust to the actual kube-proxy config file or manifest used on that node.
  2. Edit the kube-proxy configuration to bind metrics to localhost

    • Run on: every worker node
    • Open the config file:
      sudo vi /etc/kubernetes/proxy.conf
    • In the metricsBindAddress field under kubeProxyConfiguration, set it to localhost if it is set to another address, or add it if missing:
      apiVersion: kubeproxy.config.k8s.io/v1alpha1
      kind: KubeProxyConfiguration
      ...
      metricsBindAddress: "127.0.0.1:10249"
    • Remove any non-localhost setting such as 0.0.0.0:10249 or a specific node IP.
  3. If kube-proxy is launched via command-line flags, ensure flags do not override localhost binding

    • Run on: every worker node
    • Check the kube-proxy process command for a --metrics-bind-address flag:
      /bin/ps -fC kube-proxy | sed -n '2p'
    • If you see a flag binding to a non-localhost address (for example --metrics-bind-address=0.0.0.0:10249), edit the systemd unit or launch script shown in the command line (for example /etc/systemd/system/kube-proxy.service), and either:
      • change it to:
        --metrics-bind-address=127.0.0.1:10249
        or
      • remove the flag entirely so the default 127.0.0.1:10249 from the config file applies.
    • After editing a systemd unit file, reload systemd:
      sudo systemctl daemon-reload
  4. Restart kube-proxy to apply changes

    • Run on: every worker node
    • If kube-proxy is managed by systemd:
      sudo systemctl restart kube-proxy
    • If kube-proxy runs as a static pod from a manifest, saving the edited manifest or config file usually causes the kubelet to automatically restart the pod. No extra command is needed, but be aware that editing a manifest used by kubelet will restart kube-proxy on that node.
  5. Verify that metrics are now bound to localhost

    • Run on: every worker node
    /bin/ps -fC kube-proxy
    • Confirm in the command line or implied configuration that:
      • --metrics-bind-address is either absent or set to 127.0.0.1:10249, and
      • there is no binding of the metrics service to a non-localhost address.
Using kubectl

kubectl cannot modify kube-proxy’s host-level configuration or the /etc/kubernetes/proxy.conf file where the metrics binding is set; this must be fixed directly on every worker node. See the Manual Steps section for how to edit the configuration and restart kube-proxy safely.

Automation
#!/usr/bin/env bash
#
# Remediate CISKubernetes 4.3.1:
# Ensure that the kube-proxy metrics service is bound to localhost (127.0.0.1:10249)
#
# Run this script on every worker node as root.
# It is safe to re-run (idempotent).
#
# Assumptions:
# - kube-proxy is started with: kube-proxy --config=/etc/kubernetes/proxy.conf
# - /etc/kubernetes/proxy.conf is a kube-proxy config file (YAML or JSON)
#
# Operational impact:
# - This script only edits /etc/kubernetes/proxy.conf.
# - kube-proxy must be restarted (by your node/cluster management tooling) to pick up changes.

set -euo pipefail

PROXY_CONF="/etc/kubernetes/proxy.conf"
BACKUP_SUFFIX="$(date +%Y%m%d%H%M%S)"
DESIRED_METRICS_BIND_ADDRESS="127.0.0.1:10249"

log() {
printf '[%s] %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*" >&2
}

require_root() {
if [[ "$(id -u)" -ne 0 ]]; then
log "ERROR: must be run as root."
exit 1
fi
}

check_file_exists() {
if [[ ! -f "$PROXY_CONF" ]]; then
log "ERROR: $PROXY_CONF not found. This script cannot proceed on this node."
exit 1
fi
}

backup_file() {
local src="$1"
local dst="${src}.bak.${BACKUP_SUFFIX}"
cp -p "$src" "$dst"
log "Backup created: $dst"
}

current_setting() {
# Extract any explicit metricsBindAddress setting (basic grep, works for YAML/JSON)
# Returns first match or empty string.
local val
val="$(grep -E 'metricsBindAddress' "$PROXY_CONF" | head -n 1 || true)"
if [[ -z "$val" ]]; then
printf '\n'
return 0
fi
# Try to strip key and whitespace, keep value portion
val="${val#*metricsBindAddress}"
val="${val#*:}"
val="${val//\"/}"
val="${val//\'/}"
val="$(echo "$val" | xargs || true)"
printf '%s\n' "$val"
}

ensure_metrics_bind_address() {
local current
current="$(current_setting)"

if [[ -z "$current" ]]; then
log "No explicit metricsBindAddress found; adding ${DESIRED_METRICS_BIND_ADDRESS}."
backup_file "$PROXY_CONF"

# Heuristic: if file looks like JSON (starts with {), add JSON field; else treat as YAML.
if head -n1 "$PROXY_CONF" | grep -q '^{'; then
# JSON: insert/overwrite using jq if available, otherwise append a comment for manual follow-up
if command -v jq >/dev/null 2>&1; then
tmp="$(mktemp)"
jq --arg v "$DESIRED_METRICS_BIND_ADDRESS" '.metricsBindAddress = $v' "$PROXY_CONF" > "$tmp"
mv "$tmp" "$PROXY_CONF"
log "Updated JSON metricsBindAddress to ${DESIRED_METRICS_BIND_ADDRESS}."
else
log "WARNING: jq not installed; cannot safely edit JSON. Please set metricsBindAddress manually to ${DESIRED_METRICS_BIND_ADDRESS} in $PROXY_CONF."
fi
else
# YAML: append at end
cat <<EOF >>"$PROXY_CONF"

# Enforced by CIS Kubernetes 4.3.1 hardening script
metricsBindAddress: ${DESIRED_METRICS_BIND_ADDRESS}
EOF
log "Appended YAML metricsBindAddress: ${DESIRED_METRICS_BIND_ADDRESS}."
fi
elif [[ "$current" == "$DESIRED_METRICS_BIND_ADDRESS" ]]; then
log "metricsBindAddress already set to ${DESIRED_METRICS_BIND_ADDRESS}; no change needed."
else
log "metricsBindAddress currently set to '${current}', updating to ${DESIRED_METRICS_BIND_ADDRESS}."
backup_file "$PROXY_CONF"

# Replace existing line containing metricsBindAddress (YAML/JSON) with desired value
# This is a simple in-place edit, safe for typical kube-proxy configs.
sed -i.bak.tmp \
-E "s/^( *\"?metricsBindAddress\"?[ :]+).*/\1${DESIRED_METRICS_BIND_ADDRESS}/" \
"$PROXY_CONF"
rm -f "${PROXY_CONF}.bak.tmp"
log "Updated metricsBindAddress to ${DESIRED_METRICS_BIND_ADDRESS}."
fi
}

verify_result() {
log "Verification: checking /etc/kubernetes/proxy.conf for metricsBindAddress=${DESIRED_METRICS_BIND_ADDRESS}"
local after
after="$(current_setting)"
if [[ "$after" == "$DESIRED_METRICS_BIND_ADDRESS" ]]; then
log "OK: metricsBindAddress is set correctly in $PROXY_CONF."
else
log "WARNING: metricsBindAddress not confirmed as ${DESIRED_METRICS_BIND_ADDRESS}."
log "Current parsed value: '${after}' (empty means not found)."
fi

log "Verification: checking running kube-proxy process flags (may require restart to match config)."
if /bin/ps -fC kube-proxy >/dev/null 2>&1; then
/bin/ps -fC kube-proxy || true
else
log "NOTE: kube-proxy process not detected by 'ps -fC kube-proxy'. It may not be running or may use a different process name."
fi
log "If kube-proxy is running with a config file, restart it to apply the updated metricsBindAddress."
}

main() {
require_root
check_file_exists
ensure_metrics_bind_address
verify_result
}

main "$@"