Skip to main content

Minimize Admission Of Containers Added Capabilities

More Info:​

Do not generally permit containers with capabilities assigned beyond the default set.

Risk Level​

Medium

Address​

Security

Compliance Standards​

  • CIS Kubernetes

Triage and Remediation​

Remediation​

Manual Steps
  1. List pods and identify containers with added capabilities
    Run on: any machine with kubectl access

    kubectl get pods --all-namespaces -o wide
    kubectl get pods --all-namespaces -o json \
    | jq -r '.items[]
    | .metadata as $m
    | .spec.containers[]
    | select(.securityContext.capabilities.add != null)
    | "\($m.namespace) \($m.name) \(.name) \(.securityContext.capabilities.add)"'

    Review each listed pod/container and confirm whether the added capabilities are truly required for the workload.

  2. Review and decide on policy and workload changes
    Run on: any machine with kubectl access

    • Determine if the capability can be removed entirely from the workload spec.
    • If a capability is required, document the business/technical justification and ensure it is explicitly approved.
    • Decide if enforcement should be done via Pod Security Admission, PodSecurityPolicy (legacy), or another admission controller (e.g., OPA/Gatekeeper, Kyverno) to prevent future use of added capabilities.
  3. Edit workloads to remove added capabilities
    Run on: any machine with kubectl access
    For each non-exempt pod, edit its controller (Deployment/DaemonSet/StatefulSet/Job, etc.) and remove added capabilities from securityContext.capabilities:

    # Example: editing a Deployment
    kubectl -n <namespace> edit deployment <deployment-name>

    In the editor, for each container, either:

    • Remove the entire capabilities: block if only add was used, or
    • Remove the add: field, leaving other fields (e.g., drop:) intact.
      Save and exit to trigger a rollout and recreate pods without added capabilities.
  4. Adjust or create admission policies to minimize allowedCapabilities
    Run on: any machine with kubectl access
    If you use policy objects that support allowedCapabilities (e.g., PodSecurityPolicy, Gatekeeper/Kyverno policies), edit them so allowedCapabilities is either absent or an empty array:

    # Example: editing a PodSecurityPolicy (if still in use)
    kubectl edit podsecuritypolicy <psp-name>

    In the YAML:

    allowedCapabilities: []

    or remove the allowedCapabilities field entirely.
    For Gatekeeper/Kyverno, update the constraint/policy definitions similarly so they do not allow added capabilities except in explicitly justified cases.

  5. Handle exceptions via dedicated namespaces or policies (if needed)
    Run on: any machine with kubectl access

    • For workloads that must retain specific capabilities, place them in dedicated namespaces and apply narrowly-scoped policies only there.
    • Ensure that cluster-wide or default policies do not include non-empty allowedCapabilities; instead, use per-namespace/per-workload exceptions with clear labels and documentation.
  6. Verify no containers are running with added capabilities
    Run on: any machine with kubectl access

    kubectl get pods --all-namespaces -o custom-columns=POD_NAME:.metadata.name,POD_NAMESPACE:.metadata.namespace --no-headers \
    | while read -r pod_name pod_namespace; do
    kubectl get pod "${pod_name}" --namespace "${pod_namespace}" -o json \
    | jq -c '.spec.containers[]' \
    | while read -r container; do
    container_name=$(echo "${container}" | jq -r '.name')
    container_caps_add=$(echo "${container}" | jq -r '.securityContext.capabilities.add' | sed -e 's/null/notset/g')
    is_compliant=true
    caps_list=""
    if [ "${container_caps_add}" != "notset" ]; then
    for cap in $(echo "${container_caps_add}" | jq -r '.[]'); do
    caps_list+="${cap},"
    is_compliant=false
    done
    caps_list=${caps_list%,}
    fi
    if [ "${is_compliant}" = false ]; then
    echo "***pod_name: ${pod_name} container_name: ${container_name} pod_namespace: ${pod_namespace} container_caps_add: ${caps_list} is_compliant: false"
    fi
    done
    done

    Confirm that no lines with is_compliant: false remain, except for any explicitly approved, documented exceptions.

Using kubectl

On any machine with kubectl access.

This control is marked MANUAL: there is no single automatic fix, because legitimate workloads may require added Linux capabilities. Use these steps to review and, where appropriate, remove or tightly scope added capabilities.

1. Discover pods with added capabilities​

kubectl get pods --all-namespaces -o wide

Then use the supplied audit snippet (or a trimmed version) to list only non‑compliant containers:

kubectl get pods --all-namespaces -o custom-columns=POD_NAME:.metadata.name,POD_NAMESPACE:.metadata.namespace --no-headers | \
while read -r pod_name pod_namespace; do
kubectl get pod "${pod_name}" --namespace "${pod_namespace}" -o json | \
jq -c '.spec.containers[]' | while read -r container; do
container_name=$(echo "${container}" | jq -r '.name')
container_caps_add=$(echo "${container}" | jq -r '.securityContext.capabilities.add' | sed -e 's/null/notset/g')
is_compliant=true
caps_list=""
if [ "${container_caps_add}" != "notset" ]; then
for cap in $(echo "${container_caps_add}" | jq -r '.[]'); do
caps_list+="${cap},"
is_compliant=false
done
caps_list=${caps_list%,}
fi
if [ "${is_compliant}" = false ]; then
echo "NON-COMPLIANT pod=${pod_name} ns=${pod_namespace} container=${container_name} added_caps=${caps_list}"
fi
done
done

Use this output as your review list.

2. Review and decide per workload​

For each non‑compliant pod / namespace / container:

  1. Identify the owning resource:

    kubectl get pod POD_NAME -n POD_NAMESPACE -o jsonpath='{.metadata.ownerReferences}' | jq

    Then inspect the owner (e.g., Deployment, DaemonSet, Job):

    kubectl get deployment DEPLOYMENT_NAME -n POD_NAMESPACE -o yaml
  2. With the application owner, decide:

    • Are the added capabilities truly required?
    • Can they be removed entirely?
    • If not, can the set be reduced to the minimal necessary capabilities?

Document the decision for each workload as part of your risk acceptance or exception process.

3. Remove or minimize added capabilities in manifests​

Edit the owning object manifests (Deployment, StatefulSet, DaemonSet, Job, CronJob, etc.) to remove securityContext.capabilities.add where possible.

Example patterns:

  • Current (non‑compliant)
apiVersion: apps/v1
kind: Deployment
metadata:
name: example
namespace: example-ns
spec:
template:
spec:
containers:
- name: app
image: your-image
securityContext:
capabilities:
add:
- NET_ADMIN
- SYS_TIME
  • Preferred (no added capabilities)
apiVersion: apps/v1
kind: Deployment
metadata:
name: example
namespace: example-ns
spec:
template:
spec:
containers:
- name: app
image: your-image
securityContext:
capabilities: {}

or simply omit the securityContext.capabilities stanza entirely from the container spec.

Apply the change declaratively from any machine with kubectl access:

kubectl apply -f path/to/your-updated-manifest.yaml

Repeat for each workload where you have determined that added capabilities are not strictly required.

4. (Optional) Review PodSecurityPolicy / PodSecurity / admission policies​

Where you still use policy objects that can specify allowedCapabilities, ensure they do not globally allow extra capabilities beyond the default set, unless set to an empty array as per the remediation.

Examples:

PodSecurityPolicy (legacy clusters)

  • Non‑compliant:
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: restricted
spec:
allowedCapabilities:
- NET_ADMIN
- SYS_ADMIN
  • Compliant:
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: restricted
spec:
allowedCapabilities: []

Apply:

kubectl apply -f psp-restricted.yaml

Gatekeeper / Kyverno or similar admission policies

Review constraint or policy manifests and either remove allowedCapabilities or set them to [], then:

kubectl apply -f path/to/policy.yaml

5. Verification​

After updating manifests and/or policies, re‑run the audit from any machine with kubectl access:

kubectl get pods --all-namespaces -o custom-columns=POD_NAME:.metadata.name,POD_NAMESPACE:.metadata.namespace --no-headers | \
while read -r pod_name pod_namespace; do
kubectl get pod "${pod_name}" --namespace "${pod_namespace}" -o json | \
jq -c '.spec.containers[]' | while read -r container; do
container_name=$(echo "${container}" | jq -r '.name')
container_caps_add=$(echo "${container}" | jq -r '.securityContext.capabilities.add' | sed -e 's/null/notset/g')
is_compliant=true
if [ "${container_caps_add}" != "notset" ]; then
is_compliant=false
fi
if [ "${is_compliant}" = false ]; then
echo "NON-COMPLIANT pod=${pod_name} ns=${pod_namespace} container=${container_name} added_caps=${container_caps_add}"
fi
done
done

The cluster is aligned with the control when any remaining NON-COMPLIANT entries are understood, documented as exceptions, and justified as operationally required.

Automation
#!/usr/bin/env bash
#
# Purpose:
# Identify pods whose containers request added Linux capabilities and
# emit patched manifests (with added capabilities removed) that can be
# applied back to the cluster.
#
# Scope:
# Run on any machine with kubectl and jq installed and access to the cluster.
#
# Notes:
# - This does NOT mutate running pods directly; instead it generates
# corrected Pod/Workload manifests (YAML) you can review and apply.
# - This is idempotent: re-running will only regenerate patches for
# workloads that still add capabilities.
# - Because the benchmark control is MANUAL, you must review whether
# each capability is really needed before applying the change.
#
# Requirements:
# - kubectl configured with sufficient RBAC to list and get all resources.
# - jq installed and on PATH.

set -euo pipefail

OUT_DIR="${OUT_DIR:-./capability-fix-manifests}"
mkdir -p "${OUT_DIR}"

echo "Discovering pods with added capabilities (this may take some time)..."

# Map: workload_kind/workload_namespace/workload_name -> has_added_caps=true
declare -A WORKLOADS_WITH_CAPS

# Helper: identify owning workload for a pod
get_owner_ref() {
local pod_json="$1"
echo "${pod_json}" | jq -r '
.metadata.ownerReferences // [] |
map(select(.controller == true))[0] //
{"kind":"Pod","name":.metadata.name,"apiVersion":.apiVersion} |
"\(.kind),\(.name),\(.apiVersion)"
'
}

# Scan all pods for added capabilities
while IFS=$'\t' read -r pod_ns pod_name; do
pod_json="$(kubectl get pod "${pod_name}" -n "${pod_ns}" -o json 2>/dev/null || true)"
[ -z "${pod_json}" ] && continue

# Check all containers in the pod for added capabilities
has_caps="$(echo "${pod_json}" | jq -r '
[
(.spec.containers[]? | .securityContext.capabilities.add? // []),
(.spec.initContainers[]? | .securityContext.capabilities.add? // [])
]
| flatten
| length > 0
')"

if [ "${has_caps}" = "true" ]; then
owner_ref="$(get_owner_ref "${pod_json}")"
IFS=',' read -r owner_kind owner_name owner_api <<< "${owner_ref}"

# Normalize owner kind/apiVersion into a resource type we can patch
# Only handling common controllers; others you may patch manually.
if [ "${owner_kind}" = "ReplicaSet" ]; then
# Map ReplicaSet -> Deployment if possible
rs_json="$(kubectl get rs "${owner_name}" -n "${pod_ns}" -o json 2>/dev/null || true)"
if [ -n "${rs_json}" ]; then
dep_owner="$(echo "${rs_json}" | jq -r '
.metadata.ownerReferences // [] |
map(select(.controller == true and .kind == "Deployment"))[0].name // ""
')"
if [ -n "${dep_owner}" ]; then
key="Deployment/${pod_ns}/${dep_owner}"
else
key="ReplicaSet/${pod_ns}/${owner_name}"
fi
else
key="ReplicaSet/${pod_ns}/${owner_name}"
fi
elif [ "${owner_kind}" = "StatefulSet" ] || [ "${owner_kind}" = "DaemonSet" ] || [ "${owner_kind}" = "Job" ] || [ "${owner_kind}" = "CronJob" ]; then
key="${owner_kind}/${pod_ns}/${owner_name}"
else
# Fall back to pod-level patching if no controller owner
key="Pod/${pod_ns}/${pod_name}"
fi

WORKLOADS_WITH_CAPS["${key}"]=true
fi
done < <(kubectl get pods --all-namespaces -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name --no-headers)

if [ "${#WORKLOADS_WITH_CAPS[@]}" -eq 0 ]; then
echo "No pods found with added capabilities. Cluster is compliant for this control."
exit 0
fi

echo "Identified ${#WORKLOADS_WITH_CAPS[@]} workload(s)/pod(s) with added capabilities."
echo "Generating patched manifests in: ${OUT_DIR}"

# Generate patched manifests with capabilities.add removed/emptied
for key in "${!WORKLOADS_WITH_CAPS[@]}"; do
IFS='/' read -r kind ns name <<< "${key}"
echo "Processing ${kind} ${ns}/${name}"

# Get original as YAML
if ! kubectl get "${kind,,}.${kind,,}.k8s.io" "${name}" -n "${ns}" >/dev/null 2>&1; then
# Fallback to short kind (e.g., deployment)
res="${kind,,}"
else
res="${kind,,}.${kind,,}.k8s.io"
fi

if ! orig_yaml="$(kubectl get "${res}" "${name}" -n "${ns}" -o yaml 2>/dev/null)"; then
# Try core resource (Pod)
if ! orig_yaml="$(kubectl get "${kind,,}" "${name}" -n "${ns}" -o yaml 2>/dev/null)"; then
echo " WARNING: could not fetch ${kind} ${ns}/${name}; skipping"
continue
fi
fi

# Strip added capabilities from spec.template.spec.*containers for controllers,
# or spec.containers/spec.initContainers for pods.
patched_yaml="$(
echo "${orig_yaml}" | \
jq -r '
. as $root
| if .kind == "Pod" then
.spec.containers |=
map(
if .securityContext? and .securityContext.capabilities? then
.securityContext.capabilities.add = []
else
.
end
)
| .spec.initContainers |=
(map(
if .securityContext? and .securityContext.capabilities? then
.securityContext.capabilities.add = []
else
.
end
) // .spec.initContainers)
else
.spec.template.spec.containers |=
map(
if .securityContext? and .securityContext.capabilities? then
.securityContext.capabilities.add = []
else
.
end
)
| .spec.template.spec.initContainers |=
(map(
if .securityContext? and .securityContext.capabilities? then
.securityContext.capabilities.add = []
else
.
end
) // .spec.template.spec.initContainers)
end
' 2>/dev/null
)"

out_file="${OUT_DIR}/${kind}-${ns}-${name}.yaml"
echo "${patched_yaml}" > "${out_file}"
echo " Wrote patched manifest: ${out_file}"
done

cat <<EOF

Next steps (manual review required):

1. Review each generated YAML file under:
${OUT_DIR}
Ensure that removing added capabilities will not break workloads that
legitimately require them.

2. Apply the approved manifests back to the cluster, for example:
for f in ${OUT_DIR}/*.yaml; do
echo "Applying \$f"
kubectl apply -f "\$f"
done

3. Verification (required by the benchmark):

Re-run the audit command on any machine with kubectl access:

kubectl get pods --all-namespaces -o custom-columns=POD_NAME:.metadata.name,POD_NAMESPACE:.metadata.namespace --no-headers | while read -r pod_name pod_namespace
do
kubectl get pod "\${pod_name}" --namespace "\${pod_namespace}" -o json | jq -c '.spec.containers[]' | while read -r container
do
container_name=\$(echo \${container} | jq -r '.name')
container_caps_add=\$(echo \${container} | jq -r '.securityContext.capabilities.add' | sed -e 's/null/notset/g')
is_compliant=true
caps_list=""
if [ "\${container_caps_add}" != "notset" ]; then
for cap in \$(echo "\${container_caps_add}" | jq -r '.[]'); do
caps_list+="\${cap},"
is_compliant=false
done
caps_list=\${caps_list%,}
fi
if [ "\${is_compliant}" = true ]; then
echo "***pod_name: \${pod_name} container_name: \${container_name} pod_namespace: \${pod_namespace} container_caps_add: \${container_caps_add} is_compliant: true"
else
echo "***pod_name: \${pod_name} container_name: \${container_name} pod_namespace: \${pod_namespace} container_caps_add: \${caps_list} is_compliant: false"
fi
done
done

The cluster is compliant for this control when all lines show is_compliant: true.

EOF

Additional Reading:​