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 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.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
    | select($ok | not)
    | "ns=\($m.namespace) pod=\($m.name) container=\(.name)"
    ][]'
  2. For a noncompliant Pod created directly (not via Deployment/ReplicaSet/etc.), edit the Pod spec (any machine with kubectl access):

    kubectl -n <namespace> get pod <pod-name> -o yaml > /tmp/pod-fixed.yaml

    In /tmp/pod-fixed.yaml, under each .spec.containers[].securityContext and .spec.initContainers[].securityContext:

    securityContext:
    capabilities:
    drop:
    - "ALL"
    # add back only what is required, for example:
    # add:
    # - "NET_BIND_SERVICE"

    Then apply and recreate (Pods cannot be updated in place for some fields, so delete and recreate if needed):

    kubectl -n <namespace> delete pod <pod-name>
    kubectl -n <namespace> apply -f /tmp/pod-fixed.yaml
  3. For Pods managed by a higher-level controller (e.g., Deployment, StatefulSet, DaemonSet), find the owning resource (any machine with kubectl access):

    kubectl -n <namespace> get pod <pod-name> -o jsonpath='{.metadata.ownerReferences[0].kind}{" "}{.metadata.ownerReferences[0].name}{"\n"}'

    Then fetch the controller manifest:

    kubectl -n <namespace> get <Kind-lowercased> <owner-name> -o yaml > /tmp/owner-fixed.yaml
  4. Edit the controller manifest to drop all capabilities (any machine with kubectl access). In /tmp/owner-fixed.yaml, under each container and initContainer template path like:

    • Deployments: .spec.template.spec.containers[] and .spec.template.spec.initContainers[]
    • StatefulSets/DaemonSets/ReplicaSets: same template path

    Add or modify:

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

    Save the file.

  5. Apply the updated controller spec and wait for Pods to be recreated (any machine with kubectl access):

    kubectl -n <namespace> apply -f /tmp/owner-fixed.yaml
    kubectl -n <namespace> rollout status deployment/<owner-name> # for a Deployment
    # or:
    # kubectl -n <namespace> rollout status daemonset/<owner-name>
    # kubectl -n <namespace> rollout status statefulset/<owner-name>
  6. Verify compliance (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'
Using kubectl
# 1) Identify non-compliant 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
| (.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' \
| grep 'is_compliant=false' || echo "All non-system pods compliant"

# 2) Export the owning workload manifest (example: Deployment)
# Replace <namespace> and <deployment-name> with real values from step 1
# Run on: any machine with kubectl access
kubectl -n <namespace> get deploy <deployment-name> -o yaml > /tmp/deploy-<deployment-name>.yaml

# 3) Edit the manifest to drop ALL capabilities for every container
# Open /tmp/deploy-<deployment-name>.yaml in an editor and, for each container
# under spec.template.spec.containers and spec.template.spec.initContainers,
# add or modify securityContext like:

# ...
# spec:
# template:
# spec:
# containers:
# - name: app
# image: your-image:tag
# securityContext:
# capabilities:
# drop:
# - "ALL"
# # OPTIONAL: if the workload truly needs specific capabilities, add them back explicitly:
# # add:
# # - "NET_BIND_SERVICE"
# # other fields...
# initContainers:
# - name: init
# image: your-init-image:tag
# securityContext:
# capabilities:
# drop:
# - "ALL"
# # add: [] # or specific capabilities if absolutely required

# 4) Apply the updated manifest
# Run on: any machine with kubectl access
kubectl apply -f /tmp/deploy-<deployment-name>.yaml

# 5) Repeat steps 2–4 for each non-compliant owning object
# (e.g., Deployment, DaemonSet, StatefulSet, Job, CronJob, or standalone Pod)

# 6) Verification: re-run the benchmark audit command
# 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'
Automation
#!/usr/bin/env bash
set -euo pipefail

# Automation for CBP C1.5 on AKS:
# Ensure every container/initContainer drops ALL capabilities
# for Pods that are NOT managed by higher-level controllers.
#
# This script:
# - Detects non-compliant Pods (excluding system namespaces)
# - Patches only standalone Pods (no controller ownerReferences)
# - Adds securityContext.capabilities.drop: ["ALL"] where missing
# - Is idempotent and safe to re-run
#
# Run on: any machine with kubectl access to the AKS cluster

# Prereqs: kubectl, jq, yq (https://github.com/mikefarah/yq) v4+

command -v kubectl >/dev/null 2>&1 || { echo "kubectl not found"; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "jq not found"; exit 1; }
command -v yq >/dev/null 2>&1 || { echo "yq not found (v4 required)"; exit 1; }

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

echo "Discovering non-compliant standalone 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)
# Only Pods without controller ownerReferences (standalone Pods)
| select(((.metadata.ownerReferences // [])
| map(select(.controller == true)) | length) == 0)
as $pod
| ($pod.spec.containers // [] + $pod.spec.initContainers // []) as $cs
| [$cs[]
| (.securityContext.capabilities.drop // []) as $drop
| (($drop | index("ALL")) or ($drop | index("all"))) as $ok
| select($ok | not)
] as $noncompliant_containers
| select(($noncompliant_containers | length) > 0)
| "\($pod.metadata.namespace) \($pod.metadata.name)"
' > "$WORKDIR/noncompliant_pods.txt"

if [[ ! -s "$WORKDIR/noncompliant_pods.txt" ]]; then
echo "No non-compliant standalone Pods found; nothing to change."
else
echo "Found the following non-compliant standalone Pods:"
cat "$WORKDIR/noncompliant_pods.txt"
fi

while read -r NS NAME; do
[[ -z "$NS" || -z "$NAME" ]] && continue
echo "Processing Pod ${NS}/${NAME} ..."

YAML_FILE="$WORKDIR/${NS}_${NAME}.yaml"
PATCH_FILE="$WORKDIR/${NS}_${NAME}_patch.yaml"

kubectl get pod "$NAME" -n "$NS" -o yaml > "$YAML_FILE"

# Build a strategic merge patch that:
# - ensures securityContext exists on each container/initContainer
# - ensures capabilities.drop contains "ALL"
#
# This is done by transforming the live YAML into a patch that
# only touches the relevant fields.
yq eval '
.spec |= (
. // {} |
.containers |= (
. // [] |
map(
.securityContext |= (. // {}) |
.securityContext.capabilities |= (. // {}) |
.securityContext.capabilities.drop |= (
. // [] |
(if (map(ascii_upcase) | index("ALL")) == null
then . + ["ALL"] | unique
else .
end)
)
)
) |
.initContainers |= (
. // [] |
map(
.securityContext |= (. // {}) |
.securityContext.capabilities |= (. // {}) |
.securityContext.capabilities.drop |= (
. // [] |
(if (map(ascii_upcase) | index("ALL")) == null
then . + ["ALL"] | unique
else .
end)
)
)
)
)
# Reduce to only the patch-relevant structure
| {spec: {containers: .spec.containers, initContainers: .spec.initContainers}}
' "$YAML_FILE" > "$PATCH_FILE"

# If nothing meaningful changed, skip
if diff -q <(yq '.spec' "$YAML_FILE") <(yq '.spec *+ (load("'"$PATCH_FILE"'") | .spec)' "$YAML_FILE") >/dev/null 2>&1; then
echo " No changes required for ${NS}/${NAME} (already compliant)."
continue
fi

echo " Applying patch to ${NS}/${NAME} ..."
kubectl patch pod "$NAME" -n "$NS" --type merge --patch-file "$PATCH_FILE"

done < "$WORKDIR/noncompliant_pods.txt"

echo
echo "Re-running compliance check to verify..."

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)
# Only standalone Pods
| select(((.metadata.ownerReferences // [])
| map(select(.controller == true)) | length) == 0)
| .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 standalone Pods are now compliant (drop ALL)."
else "Non-compliant standalone Pods still exist; investigate controller-managed workloads separately."
end
'