#!/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