Skip to main content

Minimize Admission Of Privileged Containers

More Info:​

Do not generally permit containers to be run with the securityContext.privileged flag set to true.

Risk Level​

Critical

Address​

Security

Compliance Standards​

  • CIS Kubernetes

Triage and Remediation​

Remediation​

Manual Steps
  1. Identify all namespaces with user workloads (run on any machine with kubectl access):

    kubectl get ns

    Decide which namespaces should disallow privileged containers (typically all except core system namespaces like kube-system, kube-public, kube-node-lease, kube-admin equivalents).

  2. Create a baseline policy that denies privileged containers (run once on any machine with kubectl access; use Pod Security Admission via labels as the default, plus optional PodSecurityPolicy or AdmissionPolicy if supported in your cluster). Example using Pod Security Admission “restricted” level:

    # Label each target namespace to enforce restricted policy
    for ns in NAMESPACE1 NAMESPACE2 NAMESPACE3; do
    kubectl label namespace "$ns" \
    pod-security.kubernetes.io/enforce=restricted \
    pod-security.kubernetes.io/enforce-version=latest \
    pod-security.kubernetes.io/audit=restricted \
    pod-security.kubernetes.io/warn=restricted \
    --overwrite
    done

    Replace NAMESPACE1 NAMESPACE2 NAMESPACE3 with your chosen namespaces.

  3. (If PodSecurityPolicy is still in use in your cluster) Create a PSP that disallows privileged containers and a ClusterRole to use it (run on any machine with kubectl access):

    cat << 'EOF' | kubectl apply -f -
    apiVersion: policy/v1beta1
    kind: PodSecurityPolicy
    metadata:
    name: restricted-no-privileged
    spec:
    privileged: false
    allowPrivilegeEscalation: false
    hostPID: false
    hostIPC: false
    hostNetwork: false
    runAsUser:
    rule: 'MustRunAsNonRoot'
    seLinux:
    rule: 'RunAsAny'
    fsGroup:
    rule: 'RunAsAny'
    supplementalGroups:
    rule: 'RunAsAny'
    volumes:
    - 'configMap'
    - 'downwardAPI'
    - 'emptyDir'
    - 'persistentVolumeClaim'
    - 'projected'
    - 'secret'
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
    name: use-restricted-no-privileged-psp
    rules:
    - apiGroups: ['policy']
    resources: ['podsecuritypolicies']
    verbs: ['use']
    resourceNames: ['restricted-no-privileged']
    EOF
  4. (If PSP is in use) Bind the PSP to the service accounts that run user workloads in each target namespace (run on any machine with kubectl access):

    # Example: bind to all service accounts in a namespace
    for ns in NAMESPACE1 NAMESPACE2 NAMESPACE3; do
    kubectl create rolebinding "use-restricted-no-privileged-psp" \
    --clusterrole=use-restricted-no-privileged-psp \
    --group=system:serviceaccounts:"$ns" \
    --namespace="$ns" \
    --dry-run=client -o yaml | kubectl apply -f -
    done
  5. Manually review and update existing workloads that currently request privileged containers to either:

    • Remove securityContext.privileged: true, or
    • Move them into carefully controlled namespaces with different admission policy and an explicit exception process.

    To locate and inspect offending pods (run on any machine with kubectl access):

    # List 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_privileged=$(echo ${container} | jq -r '.securityContext.privileged' | sed -e 's/null/notset/g')
    if [ "${container_privileged}" = "true" ]; then
    echo "NON-COMPLIANT: pod=${pod_name} ns=${pod_namespace} container=${container_name}"
    fi
    done
    done

    For each non-compliant Deployment/DaemonSet/StatefulSet/etc., edit the manifest to remove securityContext.privileged: true and redeploy:

    kubectl -n TARGET_NAMESPACE edit deployment TARGET_DEPLOYMENT

    Remove or set privileged: false under the container’s securityContext, save, and let the controller recreate pods.

  6. Verification (run 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_privileged=$(echo ${container} | jq -r '.securityContext.privileged' | sed -e 's/null/notset/g')
    if [ "${container_privileged}" = "false" ] || [ "${container_privileged}" = "notset" ] ; then
    echo "***pod_name: ${pod_name} container_name: ${container_name} pod_namespace: ${pod_namespace} is_container_privileged: ${container_privileged} is_compliant: true"
    else
    echo "***pod_name: ${pod_name} container_name: ${container_name} pod_namespace: ${pod_namespace} is_container_privileged: ${container_privileged} is_compliant: false"
    fi
    done
    done

    Confirm that no lines show is_compliant: false for namespaces where privileged containers should be minimized.

Using kubectl
# Apply once per workload namespace (edit metadata.name as needed)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-privileged-containers
spec:
validationFailureAction: Enforce
background: true
rules:
- name: validate-privileged
match:
any:
- resources:
kinds:
- Pod
- Deployment
- ReplicaSet
- DaemonSet
- StatefulSet
- Job
- CronJob
validate:
message: "Privileged containers are not allowed. Do not set securityContext.privileged: true."
pattern:
spec:
containers:
- =(securityContext):
=(privileged): "false"
=(initContainers):
- =(securityContext):
=(privileged): "false"
# On any machine with kubectl access: create the policy
kubectl apply -f disallow-privileged-containers.yaml

# Optional: dry-run creation of a privileged pod to confirm it is blocked
kubectl run test-privileged \
--image=busybox \
--restart=Never \
--overrides='{"apiVersion":"v1","kind":"Pod","metadata":{"name":"test-privileged"}, "spec":{"containers":[{"name":"c","image":"busybox","securityContext":{"privileged":true},"command":["sh","-c","sleep 3600"]}]}}' \
--dry-run=server -o yaml

Verification (same style as audit, 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_privileged=$(echo ${container} | jq -r '.securityContext.privileged' | sed -e 's/null/notset/g')
if [ "${container_privileged}" = "false" ] || [ "${container_privileged}" = "notset" ] ; then
echo "***pod_name: ${pod_name} container_name: ${container_name} pod_namespace: ${pod_namespace} is_container_privileged: ${container_privileged} is_compliant: true"
else
echo "***pod_name: ${pod_name} container_name: ${container_name} pod_namespace: ${pod_namespace} is_container_privileged: ${container_privileged} is_compliant: false"
fi
done
done
Automation
#!/usr/bin/env bash
set -euo pipefail

# Purpose:
# - For each namespace with user workloads, create/patch a PodSecurityPolicy-like
# restriction using Pod Security Admission labels to forbid privileged pods.
# - This is MANUAL in CIS: script helps you detect + optionally enforce.
#
# Requirements:
# - Run on any machine with kubectl and jq configured for the target cluster.
# - Kubernetes v1.25+ recommended (PodSecurity Admission GA).
#
# OPERATIONAL IMPACT:
# - After enforcement, any new pod (or updated existing pod) with
# securityContext.privileged=true will be rejected in restricted namespaces.
# - Existing running privileged pods are NOT evicted automatically.

#---------------------------- Configurable options ----------------------------#

# Namespaces to exclude from enforcement (space-separated, exact names).
# Add control-plane / system namespaces and any namespace where privileged
# containers are explicitly required and approved via risk acceptance.
EXCLUDED_NAMESPACES=(
"kube-system"
"kube-public"
"kube-node-lease"
"default" # remove this if you want default namespace restricted too
"kube-tools" # example; remove or change as needed
)

# Pod Security Admission level to enforce:
# Valid: privileged | baseline | restricted
# Use "restricted" to strongly minimize privileged / insecure workloads.
PSA_ENFORCEMENT_LEVEL="restricted"

# If true, only report non-compliance but do NOT change any namespaces.
DRY_RUN=${DRY_RUN:-"false"} # set DRY_RUN=true in environment to only audit

#---------------------------- Helper functions ----------------------------#

contains() {
local e match="$1"
shift
for e; do [[ "$e" == "$match" ]] && return 0; done
return 1
}

log() { printf '[INFO] %s\n' "$*" >&2; }
warn() { printf '[WARN] %s\n' "$*" >&2; }
err() { printf '[ERR ] %s\n' "$*" >&2; }

require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
err "Required command '$1' not found in PATH"
exit 1
}
}

#---------------------------- Pre-flight checks -----------------------------#

require_cmd kubectl
require_cmd jq

# Verify cluster access
if ! kubectl version --request-timeout='5s' >/dev/null 2>&1; then
err "kubectl cannot reach the cluster or is not authorized"
exit 1
fi

#---------------------------- Detect privileged pods ------------------------#

log "Scanning for containers with securityContext.privileged=true ..."

NON_COMPLIANT_FOUND=0

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_privileged=$(echo "${container}" | jq -r '.securityContext.privileged' | sed -e 's/null/notset/g')
if [ "${container_privileged}" = "true" ]; then
printf 'NON_COMPLIANT pod_name=%s container_name=%s pod_namespace=%s privileged=%s\n' \
"${pod_name}" "${container_name}" "${pod_namespace}" "${container_privileged}"
NON_COMPLIANT_FOUND=1
fi
done
done

# Note: NON_COMPLIANT_FOUND is in a subshell above and will not propagate here;
# we instead rely on a verification run later. This block is for operator output.

log "Initial privileged container scan complete."

#---------------------------- Enforce namespace policies --------------------#

log "Discovering namespaces to label with Pod Security Admission..."

NAMESPACES=$(kubectl get ns -o jsonpath='{.items[*].metadata.name}')

APPLIED_COUNT=0

for ns in ${NAMESPACES}; do
if contains "${ns}" "${EXCLUDED_NAMESPACES[@]}"; then
log "Skipping excluded namespace: ${ns}"
continue
fi

# Read current labels
current_enforce=$(kubectl get ns "${ns}" -o jsonpath='{.metadata.labels.pod-security\.kubernetes\.io/enforce}' 2>/dev/null || true)

# Decide whether to patch
if [ "${current_enforce}" = "${PSA_ENFORCEMENT_LEVEL}" ]; then
log "Namespace ${ns} already has enforce=${PSA_ENFORCEMENT_LEVEL}; no change."
continue
fi

if [ "${DRY_RUN}" = "true" ]; then
warn "DRY RUN: would set pod-security.kubernetes.io/enforce=${PSA_ENFORCEMENT_LEVEL} on namespace ${ns}"
continue
fi

log "Patching namespace ${ns} to enforce Pod Security level: ${PSA_ENFORCEMENT_LEVEL}"

kubectl label namespace "${ns}" \
"pod-security.kubernetes.io/enforce=${PSA_ENFORCEMENT_LEVEL}" \
"pod-security.kubernetes.io/enforce-version=latest" \
--overwrite

APPLIED_COUNT=$((APPLIED_COUNT + 1))
done

log "Namespace labeling complete. Namespaces updated: ${APPLIED_COUNT}"

#---------------------------- Verification ----------------------------------#

log "Verifying that privileged containers are either absent or blocked by policy."

# 1) Show current privileged containers (if any remain).
log "Re-running privileged container audit:"
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_privileged=$(echo "${container}" | jq -r '.securityContext.privileged' | sed -e 's/null/notset/g')
if [ "${container_privileged}" = "false" ] || [ "${container_privileged}" = "notset" ]; then
printf 'COMPLIANT pod_name=%s container_name=%s pod_namespace=%s privileged=%s\n' \
"${pod_name}" "${container_name}" "${pod_namespace}" "${container_privileged}"
else
printf 'NON_COMPLIANT pod_name=%s container_name=%s pod_namespace=%s privileged=%s\n' \
"${pod_name}" "${container_name}" "${container_name}" "${container_privileged}"
fi
done
done

# 2) Show namespaces and their Pod Security enforce level for review.
log "Current Pod Security enforce labels per namespace:"
kubectl get ns -o custom-columns=NAME:.metadata.name,ENFORCE:'".metadata.labels.pod-security\.kubernetes\.io/enforce"' --show-labels

log "Automation completed. Review NON_COMPLIANT lines above and handle any remaining privileged workloads per policy (exceptions, redesign, or migration to excluded namespaces)."

Additional Reading:​