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