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
Remediation
Manual Steps
-
List pods and identify containers with added capabilities
Run on: any machine with kubectl accesskubectl get pods --all-namespaces -o widekubectl 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.
-
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.
-
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 removeadded capabilities fromsecurityContext.capabilities:# Example: editing a Deploymentkubectl -n <namespace> edit deployment <deployment-name>In the editor, for each container, either:
- Remove the entire
capabilities:block if onlyaddwas 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.
- Remove the entire
-
Adjust or create admission policies to minimize allowedCapabilities
Run on: any machine with kubectl access
If you use policy objects that supportallowedCapabilities(e.g., PodSecurityPolicy, Gatekeeper/Kyverno policies), edit them soallowedCapabilitiesis 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
allowedCapabilitiesfield entirely.
For Gatekeeper/Kyverno, update the constraint/policy definitions similarly so they do not allow added capabilities except in explicitly justified cases. -
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.
-
Verify no containers are running with added capabilities
Run on: any machine with kubectl accesskubectl get pods --all-namespaces -o custom-columns=POD_NAME:.metadata.name,POD_NAMESPACE:.metadata.namespace --no-headers \| while read -r pod_name pod_namespace; dokubectl get pod "${pod_name}" --namespace "${pod_namespace}" -o json \| jq -c '.spec.containers[]' \| while read -r container; docontainer_name=$(echo "${container}" | jq -r '.name')container_caps_add=$(echo "${container}" | jq -r '.securityContext.capabilities.add' | sed -e 's/null/notset/g')is_compliant=truecaps_list=""if [ "${container_caps_add}" != "notset" ]; thenfor cap in $(echo "${container_caps_add}" | jq -r '.[]'); docaps_list+="${cap},"is_compliant=falsedonecaps_list=${caps_list%,}fiif [ "${is_compliant}" = false ]; thenecho "***pod_name: ${pod_name} container_name: ${container_name} pod_namespace: ${pod_namespace} container_caps_add: ${caps_list} is_compliant: false"fidonedoneConfirm that no lines with
is_compliant: falseremain, 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:
-
Identify the owning resource:
kubectl get pod POD_NAME -n POD_NAMESPACE -o jsonpath='{.metadata.ownerReferences}' | jqThen inspect the owner (e.g., Deployment, DaemonSet, Job):
kubectl get deployment DEPLOYMENT_NAME -n POD_NAMESPACE -o yaml -
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