Skip to main content

API Server Should Enable The NamespaceLifecycle Admission

More Info:

Verifies that the NamespaceLifecycle admission plugin is not disabled so objects cannot be created in non-existent or terminating namespaces.

Risk Level

Medium

Address

Security

Compliance Standards

  • CIS Kubernetes

Triage and Remediation

Remediation

Manual Steps
  1. On every control plane node, open the API server static pod manifest for editing (this will restart the API server when saved):
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml
  1. In the spec.containers[0].command list, locate any existing --disable-admission-plugins= argument. Edit it to remove NamespaceLifecycle from the comma-separated list of plugins, ensuring the argument either:
  • does not exist at all, or
  • exists but does not contain NamespaceLifecycle (e.g. change
    --disable-admission-plugins=NamespaceLifecycle,SomeOtherPlugin
    to
    --disable-admission-plugins=SomeOtherPlugin).
  1. If NamespaceLifecycle was the only plugin listed, remove the entire --disable-admission-plugins=NamespaceLifecycle flag line from the command list.

  2. Save the file and exit the editor. The kubelet will automatically detect the manifest change and restart the kube-apiserver static pod.

  3. Wait for the API server pod to become ready:

# On any machine with kubectl access
kubectl get pods -n kube-system -l component=kube-apiserver -o wide

Confirm the pod is in Running status and READY is 1/1.

  1. Verify that the NamespaceLifecycle plugin is not disabled:
# On every control plane node
/bin/ps -ef | grep kube-apiserver | grep -v grep | grep -- '--disable-admission-plugins'

Confirm that the output either shows no --disable-admission-plugins flag, or that its value does not contain NamespaceLifecycle.

Using kubectl

kubectl cannot be used to change API server process flags or edit the static pod manifest at /etc/kubernetes/manifests/kube-apiserver.yaml on the control plane nodes. To remediate this finding, make the change directly on each control plane node as described in the Manual Steps section.

Automation
#!/usr/bin/env bash
#
# Fix: Ensure the NamespaceLifecycle admission plugin is NOT disabled
# Scope: every control plane node
# Target file: /etc/kubernetes/manifests/kube-apiserver.yaml
#
# Usage:
# - Run on each control plane node as root.
# - Safe to re-run; idempotent.

set -euo pipefail

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

if [[ $EUID -ne 0 ]]; then
echo "ERROR: Run as root on each control plane node." >&2
exit 1
fi

if [[ ! -f "$APISERVER_MANIFEST" ]]; then
echo "ERROR: kube-apiserver manifest not found at $APISERVER_MANIFEST" >&2
exit 1
fi

mkdir -p "$BACKUP_DIR"
cp -p "$APISERVER_MANIFEST" "$BACKUP_DIR/kube-apiserver.yaml.$TIMESTAMP"

# Function: remove NamespaceLifecycle from an existing --disable-admission-plugins arg
# Handles comma-separated lists and possible surrounding spaces.
clean_disable_plugins_value() {
local value="$1"
local cleaned

# Add leading/trailing commas to simplify removal
cleaned=",$value,"

# Remove 'NamespaceLifecycle' (with or without surrounding spaces) from list
cleaned="$(printf '%s' "$cleaned" \
| sed -E 's/,\s*NamespaceLifecycle\s*,/,/g')"

# Remove possible duplicate commas
cleaned="$(printf '%s' "$cleaned" \
| sed -E 's/,+/,/g')"

# Trim leading/trailing commas
cleaned="$(printf '%s' "$cleaned" \
| sed -E 's/^,+//; s/,+$//')"

# Trim spaces around commas
cleaned="$(printf '%s' "$cleaned" \
| sed -E 's/\s*,\s*/,/g')"

printf '%s' "$cleaned"
}

# Work on a temporary file and then move into place atomically
TMP_FILE="$(mktemp)"
cp "$APISERVER_MANIFEST" "$TMP_FILE"

# Detect if there is any --disable-admission-plugins arg present
if grep -q -- '--disable-admission-plugins' "$TMP_FILE"; then
# Process each line containing --disable-admission-plugins
# Supports both:
# - --disable-admission-plugins=Foo,Bar
# - --disable-admission-plugins Foo,Bar
# We only modify NamespaceLifecycle; other plugins remain untouched.
mapfile -t lines < <(grep -n -- '--disable-admission-plugins' "$TMP_FILE" | cut -d: -f1)

for lineno in "${lines[@]}"; do
line="$(sed -n "${lineno}p" "$TMP_FILE")"

if printf '%s' "$line" | grep -qE -- '--disable-admission-plugins='; then
# Form: --disable-admission-plugins=Foo,Bar
before="$(printf '%s' "$line" | sed -E 's/(.*--disable-admission-plugins=)([^"]*)(.*)/\1/')"
value="$(printf '%s' "$line" | sed -E 's/.*--disable-admission-plugins=([^"]*).*/\1/')"
after="$(printf '%s' "$line" | sed -E 's/.*--disable-admission-plugins=[^"]*(.*)/\1/')"

new_value="$(clean_disable_plugins_value "$value")"

if [[ -z "$new_value" ]]; then
# Entire list would be empty; remove the flag altogether
# Remove ' --disable-admission-plugins=...' from the line
newline="$(printf '%s' "$line" \
| sed -E 's/[[:space:]]*--disable-admission-plugins=[^"[:space:]]*//')"
else
newline="${before}${new_value}${after}"
fi
else
# Form: --disable-admission-plugins Foo,Bar
# Split on the first occurrence of the flag
prefix="$(printf '%s' "$line" | sed -E 's/(.*)--disable-admission-plugins(.*)/\1/')"
rest="$(printf '%s' "$line" | sed -E 's/.*--disable-admission-plugins(.*)/\1/')"

# Extract the value (first token after the flag)
value="$(printf '%s' "$rest" | sed -E 's/^[[:space:]]+([^[:space:]]+).*/\1/')"
suffix="$(printf '%s' "$rest" | sed -E 's/^[[:space:]]+[^[:space:]]+(.*)/\1/')"

new_value="$(clean_disable_plugins_value "$value")"

if [[ -z "$new_value" ]]; then
# Remove the flag entirely
newline="$(printf '%s' "$prefix$rest" \
| sed -E 's/[[:space:]]*--disable-admission-plugins[[:space:]]+[^[:space:]]+//')"
else
newline="${prefix}--disable-admission-plugins ${new_value}${suffix}"
fi
fi

# Replace line in temp file
sed -i "${lineno}s~.*~${newline}~" "$TMP_FILE"
done
else
# No --disable-admission-plugins flag: nothing to change; the plugin is enabled by default.
echo "INFO: --disable-admission-plugins not present; NamespaceLifecycle is not disabled."
fi

# Replace the manifest only if it changed
if cmp -s "$APISERVER_MANIFEST" "$TMP_FILE"; then
echo "INFO: No changes required in $APISERVER_MANIFEST"
rm -f "$TMP_FILE"
else
mv "$TMP_FILE" "$APISERVER_MANIFEST"
echo "INFO: Updated $APISERVER_MANIFEST (kube-apiserver static pod will restart automatically)."
fi

# Verification: ensure NamespaceLifecycle is NOT listed under --disable-admission-plugins
echo "INFO: Waiting for kube-apiserver process to be running..."
sleep 10

echo "INFO: Verifying that NamespaceLifecycle is not disabled..."
if /bin/ps -ef | grep kube-apiserver | grep -v grep | grep -q -- '--disable-admission-plugins'; then
if /bin/ps -ef | grep kube-apiserver | grep -v grep \
| grep -- '--disable-admission-plugins' \
| grep -q 'NamespaceLifecycle'; then
echo "ERROR: NamespaceLifecycle is still present in --disable-admission-plugins." >&2
exit 1
fi
echo "SUCCESS: kube-apiserver is running and NamespaceLifecycle is NOT disabled."
else
echo "SUCCESS: kube-apiserver is running with no --disable-admission-plugins flag; NamespaceLifecycle is enabled by default."
fi