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 noncompliant pods (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) | .kind + "/" + $m.namespace + "/" + .name) // ("Pod/" + $m.namespace + "/" + $m.name)
    ] | unique[]'
  2. For a workload you control (example: Deployment in namespace “prod”), retrieve its manifest (run on any machine with kubectl access):

    kubectl -n prod get deployment my-app -o yaml > /tmp/my-app-deployment.yaml
  3. Edit the manifest to drop all capabilities and add back only what is needed (run on any machine with kubectl access):

    • Open the file:
      nano /tmp/my-app-deployment.yaml
    • Under each containers[] and initContainers[] item, add or update:
      securityContext:
      capabilities:
      drop:
      - "ALL"
      # add:
      # - "NET_BIND_SERVICE"
    • Remove any unneeded capabilities from add:; keep only strictly required ones.
  4. Apply the updated manifest (run on any machine with kubectl access):

    kubectl apply -f /tmp/my-app-deployment.yaml
  5. Repeat steps 2–4 for each noncompliant controller or standalone Pod you manage (e.g., deployment, statefulset, daemonset, job, cronjob, or pod).

  6. Verify all non-excluded namespaces are compliant (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'

    Confirm remaining lines (if any) all show is_compliant=true.

Using kubectl

On any machine with kubectl access:

  1. Identify non‑compliant pods and their owning controllers
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)
| "\($m.namespace) \($m.name) \($own.kind) \($own.name)"
] | unique[]' | column -t

This lists: NAMESPACE POD OWNER_KIND OWNER_NAME. For entries where OWNER_KIND is empty, you must edit the Pod directly; otherwise edit the owning object (Deployment, StatefulSet, DaemonSet, Job, CronJob, etc.).

  1. Edit the owning controller manifest(s)

For each non‑compliant controller (example: a Deployment in namespace prod named web):

kubectl -n prod edit deployment web

Under every spec.template.spec.containers[] and spec.template.spec.initContainers[] entry, add:

securityContext:
capabilities:
drop:
- "ALL"
# add:
# - "CAP_NEEDED_BY_APP"

Maintain any existing securityContext fields; just merge the capabilities stanza. Save and exit to apply.

  1. Edit standalone Pods (no controller)

For a Pod in namespace default named test-pod with no owner:

kubectl -n default edit pod test-pod

Add the same block to each container and initContainer:

securityContext:
capabilities:
drop:
- "ALL"
# add:
# - "CAP_NEEDED_BY_APP"

Note: direct Pod edits are not persisted if something external (e.g., Helm, GitOps) recreates them; prefer fixing source manifests/IaC where applicable.

  1. Declarative example for future manifests

When authoring or updating manifests, ensure each container includes:

apiVersion: apps/v1
kind: Deployment
metadata:
name: example
namespace: prod
spec:
replicas: 1
selector:
matchLabels:
app: example
template:
metadata:
labels:
app: example
spec:
containers:
- name: app
image: gcr.io/PROJECT/IMAGE:TAG
securityContext:
capabilities:
drop:
- "ALL"
# add:
# - "CAP_NET_BIND_SERVICE"

Apply with:

kubectl apply -f example-deployment.yaml
  1. Verification

Run the benchmark audit command again 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 output is is_compliant=true only, or that each listed container shows is_compliant=true.

Automation
#!/usr/bin/env bash
# Purpose: Ensure all non-system Pods' containers drop ALL Linux capabilities.
# Platform: GKE (or any Kubernetes cluster accessible via current kube-context)
# Requirements: kubectl, jq, sed; current kube-context must target the cluster.

set -euo pipefail

# Namespace exclusions (match audit command)
EXCLUDED_NAMESPACES=("kube-system" "kube-public" "kube-node-lease")

log() { printf '%s\n' "$*" >&2; }

# Build jq filter to exclude system namespaces
jq_ns_filter='["kube-system","kube-public","kube-node-lease"] as $e
| select(.metadata.namespace as $n | $e | index($n) | not)'

# 1. Discover non-compliant Pods (jsonPointer to each offending container)
log "[INFO] Discovering non-compliant Pods/containers ..."
NON_COMPLIANT_JSON=$(kubectl get pods --all-namespaces -o json | jq -c '
.items[]
| '"$jq_ns_filter"'
| . as $pod
| ([($pod.spec.containers // [] | to_entries[] | . + {type:"containers"})]
+ [($pod.spec.initContainers // [] | to_entries[] | . + {type:"initContainers"})])[]
| . as $c
| ($pod.metadata.namespace // "") as $ns
| ($pod.metadata.name // "") as $podName
| ($c.type) as $ctype
| ($c.key | tostring) as $idx
| ($c.value.name // "") as $cname
| ($c.value.securityContext.capabilities.drop // []) as $drop
| (($drop | index("ALL")) or ($drop | index("all"))) as $ok
| select($ok | not)
| {
namespace: $ns,
pod: $podName,
containerType: $ctype,
containerIndex: $idx,
containerName: $cname
}
' || true)

if [[ -z "$NON_COMPLIANT_JSON" ]]; then
log "[INFO] No running Pods found or jq produced no output."
fi

if ! echo "$NON_COMPLIANT_JSON" | jq -e 'length' >/dev/null 2>&1; then
# jq -c emitted multiple lines; that's fine.
:
fi

# Aggregate into an array
mapfile -t NON_COMPLIANT <<<"$(printf '%s\n' "$NON_COMPLIANT_JSON" | sed '/^\s*$/d')"

if [[ ${#NON_COMPLIANT[@]} -eq 0 ]]; then
log "[INFO] All checked containers already drop ALL capabilities. Nothing to change."
else
log "[INFO] Found ${#NON_COMPLIANT[@]} non-compliant container entries."
fi

# 2. For each unique (namespace, pod), patch the Pod template in its controller
# by setting drop: ["ALL"] on all containers of that Pod kind.
# WARNING: This edits workload manifests; review diffs in git/IaC after running.

# Helper: deduplicate "owner kind/ns/name" triples per Pod using jq again
UNIQUE_OWNERS=$(kubectl get pods --all-namespaces -o json | jq -r '
.items[]
| '"$jq_ns_filter"'
| . as $pod
| ([($pod.spec.containers // [])[]] + [($pod.spec.initContainers // [])[]]) as $cs
| ($cs | map(.securityContext.capabilities.drop // []) | flatten) as $drops
| (($drops | index("ALL")) or ($drops | index("all"))) as $ok
| select($ok | not)
| ($pod.metadata.ownerReferences // []) as $owners
| ($owners | map(select(.controller)) | first) as $own
| if $own == null then empty else
"ns=\($pod.metadata.namespace) pod=\($pod.metadata.name) " +
"ok=" + ($ok|tostring) + " " +
"ownerKind=\($own.kind) ownerName=\($own.name) uid=\($own.uid)"
end
' | sort -u)

if [[ -z "$UNIQUE_OWNERS" ]]; then
log "[INFO] Non-compliant Pods appear to be unmanaged (no controller)."
log "[INFO] You must fix these Pod specs in their source manifests or Helm charts manually."
fi

log "[INFO] Patching controllers to enforce capabilities.drop=[\"ALL\"] where possible ..."

# Function to generate a generic patch for Deployments/StatefulSets/DaemonSets/Jobs/CronJobs
gen_patch() {
cat <<'EOF'
spec:
template:
spec:
containers:
- name: PLACEHOLDER
securityContext:
capabilities:
drop:
- "ALL"
initContainers:
- name: PLACEHOLDER
securityContext:
capabilities:
drop:
- "ALL"
EOF
}

while read -r line; do
[[ -z "$line" ]] && continue
ns=$(sed -n 's/.*ns=\([^ ]*\).*/\1/p' <<<"$line")
kind=$(sed -n 's/.*ownerKind=\([^ ]*\).*/\1/p' <<<"$line")
name=$(sed -n 's/.*ownerName=\([^ ]*\).*/\1/p' <<<"$line")

# Only handle common controller types via kubectl patch; others must be updated in source/IaC.
case "$kind" in
Deployment|StatefulSet|DaemonSet|Job|CronJob)
log "[INFO] Patching $kind/$ns/$name ..."
# Build a strategic merge patch that sets drop: ["ALL"] on all containers by name.
# We first fetch container names from the controller template.
tmpl_json=$(kubectl get "$kind" "$name" -n "$ns" -o json 2>/dev/null || true)
if [[ -z "$tmpl_json" ]]; then
log "[WARN] Could not fetch $kind/$ns/$name; skipping."
continue
fi

c_names=$(echo "$tmpl_json" | jq -r '.spec.template.spec.containers[].name' 2>/dev/null || true)
ic_names=$(echo "$tmpl_json" | jq -r '.spec.template.spec.initContainers[].name' 2>/dev/null || true)

patch_yaml="spec:
template:
spec:
"

if [[ -n "$c_names" ]]; then
patch_yaml+=" containers:
"
while read -r cn; do
[[ -z "$cn" ]] && continue
patch_yaml+=" - name: ${cn}
securityContext:
capabilities:
drop:
- \"ALL\"
"
done <<<"$c_names"
fi

if [[ -n "$ic_names" ]]; then
patch_yaml+=" initContainers:
"
while read -r icn; do
[[ -z "$icn" ]] && continue
patch_yaml+=" - name: ${icn}
securityContext:
capabilities:
drop:
- \"ALL\"
"
done <<<"$ic_names"
fi

printf '%s\n' "$patch_yaml" | kubectl patch "$kind" "$name" -n "$ns" --type merge -p "$(cat)" >/dev/null
;;
*)
log "[WARN] Owner kind $kind for $ns/$name is not handled automatically. Update its manifest/IaC to add capabilities.drop: [\"ALL\"]."
;;
esac
done <<<"$UNIQUE_OWNERS"

log "[INFO] Waiting for updated Pods to roll out ..."
sleep 10

# 3. Verification: re-run the audit with is_compliant summary
log "[INFO] Verifying that all applicable containers drop ALL capabilities ..."
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 | map(select(. | test("is_compliant=false$"))) | length) == 0
then "is_compliant=true"
else $rows[]
end
'