Pod Security Standards and Gatekeeper
Admission is the last chance to reject a bad pod before it is stored. Kubernetes has built-in plugins for that. Pod Security Standards cover the common cases. Gatekeeper covers the rest.
The request path is:
API request → authn → authz → mutating admission → validating admission → etcd
Mutating runs first. Validating sees the object after mutation. If you are debugging a deny, look at both.
Built-in plugins worth having
On the API server:
--enable-admission-plugins=NodeRestriction,ResourceQuota,LimitRanger,ServiceAccount,DefaultStorageClass,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,PodSecurity
NodeRestriction limits what a kubelet identity can write. PodSecurity enforces the three levels below. Webhook plugins are how Gatekeeper attaches.
Pod Security Standards
Three levels, least to most tight:
- privileged — no extra restrictions
- baseline — blocks known privilege-escalation patterns
- restricted — hardened defaults
The practical way to apply them is namespace labels:
kubectl label namespace secure-apps \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restricted
enforce rejects. warn and audit tell you what would have been rejected.
Cluster-wide defaults live in admission config:
# /etc/kubernetes/admission-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1beta1
kind: PodSecurityConfiguration
defaults:
enforce: "baseline"
enforce-version: "latest"
audit: "restricted"
audit-version: "latest"
warn: "restricted"
warn-version: "latest"
exemptions:
namespaces: ["kube-system", "kube-public"]
Baseline blocks privileged: true, host namespaces, hostPath, and the loud capabilities (SYS_ADMIN, NET_ADMIN, and friends). Restricted also wants non-root, allowPrivilegeEscalation: false, drop: ["ALL"], and seccompProfile.type: RuntimeDefault.
A restricted-shaped pod:
apiVersion: v1
kind: Pod
metadata:
name: restricted-pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: nginx
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
readOnlyRootFilesystem: true
Test before you apply:
kubectl --dry-run=server apply -f pod.yaml
kubectl get namespace default --show-labels
kubectl get events --field-selector reason=PodSecurityViolation
Gatekeeper
PSS is a fixed menu. Gatekeeper is policy as code: a ConstraintTemplate (Rego) plus a Constraint (where it applies).
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/release-3.14/deploy/gatekeeper.yaml
kubectl get pods -n gatekeeper-system
A template that rejects privileged containers:
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8sblockprivileged
spec:
crd:
spec:
names:
kind: K8sBlockPrivileged
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sblockprivileged
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
container.securityContext.privileged == true
msg := "Privileged containers are not allowed"
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sBlockPrivileged
metadata:
name: block-privileged-containers
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
Other policies that come up constantly: require CPU and memory limits, block UID 0, refuse :latest and untagged images.
kubectl get constrainttemplates
kubectl get constraints
kubectl describe constraint block-privileged-containers
kubectl logs -n gatekeeper-system -l control-plane=controller-manager
PSS for the baseline. Gatekeeper for the house rules. Do not put kube-system under restricted unless you like broken addons.