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
Remediation
Manual Steps
-
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; dokubectl get pod "${pod_name}" --namespace "${pod_namespace}" -o json | jq -c '.spec.containers[]' | \while read -r container; docontainer_name=$(echo "${container}" | jq -r '.name')container_privileged=$(echo "${container}" | jq -r '.securityContext.privileged' | sed -e 's/null/notset/g')if [ "${container_privileged}" = "true" ]; thenecho "NON-COMPLIANT pod=${pod_name} ns=${pod_namespace} container=${container_name}"fidonedone -
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.
-
Create a baseline policy to deny privileged containers in a namespace
Run on: any machine with kubectl access
Example using the built-inPodSecurityadmission (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 --overwriteThis label set enforces the “restricted” Pod Security level, which disallows privileged containers (
securityContext.privileged: true) in that namespace. -
(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: trueare rejected. Example Kyverno ClusterPolicy (adjust names/namespaces to your environment before applying):cat << 'EOF' > deny-privileged-containers.yamlapiVersion: kyverno.io/v1kind: ClusterPolicymetadata:name: deny-privileged-containersspec:validationFailureAction: enforcebackground: truerules:- name: disallow-privilegedmatch:any:- resources:kinds:- Podnamespaces:- "<NAMESPACE1>"- "<NAMESPACE2>"validate:message: "Privileged containers are not allowed."pattern:spec:containers:- name: "*"=(securityContext):=(privileged): "false"EOFkubectl apply -f deny-privileged-containers.yamlReview this policy with your security team before applying, and tailor namespace lists and any needed exceptions.
-
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, removesecurityContext.privileged: truefrom each container, or set it tofalse, 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 insteadkubectl edit deployment <DEPLOYMENT_NAME> -n <NAMESPACE># or apply the corrected manifestkubectl 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.
- Retrieve and edit the manifest:
-
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: v1kind: Podmetadata:name: test-privilegedspec:containers:- name: testimage: busyboxcommand: ["sh", "-c", "sleep 3600"]securityContext:privileged: trueEOF
- Confirm that any remaining running containers with
privileged: trueare 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; dokubectl get pod "${pod_name}" --namespace "${pod_namespace}" -o json | jq -c '.spec.containers[]' | \while read -r container; docontainer_name=$(echo "${container}" | jq -r '.name')container_privileged=$(echo "${container}" | jq -r '.securityContext.privileged' | sed -e 's/null/notset/g')if [ "${container_privileged}" = "true" ]; thenecho "***pod_name: ${pod_name} container_name: ${container_name} pod_namespace: ${pod_namespace} is_container_privileged: ${container_privileged} is_compliant: false"fidonedone
Ensure that no lines with
is_compliant: falseappear for regular workload namespaces. - Attempt to create a test privileged pod in a protected workload namespace; it should be rejected:
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 "$@"