Containers Should Drop All Linux Capabilities
More Info:
Verifies every container drops ALL capabilities and adds back only what it needs. Excess capabilities expand the attack surface of a compromised container.
Risk Level
High
Address
Security
Compliance Standards
- Cloudanix Best Practice
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
Identify noncompliant pods and their owning workloads (run on any machine with kubectl access):
kubectl get pods --all-namespaces -o json | jq -r '[ .items[]| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)| .metadata as $m| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own| ((.spec.containers // []) + (.spec.initContainers // []))[]| (.securityContext.capabilities.drop // []) as $drop| (($drop | index("ALL")) or ($drop | index("all"))) as $ok| select($ok | not)| "ns=\($m.namespace) pod=\($m.name) container=\(.name)"+ (if $own == null then "" else " ownerKind=\($own.kind) ownerName=\($own.name)" end)][]' -
For each listed pod, edit the owning workload manifest (Deployment, StatefulSet, DaemonSet, Job, CronJob, or the Pod itself) to drop all capabilities (run on any machine with kubectl access). Example for a Deployment:
kubectl -n <namespace> edit deployment <deployment-name>Under each
spec.template.spec.containers[](andinitContainers[]if present), add or modify:securityContext:capabilities:drop:- "ALL"# add:# - "NET_BIND_SERVICE" # example only if strictly required -
If the pod is not controlled by a higher-level workload (no ownerReferences), edit the Pod spec directly and reapply it as a new manifest (pods cannot be updated in place for some fields). Export the pod spec, clean it, and save as a manifest (run on any machine with kubectl access):
kubectl -n <namespace> get pod <pod-name> -o yaml > /tmp/<pod-name>.yamlEdit
/tmp/<pod-name>.yaml:- Remove
status:section. - Remove fields under
metadata:such asuid,resourceVersion,managedFields,creationTimestamp, andownerReferences. - Under each container and initContainer, ensure:
securityContext:capabilities:drop:- "ALL"# add:# - "<ONLY_WHAT_IS_NEEDED>"
- Remove
-
Apply the updated standalone Pod manifest (run on any machine with kubectl access):
kubectl apply -f /tmp/<pod-name>.yaml -
Confirm updated workloads have rolled out successfully (run on any machine with kubectl access), for example for a Deployment:
kubectl -n <namespace> rollout status deployment/<deployment-name> -
Verify compliance across the cluster (run on any machine with kubectl access):
kubectl get pods --all-namespaces -o json | jq -r '[ .items[]| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)| .metadata as $m| (.spec.nodeName // "") as $node| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own| ((.spec.containers // []) + (.spec.initContainers // []))[]| (.securityContext.capabilities.drop // []) as $drop| (($drop | index("ALL")) or ($drop | index("all"))) as $ok| "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)+ (if $node == "" then "" else " node=\($node)" end)+ (if $labels == "" then "" else " labels=\($labels)" end)+ (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)+ " container=\(.name) image=\(.image)"+ " capabilitiesDrop=\(if ($drop | length) == 0 then "none" else ($drop | join("+")) end)"+ " is_compliant=\(if $ok then "true" else "false" end)"] as $rows| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'Ensure that the output is either a single line
is_compliant=trueor that every listed container hasis_compliant=true.
Using kubectl
On any machine with kubectl access:
-
Identify the non‑compliant Pod and its controller
kubectl get pods -Akubectl get pod <POD_NAME> -n <NAMESPACE> -o yaml | yq '.metadata.ownerReferences'If the Pod is owned by a controller (Deployment, StatefulSet, DaemonSet, Job, CronJob, etc.), you must edit the controller, not the Pod, or the change will be overwritten.
-
Edit the Pod spec (ephemeral / stand‑alone Pod only)
For a Pod not managed by a controller:kubectl edit pod <POD_NAME> -n <NAMESPACE>Under each container (and initContainer if present), add:
spec:containers:- name: <container-name>securityContext:capabilities:drop:- "ALL"# add:# - "NET_BIND_SERVICE" # example only; include only what is requiredinitContainers:- name: <init-container-name>securityContext:capabilities:drop:- "ALL" -
Edit the owning controller (preferred, declarative)
Deployment example:
kubectl edit deployment <DEPLOYMENT_NAME> -n <NAMESPACE>Add the same block under every container and initContainer:
spec:template:spec:containers:- name: <container-name>securityContext:capabilities:drop:- "ALL"# add:# - "NET_BIND_SERVICE"initContainers:- name: <init-container-name>securityContext:capabilities:drop:- "ALL"For other controllers, replace
deploymentwithstatefulset,daemonset,job, orcronjoband edit.spec.template.specsimilarly.If you manage manifests declaratively, update your YAML files with the same
securityContext.capabilities.drop: ["ALL"]blocks and apply:kubectl apply -f <your-manifest>.yaml -
Verification
Re‑run the benchmark audit command from any machine with kubectl access:
kubectl get pods --all-namespaces -o json | jq -r '[ .items[]| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)| .metadata as $m| (.spec.nodeName // "") as $node| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own| ((.spec.containers // []) + (.spec.initContainers // []))[]| (.securityContext.capabilities.drop // []) as $drop| (($drop | index("ALL")) or ($drop | index("all"))) as $ok| "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)+ (if $node == "" then "" else " node=\($node)" end)+ (if $labels == "" then "" else " labels=\($labels)" end)+ (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)+ " container=\(.name) image=\(.image)"+ " capabilitiesDrop=\(if ($drop | length) == 0 then "none" else ($drop | join("+")) end)"+ " is_compliant=\(if $ok then "true" else "false" end)"] as $rows| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'Confirm that all listed containers now show
capabilitiesDrop=ALLandis_compliant=true(or that the output is justis_compliant=true).
Automation
#!/usr/bin/env bash
set -euo pipefail
# Automation for: Containers Should Drop All Linux Capabilities (CBP C1.5)
# Scope: Oracle OKE, all namespaces except: kube-system, kube-public, kube-node-lease
# Runs on: any machine with kubectl access and jq installed
if ! command -v kubectl >/dev/null 2>&1; then
echo "kubectl not found in PATH" >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "jq not found in PATH" >&2
exit 1
fi
WORKDIR="$(pwd)/capability-fix-$(date +%s)"
mkdir -p "${WORKDIR}"
echo "Discovering non-compliant pods..."
kubectl get pods --all-namespaces -o json | jq -r '
.items[]
| select(.metadata.namespace as $n
| ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| select((
((.spec.containers // []) + (.spec.initContainers // []))[]
| (.securityContext.capabilities.drop // []) as $drop
| (($drop | index("ALL")) or ($drop | index("all"))) == false
) // false)
| [.metadata.namespace, .metadata.name]
| @tsv' | sort -u > "${WORKDIR}/non_compliant_pods.tsv" || true
if [[ ! -s "${WORKDIR}/non_compliant_pods.tsv" ]]; then
echo "No non-compliant pods found. Nothing to change."
else
echo "Found non-compliant pods:"
cat "${WORKDIR}/non_compliant_pods.tsv"
fi
fix_pod() {
local ns="$1"
local pod="$2"
echo "Processing Pod ${ns}/${pod}..."
# Determine owner (Deployment/StatefulSet/DaemonSet/Job/CronJob) if any
owner_json="$(kubectl get pod "${pod}" -n "${ns}" -o jsonpath='{.metadata.ownerReferences}' 2>/dev/null || echo "")"
owner_kind=""
owner_name=""
if [[ -n "${owner_json}" && "${owner_json}" != "null" ]]; then
owner_kind="$(kubectl get pod "${pod}" -n "${ns}" -o jsonpath='{.metadata.ownerReferences[0].kind}' || true)"
owner_name="$(kubectl get pod "${pod}" -n "${ns}" -o jsonpath='{.metadata.ownerReferences[0].name}' || true)"
fi
# Decide target resource: prefer owner controller if present
target_kind="Pod"
target_name="${pod}"
if [[ -n "${owner_kind}" && -n "${owner_name}" ]]; then
target_kind="${owner_kind}"
target_name="${owner_name}"
fi
# Get raw YAML for target
yaml_path="${WORKDIR}/${ns}-${target_kind,,}-${target_name}.yaml"
echo " Fetching ${target_kind} ${ns}/${target_name}..."
if ! kubectl get "${target_kind,,}" "${target_name}" -n "${ns}" -o yaml > "${yaml_path}"; then
echo " WARNING: Failed to fetch ${target_kind} ${ns}/${target_name}, skipping." >&2
return
fi
# Create a patched copy
patched_path="${yaml_path%.yaml}.patched.yaml"
cp "${yaml_path}" "${patched_path}"
# Use yq if available for safer YAML edits; otherwise use jq on JSON
if command -v yq >/dev/null 2>&1; then
echo " Patching capabilities with yq..."
yq '
(.. | select(has("containers")) ) as $p
| . as $root
| ($root | path($p)) as $path
| ($root | getpath($path)) as $obj
| ($obj.containers // []) |=
(map(
.securityContext.capabilities.drop =
( ((.securityContext.capabilities.drop // [])
| map(select(. != "all" and . != "ALL"))
+ ["ALL"]) | unique )
))
| ($obj.initContainers // []) |=
(map(
.securityContext.capabilities.drop =
( ((.securityContext.capabilities.drop // [])
| map(select(. != "all" and . != "ALL"))
+ ["ALL"]) | unique )
))
' "${yaml_path}" > "${patched_path}.tmp" && mv "${patched_path}.tmp" "${patched_path}"
else
echo " yq not found; patching via JSON round-trip with jq..."
json_path="${yaml_path%.yaml}.json"
json_patched_path="${yaml_path%.yaml}.patched.json"
kubectl get "${target_kind,,}" "${target_name}" -n "${ns}" -o json > "${json_path}"
jq '
def ensure_drop_all:
.securityContext |= ( . // {} )
| .securityContext.capabilities |= ( . // {} )
| .securityContext.capabilities.drop |= (
( . // [] )
| map(select(. != "all" and . != "ALL"))
| . + ["ALL"]
| unique
);
if .spec.template? then
.spec.template.spec.containers |= (map(ensure_drop_all)) |
.spec.template.spec.initContainers |= (map(ensure_drop_all) // .)
else
.spec.containers |= (map(ensure_drop_all)) |
.spec.initContainers |= (map(ensure_drop_all) // .)
end
' "${json_path}" > "${json_patched_path}"
# Convert back to YAML with kubectl
kubectl apply -f "${json_patched_path}" --dry-run=client -o yaml > "${patched_path}"
fi
echo " Applying patched manifest for ${target_kind} ${ns}/${target_name}..."
kubectl apply -f "${patched_path}"
echo " Waiting for updated pods from ${target_kind} ${ns}/${target_name} to be Ready..."
case "${target_kind}" in
Deployment|StatefulSet|DaemonSet)
kubectl rollout status "${target_kind,,}/${target_name}" -n "${ns}" --timeout=5m || \
echo " WARNING: rollout status for ${target_kind} ${ns}/${target_name} did not complete in time." >&2
;;
CronJob)
echo " NOTE: CronJob pods will reflect changes on next scheduled run."
;;
Job|Pod)
kubectl get pod -n "${ns}" "${pod}" >/dev/null 2>&1 || true
;;
esac
}
if [[ -s "${WORKDIR}/non_compliant_pods.tsv" ]]; then
while IFS=$'\t' read -r ns pod; do
fix_pod "${ns}" "${pod}"
done < "${WORKDIR}/non_compliant_pods.tsv"
fi
echo
echo "Verifying compliance after remediation..."
kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| ((.spec.containers // []) + (.spec.initContainers // []))[]
| (.securityContext.capabilities.drop // []) as $drop
| (($drop | index("ALL")) or ($drop | index("all"))) as $ok
| select($ok | not)
] | if length == 0 then "is_compliant=true" else "is_compliant=false" end'