Skip to main content

Minimize The Admission Of Privileged Containers

More Info:

Privileged containers have effectively unrestricted host access and can compromise the node. Block their admission in workload namespaces.

Risk Level

Critical

Address

Security

Compliance Standards

  • CIS Kubernetes

Triage and Remediation

Remediation

Manual Steps
  1. Identify workload namespaces and existing privileged pods
    Run on: any machine with kubectl access

    # List namespaces (exclude obvious system ones; adjust as needed)
    kubectl get ns

    # List current privileged containers (for awareness)
    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
  2. Decide namespace policy scope and exceptions
    Run on: administrator’s workstation (planning)

    • For each workload namespace, decide whether all privileged containers must be denied, or whether specific service accounts/namespaces need an exception (for example, infrastructure tooling).
    • For any allowed exceptions, plan separate namespaces or dedicated service accounts with clearly documented justification, since this control is MANUAL and risk-based.
  3. Create a baseline policy to deny privileged containers in a namespace
    Run on: any machine with kubectl access
    Example using the built-in PodSecurity admission (Kubernetes ≥1.25). Replace <NAMESPACE> with an actual workload namespace and repeat for each one where privileged containers must be blocked.

    kubectl label namespace <NAMESPACE> \
    pod-security.kubernetes.io/enforce=restricted \
    pod-security.kubernetes.io/enforce-version=latest \
    pod-security.kubernetes.io/warn=restricted \
    pod-security.kubernetes.io/warn-version=latest \
    pod-security.kubernetes.io/audit=restricted \
    pod-security.kubernetes.io/audit-version=latest --overwrite

    This label set enforces the “restricted” Pod Security level, which disallows privileged containers (securityContext.privileged: true) in that namespace.

  4. (If PodSecurity admission is not available) Apply a restrictive admission policy
    Run on: any machine with kubectl access
    If your cluster uses a policy engine (for example, Kubernetes-native PodSecurityPolicies on legacy clusters, or Gatekeeper/Kyverno), implement or tighten the admission policy for each workload namespace so that containers with .securityContext.privileged: true are rejected. Example Kyverno ClusterPolicy (adjust names/namespaces to your environment before applying):

    cat << 'EOF' > deny-privileged-containers.yaml
    apiVersion: kyverno.io/v1
    kind: ClusterPolicy
    metadata:
    name: deny-privileged-containers
    spec:
    validationFailureAction: enforce
    background: true
    rules:
    - name: disallow-privileged
    match:
    any:
    - resources:
    kinds:
    - Pod
    namespaces:
    - "<NAMESPACE1>"
    - "<NAMESPACE2>"
    validate:
    message: "Privileged containers are not allowed."
    pattern:
    spec:
    containers:
    - name: "*"
    =(securityContext):
    =(privileged): "false"
    EOF

    kubectl apply -f deny-privileged-containers.yaml

    Review this policy with your security team before applying, and tailor namespace lists and any needed exceptions.

  5. Refactor or remove existing privileged workloads
    Run on: any machine with kubectl access
    For each privileged pod identified in step 1 in user/workload namespaces:

    • Retrieve and edit the manifest:
      kubectl get pod <POD_NAME> -n <NAMESPACE> -o yaml > /tmp/<POD_NAME>.yaml
    • In /tmp/<POD_NAME>.yaml, remove securityContext.privileged: true from each container, or set it to false, and adjust capabilities/host access to meet functional needs without privilege.
    • Recreate the workload (ideally via its Deployment/DaemonSet/Job manifests rather than directly from a Pod):
      # If the pod is managed by a controller, edit the controller instead
      kubectl edit deployment <DEPLOYMENT_NAME> -n <NAMESPACE>
      # or apply the corrected manifest
      kubectl apply -f /tmp/<WORKLOAD>.yaml
    • Where privileged access is truly required and accepted, move such workloads into a clearly designated, tightly controlled namespace and exempt it deliberately from the restrictive policy, with documented approval.
  6. Verify that privileged containers are no longer admitted
    Run on: any machine with kubectl access

    • Attempt to create a test privileged pod in a protected workload namespace; it should be rejected:
      cat << 'EOF' | kubectl apply -n <NAMESPACE> -f -
      apiVersion: v1
      kind: Pod
      metadata:
      name: test-privileged
      spec:
      containers:
      - name: test
      image: busybox
      command: ["sh", "-c", "sleep 3600"]
      securityContext:
      privileged: true
      EOF
    • Confirm that any remaining running containers with privileged: true are only in explicitly approved namespaces (if any) and that user/workload namespaces are clean:
      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 "***pod_name: ${pod_name} container_name: ${container_name} pod_namespace: ${pod_namespace} is_container_privileged: ${container_privileged} is_compliant: false"
      fi
      done
      done

    Ensure that no lines with is_compliant: false appear for regular workload namespaces.

Using kubectl
# 1) Create a PodSecurity admission label set that forbids privileged containers
# Run on: any machine with kubectl access

# Example: apply to one user workload namespace (replace "my-workload-namespace" with the real name)
kubectl label namespace my-workload-namespace \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/audit-version=latest \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/warn-version=latest --overwrite

# To apply the same policy to multiple namespaces (edit the list as needed):
for ns in team-a team-b production staging; 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/audit-version=latest \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/warn-version=latest --overwrite
done

Example restrictive policy via a (legacy) PodSecurityPolicy-like admission controller (only if your cluster still uses it and you have the controller enabled):

# Save as psp-no-privileged.yaml, then apply with:
# kubectl apply -f psp-no-privileged.yaml
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: restricted-no-privileged
spec:
privileged: false
allowPrivilegeEscalation: false
requiredDropCapabilities:
- ALL
volumes:
- 'configMap'
- 'emptyDir'
- 'projected'
- 'secret'
- 'downwardAPI'
- 'persistentVolumeClaim'
hostNetwork: false
hostIPC: false
hostPID: false
runAsUser:
rule: 'MustRunAsNonRoot'
seLinux:
rule: 'RunAsAny'
supplementalGroups:
rule: 'MustRunAs'
ranges:
- min: 1
max: 65535
fsGroup:
rule: 'MustRunAs'
ranges:
- min: 1
max: 65535
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: use-restricted-no-privileged
rules:
- apiGroups: ['policy']
resources: ['podsecuritypolicies']
verbs: ['use']
resourceNames: ['restricted-no-privileged']
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: use-restricted-no-privileged
namespace: my-workload-namespace
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: use-restricted-no-privileged
subjects:
- kind: Group
name: system:serviceaccounts:my-workload-namespace
apiGroup: rbac.authorization.k8s.io
# 2) Verification (no privileged containers in existing pods)
# 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
Automation
#!/usr/bin/env bash
# Purpose: Minimize admission of privileged containers by enforcing a Pod Security policy
# (via Pod Security admission labels) or a fallback namespace-wide PSP-like
# restriction using a validating admission policy (for clusters that support it).
# Scope: Runs from any machine with kubectl access and current-context set appropriately.
# Notes: This is a MANUAL benchmark control. This script helps implement a common,
# restrictive policy, but you must review namespaces and exceptions yourself.

set -euo pipefail

# -----------------------------
# Configuration (edit as needed)
# -----------------------------

# Namespaces to EXCLUDE from restriction (typically system/control-plane namespaces).
# Add any namespaces that legitimately require privileged containers.
EXCLUDED_NAMESPACES=(
"kube-system"
"kube-public"
"kube-node-lease"
"kube-admin"
"kube-monitoring"
"default" # Remove this if you run user workloads in "default" and want it restricted
)

# Label key/value for Pod Security Admission. "restricted" blocks privileged containers.
PSA_LABEL_KEY="pod-security.kubernetes.io/enforce"
PSA_LABEL_VALUE="restricted"

# Whether to attempt creating a validating admission policy as a fallback for clusters
# without Pod Security Admission labels (set to "true" or "false").
ENABLE_VALIDATING_FALLBACK="true"

# Name/namespace for the validating admission policy resources (Kubernetes 1.27+).
VAP_NAME="deny-privileged-containers"
VAP_TEMPLATE_NAME="deny-privileged-containers-template"

# -----------------------------------
# Helpers
# -----------------------------------

kubectl_bin() {
command -v kubectl
}

in_excluded_namespace() {
local ns="$1"
for e in "${EXCLUDED_NAMESPACES[@]}"; do
if [[ "$ns" == "$e" ]]; then
return 0
fi
done
return 1
}

supports_pod_security_admission() {
# Heuristic: check if namespace accepts pod-security.kubernetes.io/enforce label without error
local test_ns="kube-system"
if ! $(kubectl_bin) get ns "$test_ns" >/dev/null 2>&1; then
return 1
fi
# Dry-run label; if it succeeds, assume PSA is available.
if $(kubectl_bin) label ns "$test_ns" "${PSA_LABEL_KEY}=${PSA_LABEL_VALUE}" --dry-run=server >/dev/null 2>&1; then
return 0
fi
return 1
}

kubectl_api_exists() {
local api="$1"
if $(kubectl_bin) api-resources --api-group="${api}" >/dev/null 2>&1; then
return 0
fi
return 1
}

# -----------------------------------
# 1. Enforce Pod Security "restricted" in workload namespaces (preferred)
# -----------------------------------

enforce_pod_security_admission() {
echo "==> Enforcing Pod Security Admission 'restricted' in workload namespaces"

local ns
# Get all namespaces
mapfile -t ALL_NS < <($(kubectl_bin) get ns -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}')

for ns in "${ALL_NS[@]}"; do
if in_excluded_namespace "$ns"; then
echo "Skipping excluded namespace: $ns"
continue
fi

# Idempotent label application
echo "Labeling namespace '$ns' with ${PSA_LABEL_KEY}=${PSA_LABEL_VALUE}"
$(kubectl_bin) label namespace "$ns" "${PSA_LABEL_KEY}=${PSA_LABEL_VALUE}" --overwrite >/dev/null
done
}

# -----------------------------------
# 2. Fallback: ValidatingAdmissionPolicy to deny privileged pods (if supported)
# -----------------------------------

apply_validating_admission_policy() {
if [[ "${ENABLE_VALIDATING_FALLBACK}" != "true" ]]; then
echo "Validating admission fallback disabled; skipping."
return 0
fi

if ! kubectl_api_exists "admissionregistration.k8s.io"; then
echo "Cluster does not support admissionregistration.k8s.io; cannot apply validating admission policy."
return 0
fi

echo "==> Applying ValidatingAdmissionPolicy to deny privileged containers (cluster-wide fallback)"

# This policy denies creation/update of any Pod with any container.securityContext.privileged == true.
# It is cluster-wide; you may need to refine the namespaces using matchConstraints.
cat <<'EOF' | $(kubectl_bin) apply -f -
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: deny-privileged-containers
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE","UPDATE"]
resources: ["pods"]
validations:
- expression: "!(has(object.spec.containers) && object.spec.containers.exists(c, has(c.securityContext) && c.securityContext.privileged == true))"
message: "Privileged containers are not allowed."
- expression: "!(has(object.spec.initContainers) && object.spec.initContainers.exists(c, has(c.securityContext) && c.securityContext.privileged == true))"
message: "Privileged init containers are not allowed."
EOF

# Create a binding that applies the policy cluster-wide
cat <<'EOF' | $(kubectl_bin) apply -f -
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: deny-privileged-containers-binding
spec:
policyName: deny-privileged-containers
validationActions: ["Deny"]
EOF
}

# -----------------------------------
# 3. Optional: Detect and report existing privileged containers
# -----------------------------------

report_existing_privileged_containers() {
echo "==> Scanning for existing Pods with privileged containers (this does not change them)"
$(kubectl_bin) 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_bin) 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
}

# -----------------------------------
# 4. Main
# -----------------------------------

main() {
if ! command -v jq >/dev/null 2>&1; then
echo "jq is required for verification; please install jq and re-run."
exit 1
fi

echo "==> Verifying kubectl connectivity"
$(kubectl_bin) version --request-timeout=5s >/dev/null

if supports_pod_security_admission; then
enforce_pod_security_admission
else
echo "Pod Security Admission labels not supported (or dry-run server check failed)."
apply_validating_admission_policy
fi

# Verification: rerun the supplied audit to show any remaining privileged containers.
echo "==> Verification: listing containers and their privileged status after policy application"
report_existing_privileged_containers

echo "==> Completed. Review any 'is_compliant: false' lines and either:"
echo " - Remove 'privileged: true' from their Pod specs, or"
echo " - Move them into an explicitly excluded namespace and justify the exception."
}

main "$@"