Pod Security Standards for CKS¶
π Pod Security Standards - Detailed policy definitions for each PSA level π Pod Security Admission - PSA controller configuration
Pod Security Admission (PSA)¶
Overview¶
Pod Security Admission is a built-in Kubernetes admission controller that enforces Pod Security Standards at the namespace level. It replaced the deprecated PodSecurityPolicy (PSP) starting in Kubernetes 1.25.
PSA Profiles¶
Privileged¶
- Unrestricted policy - allows everything
- Intended for system and infrastructure workloads (kube-system)
- No restrictions on capabilities, volumes, or host access
- Should only be used for trusted, essential system components
Baseline¶
- Minimally restrictive policy that prevents known privilege escalations
- Suitable for most non-critical workloads
- Blocks: hostNetwork, hostPID, hostIPC, privileged containers, host ports
- Allows: most capabilities, volume types, running as root
Baseline restrictions: - No hostNetwork, hostPID, hostIPC - No privileged containers - No host port bindings (or limited range) - No proc mount types other than Default - No unsafe sysctls - Restricted volume types (no hostPath) - No Windows HostProcess
Restricted¶
- Heavily restricted policy following current pod hardening best practices
- Required for security-sensitive workloads
- Blocks everything Baseline blocks, plus additional restrictions
Restricted additional requirements: - Must run as non-root (runAsNonRoot: true) - Must not allow privilege escalation (allowPrivilegeEscalation: false) - Must drop ALL capabilities (may add NET_BIND_SERVICE only) - Must use a seccomp profile (RuntimeDefault or Localhost) - Restricted volume types (only configMap, emptyDir, projected, secret, downwardAPI, PVC, ephemeral)
PSA Modes¶
| Mode | Behavior |
|---|---|
| enforce | Violations reject the pod |
| audit | Violations logged in audit log but pod is allowed |
| warn | Violations generate user-facing warnings but pod is allowed |
Configuring PSA with Namespace Labels¶
# Enforce restricted in production
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest
# Warn and audit in staging (don't block, just alert)
kubectl label namespace staging \
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
# Baseline for general workloads
kubectl label namespace general \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/enforce-version=latest \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/warn-version=latest
π Enforce Pod Security Standards with Namespace Labels - Step-by-step guide
Pod That Passes Restricted PSA¶
apiVersion: v1
kind: Pod
metadata:
name: restricted-pod
namespace: production
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:v1.0
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true
runAsNonRoot: true
resources:
limits:
memory: "128Mi"
cpu: "500m"
SecurityContext Deep Dive¶
π Security Context - Configuring security context for pods and containers
Pod-Level SecurityContext¶
spec:
securityContext:
runAsUser: 1000 # UID for all containers
runAsGroup: 3000 # Primary GID for all containers
fsGroup: 2000 # GID for volume ownership
runAsNonRoot: true # Prevent running as root
seccompProfile: # Seccomp profile for all containers
type: RuntimeDefault
supplementalGroups: [4000] # Additional GIDs
Container-Level SecurityContext¶
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false # Prevent gaining more privileges
privileged: false # Not a privileged container
readOnlyRootFilesystem: true # Immutable filesystem
runAsNonRoot: true # Must not run as UID 0
runAsUser: 1000 # Specific UID
capabilities:
drop: ["ALL"] # Drop all Linux capabilities
add: ["NET_BIND_SERVICE"] # Add only what's needed
seccompProfile:
type: RuntimeDefault # Use default seccomp profile
Linux Capabilities¶
Common Capabilities: | Capability | Purpose | Risk | |-----------|---------|------| | NET_BIND_SERVICE | Bind to ports < 1024 | Low - often needed | | NET_RAW | Use raw sockets | Medium - network sniffing | | SYS_ADMIN | Broad system admin | HIGH - near root | | SYS_PTRACE | Debug processes | HIGH - can read memory | | DAC_OVERRIDE | Bypass file permissions | HIGH - access any file | | CHOWN | Change file ownership | Medium | | SETUID/SETGID | Change UID/GID | HIGH - privilege escalation |
Best Practice: Drop ALL capabilities and add only what is specifically needed:
securityContext:
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"] # Only if needed
OPA Gatekeeper¶
π OPA Gatekeeper - Kubernetes admission control with OPA π Gatekeeper Library - Pre-built constraint templates
How Gatekeeper Works¶
- ConstraintTemplate - Defines the policy logic using Rego language
- Constraint - Applies a template with specific parameters to target resources
- Gatekeeper acts as a ValidatingAdmissionWebhook
- Every resource create/update is evaluated against matching constraints
- Violations prevent the resource from being created
Installing Gatekeeper¶
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/release-3.14/deploy/gatekeeper.yaml
Common Constraint Templates¶
Required Labels¶
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
openAPIV3Schema:
type: object
properties:
labels:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("Missing required labels: %v", [missing])
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-team-label
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Namespace"]
parameters:
labels: ["team", "environment"]
Block Latest Tag¶
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sdisallowedtags
spec:
crd:
spec:
names:
kind: K8sDisallowedTags
validation:
openAPIV3Schema:
type: object
properties:
tags:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sdisallowedtags
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
tag := split(container.image, ":")[1]
tag == input.parameters.tags[_]
msg := sprintf("container <%v> uses disallowed tag <%v>", [container.name, tag])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not contains(container.image, ":")
msg := sprintf("container <%v> has no tag (defaults to latest)", [container.name])
}
Secrets Management¶
π Secrets - Kubernetes Secret objects π Good Practices for Secrets - Secret management recommendations
Encryption at Rest¶
π Encrypting Secret Data at Rest - Configuration guide
# /etc/kubernetes/enc/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {} # Fallback for reading unencrypted secrets
Provider order matters: - First provider is used for encryption of new secrets - All providers are tried for decryption (in order) - identity: {} means no encryption (plain text) - If identity is first, secrets are NOT encrypted
External Secret Management¶
- HashiCorp Vault - External secret store with Kubernetes integration
- AWS Secrets Manager / Azure Key Vault / GCP Secret Manager - Cloud-native options
- External Secrets Operator - Syncs external secrets to Kubernetes
- Sealed Secrets - Encrypt secrets for safe storage in Git
Runtime Sandboxes¶
π Runtime Class - Container runtime selection
gVisor (runsc)¶
- Application-level kernel that intercepts system calls
- Provides an additional layer of isolation between containers and the host
- Runs in user space - no direct kernel access from containers
- Trade-off: some performance overhead and limited syscall compatibility
Kata Containers¶
- Lightweight VMs that provide hardware-level isolation
- Each container runs in its own VM with a dedicated kernel
- Stronger isolation than gVisor but higher resource overhead
- Good for multi-tenant environments with untrusted workloads
RuntimeClass Configuration¶
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc
---
apiVersion: v1
kind: Pod
metadata:
name: sandboxed-pod
spec:
runtimeClassName: gvisor
containers:
- name: app
image: myapp:v1.0
Key Takeaways¶
- PSA over PSP - Pod Security Admission is the current standard; PodSecurityPolicy is removed
- Restricted level - Know all requirements (non-root, no escalation, drop capabilities, seccomp)
- SecurityContext - Understand both pod-level and container-level settings
- Capabilities - Drop ALL, add only what is needed; know which capabilities are dangerous
- Gatekeeper - Understand ConstraintTemplate and Constraint relationship
- Secrets - Enable encryption at rest; provider order determines encryption behavior
- Sandboxes - gVisor for syscall filtering, Kata for VM isolation; use RuntimeClass