Skip to main content

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

Manual Steps
  1. Identify offending pods and containers (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.containers // []) + (.spec.initContainers // []))[]
    | (.securityContext.capabilities.drop // []) as $drop
    | (($drop | index("ALL")) or ($drop | index("all"))) as $ok
    | select($ok | not)
    | "\($m.namespace) \($m.name) \(.name)"
    ][]'

    Each line is: NAMESPACE POD_NAME CONTAINER_NAME. Use it as input for the next steps.

  2. For each non-compliant workload managed by a higher-level controller (Deployment, StatefulSet, DaemonSet, Job, CronJob), edit the controller manifest (run on any machine with kubectl access):

    kubectl -n <NAMESPACE> get deployment <DEPLOYMENT_NAME> -o yaml > /tmp/deploy-<DEPLOYMENT_NAME>.yaml

    Or replace deployment with statefulset, daemonset, job, or cronjob as appropriate.

  3. In the saved manifest file, add a securityContext.capabilities.drop: ["ALL"] to every container (including initContainers) under spec.template.spec. For example:

    spec:
    template:
    spec:
    containers:
    - name: app
    image: your-image
    securityContext:
    capabilities:
    drop:
    - "ALL"
    # add back only what is strictly needed, e.g.:
    # add:
    # - NET_BIND_SERVICE
    initContainers:
    - name: init
    image: your-init-image
    securityContext:
    capabilities:
    drop:
    - "ALL"

    If specific Linux capabilities are required, list them under add: explicitly; otherwise omit add:.

  4. Apply the updated manifest (run on any machine with kubectl access):

    kubectl apply -f /tmp/deploy-<DEPLOYMENT_NAME>.yaml

    Repeat Steps 2–4 for each affected controller. This will cause controlled rollouts of new pods.

  5. For stand-alone Pods (not owned by a controller), recreate them with the proper securityContext (run on any machine with kubectl access):

    kubectl -n <NAMESPACE> get pod <POD_NAME> -o yaml > /tmp/pod-<POD_NAME>.yaml

    Edit /tmp/pod-<POD_NAME>.yaml:

    • Remove metadata.uid, metadata.resourceVersion, metadata.creationTimestamp, metadata.managedFields, status, and any ownerReferences.
    • Under spec.containers[] and spec.initContainers[] add:
      securityContext:
      capabilities:
      drop:
      - "ALL"
      # add:
      # - <ONLY-REQUIRED-CAPABILITY>

    Then delete and recreate:

    kubectl -n <NAMESPACE> delete pod <POD_NAME>
    kubectl apply -f /tmp/pod-<POD_NAME>.yaml
  6. Verify compliance (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.containers // []) + (.spec.initContainers // []))[]
    | (.securityContext.capabilities.drop // []) as $drop
    | (($drop | index("ALL")) or ($drop | index("all"))) as $ok
    | select($ok | not)
    ] as $rows
    | if ($rows | length) == 0 then "is_compliant=true" else "is_compliant=false" end'

    The output must be is_compliant=true.

Using kubectl

On any machine with kubectl access to the cluster:

  1. Identify the noncompliant pod and its owning controller (from the audit output’s owner= field). For example, if you see:

    owner=Deployment/default/my-app/...

    then you must edit the Deployment/my-app in the default namespace (never edit only the live Pod).

  2. Edit the controller and add the capabilities drop to every container (including initContainers if present). Example for a Deployment:

    kubectl -n default edit deployment my-app

    In the opened manifest, under each container, add a securityContext.capabilities.drop: ["ALL"]. If the container truly needs a specific capability, add it back under add.

    Example spec.template.spec snippet:

    spec:
    template:
    spec:
    securityContext:
    runAsNonRoot: true
    containers:
    - name: app
    image: nginx:1.27
    securityContext:
    allowPrivilegeEscalation: false
    capabilities:
    drop:
    - "ALL"
    add:
    - "NET_BIND_SERVICE" # only if strictly required
    # ...
    initContainers:
    - name: init-job
    image: busybox:1.36
    securityContext:
    capabilities:
    drop:
    - "ALL"
    # add: [] or omit unless a specific capability is needed

    Save and exit; Kubernetes will roll out updated Pods with the new securityContext.

  3. For objects not managed by higher-level controllers (for example, a standalone Pod manifest in Git), update the source manifest similarly, then apply:

    kubectl apply -f /absolute/path/to/pod-or-controller.yaml
  4. Verification (from any machine with kubectl):

    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 // []) | map(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) container=\(.name) is_compliant=\(if $ok then "true" else "false" end)"
    ] as $rows
    | if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'

    Confirm all listed is_compliant= values are true (or that no rows are output, meaning all applicable pods comply).

Automation
#!/usr/bin/env bash
set -euo pipefail

# Automation for: Containers Should Drop ALL Linux Capabilities (CBP C1.5)
# 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

TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT

echo "Identifying non-compliant Pods (excluding kube-system, kube-public, kube-node-lease)..."

# Get namespaced Pod list that is non-compliant (no ALL/all in capabilities.drop)
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 // [])) as $allc
| [ $allc[]
| (.securityContext.capabilities.drop // []) as $drop
| (($drop | index("ALL")) or ($drop | index("all"))) as $ok
| select($ok | not)
] as $badContainers
| select(($badContainers | length) > 0)
| "\($m.namespace) \($m.name)"
' | sort -u > "$TMPDIR/non_compliant_pods.txt"

if ! [[ -s "$TMPDIR/non_compliant_pods.txt" ]]; then
echo "All Pods already drop ALL capabilities (excluding system namespaces). Nothing to do."
else
echo "Non-compliant Pods detected:"
cat "$TMPDIR/non_compliant_pods.txt"
fi

# Function: patch a single Pod manifest yaml to ensure ALL is in drop list for all containers/initContainers
patch_pod_yaml() {
local in_yaml=$1 out_yaml=$2

# jq-based transformation via yq (if available) or kubectl-kustomize-style is complex.
# Use yq v4 if present for robust YAML editing.
if command -v yq >/dev/null 2>&1; then
yq '
.spec |= (
if has("containers") then
.containers |= map(
.securityContext.capabilities.drop |= (
(//[]) as $d
| if ($d | index("ALL")) != null then $d else ($d + ["ALL"]) end
)
)
else . end
)
| .spec |= (
if has("initContainers") then
.initContainers |= map(
.securityContext.capabilities.drop |= (
(//[]) as $d
| if ($d | index("ALL")) != null then $d else ($d + ["ALL"]) end
)
)
else . end
)
' "$in_yaml" > "$out_yaml"
return 0
fi

# Fallback: server-side strategic merge patch using kubectl (no local YAML change)
# This function is not used when yq is missing.
return 1
}

if [[ -s "$TMPDIR/non_compliant_pods.txt" ]]; then
echo
echo "Patching non-compliant Pods (type: Pod only; higher-level controllers must be fixed via their manifests separately)."

while read -r NS NAME; do
[[ -z "$NS" || -z "$NAME" ]] && continue

echo "Processing Pod: $NS/$NAME"

POD_JSON="$(kubectl get pod "$NAME" -n "$NS" -o json)"

# Build a strategic merge patch that adds "ALL" to drop for each container/initContainer
PATCH="$(echo "$POD_JSON" | jq '
{
"spec": {
"containers": ( (.spec.containers // []) | map(
{
"name": .name,
"securityContext": {
"capabilities": {
"drop": (
(
((.securityContext.capabilities.drop // []) | map(
if . == "all" then "ALL" else . end
)) as $d
| if ($d | index("ALL")) != null then $d else ($d + ["ALL"]) end
)
)
}
}
}
)),
"initContainers": ( (.spec.initContainers // []) | map(
{
"name": .name,
"securityContext": {
"capabilities": {
"drop": (
(
((.securityContext.capabilities.drop // []) | map(
if . == "all" then "ALL" else . end
)) as $d
| if ($d | index("ALL")) != null then $d else ($d + ["ALL"]) end
)
)
}
}
}
))
}
}
')"

echo "$PATCH" > "$TMPDIR/patch-$NS-$NAME.json"

kubectl patch pod "$NAME" -n "$NS" --type merge -p "$(cat "$TMPDIR/patch-$NS-$NAME.json")" >/dev/null

done < "$TMPDIR/non_compliant_pods.txt"
fi

echo
echo "Re-running compliance audit to verify result..."

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)
] as $rows
| if ($rows | length) == 0 then
"All non-system Pods now have securityContext.capabilities.drop including ALL (is_compliant=true)"
else
"Some Pods are still non-compliant (is_compliant=false). Review owning controllers and their manifests."
end
'