Skip to main content

API Server Authorization Mode Should Include Node

More Info:

Verifies that --authorization-mode includes Node. The Node authorizer restricts what kubelets can read and write, limiting the blast radius of a compromised node.

Risk Level

High

Address

Security

Compliance Standards

  • CIS Kubernetes

Triage and Remediation

Remediation

Manual Steps
  1. On every control plane node, back up the existing manifest before making changes:

    sudo cp -a /etc/kubernetes/manifests/kube-apiserver.yaml /etc/kubernetes/manifests/kube-apiserver.yaml.bak.$(date +%F-%H%M%S)
  2. On every control plane node, open the API server static pod manifest for editing:

    sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml

    (Use any text editor available if vi is not preferred.)

  3. In the command: section of the kube-apiserver container, locate any existing --authorization-mode= argument.

    • If present, ensure it includes Node (for example: --authorization-mode=Node,RBAC).
    • If not present, add a new line under the other -- flags, for example:
    - --authorization-mode=Node,RBAC
  4. Save the file and exit the editor. The kubelet will automatically detect the manifest change and restart the kube-apiserver static pod; expect a brief control plane interruption while it restarts.

  5. On every control plane node, verify that the running API server process includes --authorization-mode with Node:

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

    Confirm in the output that the kube-apiserver command line contains an argument similar to:

    --authorization-mode=Node,RBAC

    (or another value that includes Node).

Using kubectl

kubectl cannot modify the kube-apiserver static pod manifest or its process flags, so this setting cannot be fixed via the Kubernetes API. To remediate, you must edit /etc/kubernetes/manifests/kube-apiserver.yaml directly on every control plane node as described in the Manual Steps section.

Automation
#!/usr/bin/env bash
#
# Remediation: Ensure kube-apiserver --authorization-mode includes Node
# Target: Every control plane node
#
# This script:
# - Backs up /etc/kubernetes/manifests/kube-apiserver.yaml
# - Ensures --authorization-mode includes "Node" and "RBAC"
# - Leaves any other existing modes intact (deduplicated)
# - Relies on kubelet to restart the kube-apiserver static pod
# - Verifies the running kube-apiserver process args
#
# Run on: every control plane node (with root privileges)
# Usage: sudo bash fix-apiserver-authorization-mode-node.sh

set -euo pipefail

APISERVER_MANIFEST="/etc/kubernetes/manifests/kube-apiserver.yaml"
BACKUP_SUFFIX=".pre_authmode_node_$(date +%Y%m%d%H%M%S)"
REQUIRED_MODES=("Node" "RBAC")

# --- Helper functions ---

err() {
echo "ERROR: $*" >&2
}

info() {
echo "INFO: $*"
}

check_root() {
if [[ "$(id -u)" -ne 0 ]]; then
err "This script must be run as root."
exit 1
fi
}

check_manifest_exists() {
if [[ ! -f "${APISERVER_MANIFEST}" ]]; then
err "Manifest ${APISERVER_MANIFEST} not found. This node may not be a control plane node or uses a different path."
exit 1
fi
}

backup_manifest() {
local backup_path="${APISERVER_MANIFEST}${BACKUP_SUFFIX}"
cp "${APISERVER_MANIFEST}" "${backup_path}"
info "Backup created at ${backup_path}"
}

# Extract the current --authorization-mode value (comma-separated list)
get_current_modes() {
# Grep the line that contains --authorization-mode and extract its value
# Handles patterns like:
# - --authorization-mode=RBAC,Webhook
# - - --authorization-mode=RBAC,Webhook
# - --authorization-mode RBAC,Webhook
# - - --authorization-mode
# - RBAC,Webhook
#
# We get the first occurrence only.
python3 - "$APISERVER_MANIFEST" << 'PYEOF'
import sys, re, yaml, os

manifest_path = sys.argv[1]
with open(manifest_path) as f:
doc = yaml.safe_load(f)

c = doc.get("spec", {}).get("containers", [])
if not c:
sys.exit(0)

args = c[0].get("command") or c[0].get("args") or []
modes = None

# Normalize args to list of strings
args = [str(a) for a in args]

for i, a in enumerate(args):
if a.startswith("--authorization-mode="):
modes = a.split("=", 1)[1].strip()
break
if a == "--authorization-mode" and i + 1 < len(args):
modes = args[i+1].strip()
break

if modes:
print(modes)
PYEOF
}

# Write updated modes back into the manifest, preserving YAML structure
set_modes() {
local new_modes="$1"
python3 - "$APISERVER_MANIFEST" "$new_modes" << 'PYEOF'
import sys, yaml, copy

manifest_path, new_modes = sys.argv[1], sys.argv[2]

with open(manifest_path) as f:
doc = yaml.safe_load(f)

c = doc.get("spec", {}).get("containers", [])
if not c:
sys.exit(0)

container = c[0]
args = container.get("command") or container.get("args") or []

# Normalize args to list of strings
args = [str(a) for a in args]

updated = False
for i, a in enumerate(args):
if a.startswith("--authorization-mode="):
args[i] = f"--authorization-mode={new_modes}"
updated = True
break
if a == "--authorization-mode" and i + 1 < len(args):
args[i+1] = new_modes
updated = True
break

if not updated:
# Append as a new argument: --authorization-mode=<modes>
args.append(f"--authorization-mode={new_modes}")

# Decide whether original used "command" or "args"
if container.get("command"):
container["command"] = args
else:
container["args"] = args

with open(manifest_path, "w") as f:
yaml.safe_dump(doc, f, default_flow_style=False)
PYEOF
}

normalize_modes() {
local current="$1"
# Split on commas into an array, trim spaces, dedupe, ensure REQUIRED_MODES present
python3 - << PYEOF
modes_str = """${current}"""
required = ${REQUIRED_MODES[@]/#/\"}
required = [${required}]
modes = [m.strip() for m in modes_str.split(",") if m.strip()] if modes_str.strip() else []

# ensure required modes are present
for r in required:
if r not in modes:
modes.append(r)

# dedupe while preserving order
seen = set()
result = []
for m in modes:
if m not in seen:
seen.add(m)
result.append(m)

print(",".join(result))
PYEOF
}

verify_running_process() {
info "Waiting for kube-apiserver static pod to be restarted by kubelet (up to 120s)..."
local timeout=120
local interval=5
local elapsed=0

while (( elapsed < timeout )); do
if /bin/ps -ef | grep kube-apiserver | grep -v grep >/dev/null 2>&1; then
break
fi
sleep "${interval}"
elapsed=$((elapsed + interval))
done

if ! /bin/ps -ef | grep kube-apiserver | grep -v grep >/dev/null 2>&1; then
err "kube-apiserver process not detected after waiting. Check pod status with 'crictl ps' or 'docker ps' depending on your runtime."
return 1
fi

info "Verifying that kube-apiserver is running with --authorization-mode including Node..."
if /bin/ps -ef | grep kube-apiserver | grep -v grep | grep -- '--authorization-mode' | grep -q 'Node'; then
info "Verification succeeded: kube-apiserver --authorization-mode includes Node."
else
err "Verification FAILED: kube-apiserver --authorization-mode does not include Node in the running process."
return 1
fi
}

# --- Main ---

check_root
check_manifest_exists

info "Reading current --authorization-mode from ${APISERVER_MANIFEST}..."
CURRENT_MODES="$(get_current_modes || true)"

if [[ -z "${CURRENT_MODES}" ]]; then
info "No existing --authorization-mode found; will add Node,RBAC."
NEW_MODES="$(normalize_modes "")"
else
info "Current --authorization-mode modes: ${CURRENT_MODES}"
NEW_MODES="$(normalize_modes "${CURRENT_MODES}")"
fi

info "New desired --authorization-mode modes: ${NEW_MODES}"

if [[ "${CURRENT_MODES}" == "${NEW_MODES}" ]]; then
info "Manifest already configured with required modes; no change needed."
else
backup_manifest
set_modes "${NEW_MODES}"
info "Updated ${APISERVER_MANIFEST} with --authorization-mode=${NEW_MODES}"
fi

# Verification (running process)
verify_running_process