Kubernetes Security Patterns
Security hardening reference for all resources generated by this skill.
Overview
Kubernetes Security Patterns
Security hardening reference for all resources generated by this skill.
Pod-level securityContext
Applied at spec.template.spec.securityContext in Deployments/StatefulSets/DaemonSets.
securityContext:
runAsNonRoot: true # Reject containers running as root (UID 0)
runAsUser: 1000 # Specific UID; must be non-zero when runAsNonRoot: true
runAsGroup: 3000 # GID for the primary process
fsGroup: 2000 # GID for volume mounts; files will be group-owned by this GID
fsGroupChangePolicy: OnRootMismatch # Only chown if owner/mode mismatch (faster)
supplementalGroups: [4000] # Additional GIDs for the process
seccompProfile:
type: RuntimeDefault # Use the node's default seccomp profile (recommended)
# type: Localhost # Load a custom profile from the node
# localhostProfile: profiles/my-profile.json
# type: Unconfined # No seccomp filtering (avoid in production)
sysctls: [] # Allow only safe sysctls; empty is safest
Key points:
runAsNonRoot: trueis enforced at admission; the image's USER must be non-zerofsGroupis required for writable volume mounts; without it, mounted volumes may not be writable by the processseccompProfile: RuntimeDefaultadds significant syscall filtering with minimal application impact
Container-level securityContext
Applied at spec.template.spec.containers[*].securityContext.
securityContext:
allowPrivilegeEscalation: false # Prevent gaining more privileges than parent (always set false)
readOnlyRootFilesystem: true # Mount container root FS as read-only
runAsNonRoot: true # Container-level override of pod-level
runAsUser: 1000 # Container-level override of pod-level
capabilities:
drop:
- ALL # Drop all Linux capabilities
add:
- NET_BIND_SERVICE # Re-add only what's required (binding port <1024)
privileged: false # Never set true in production
procMount: Default # Default; Unmasked only for container-in-container
seccompProfile:
type: RuntimeDefault # Container-level override of pod-level
Minimum recommended set (add to every container):
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
When readOnlyRootFilesystem: true requires temp space, add an emptyDir volume:
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
NetworkPolicy Patterns
Default deny all (apply first in every namespace)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: my-namespace
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
Allow ingress from same namespace only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-same-namespace
namespace: my-namespace
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
- from:
- podSelector: {} # Any pod in the same namespace
Allow ingress from specific namespace (e.g., monitoring)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-monitoring
namespace: my-namespace
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: my-app
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus # Both conditions must be true (AND)
Allow egress DNS + specific services only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-egress-selective
namespace: my-namespace
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: my-app
policyTypes:
- Egress
egress:
- ports: # DNS — always required for service discovery
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: my-db
ports:
- protocol: TCP
port: 5432
Key points:
- In
from/to, multiple items are ORed; within an item,namespaceSelector+podSelectorare ANDed - Always allow DNS egress (port 53 UDP+TCP) or pod DNS lookups will fail
- Apply
default-deny-allbefore adding allow rules
Minimal RBAC
When to use Role vs ClusterRole
| Scenario | Use |
|---|---|
| Access resources in one namespace | Role + RoleBinding |
| Access resources across all namespaces | ClusterRole + ClusterRoleBinding |
| Reuse the same permissions in multiple namespaces | ClusterRole + RoleBinding (per namespace) |
| Node-level or non-namespaced resources | ClusterRole + ClusterRoleBinding |
Aggregated ClusterRoles
Use aggregation to extend built-in roles without modifying them:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: my-custom-viewer
labels:
rbac.authorization.k8s.io/aggregate-to-view: "true" # Extends 'view' ClusterRole
rules:
- apiGroups: ["my.custom.io"]
resources: ["myresources"]
verbs: ["get", "list", "watch"]
Principle of least privilege
rules:
# Good: specific resources, specific verbs, specific names
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["only-this-secret"]
verbs: ["get"]
# Avoid: wildcard verbs or resources
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
ImagePullSecrets and Private Registries
Attach to ServiceAccount (recommended — applies to all pods using the SA)
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app
namespace: default
imagePullSecrets:
- name: my-registry-secret
Attach directly to Pod (use when SA approach is not applicable)
spec:
imagePullSecrets:
- name: my-registry-secret
Create the pull secret
kubectl create secret docker-registry my-registry-secret \\
--docker-server=registry.example.com \\
--docker-username=my-user \\
--docker-password=my-password \\
--namespace=default
ServiceAccount Token Auto-mounting
By default, Kubernetes mounts a ServiceAccount token into every pod at /var/run/secrets/kubernetes.io/serviceaccount/. Disable this for workloads that don't need API server access:
# Disable on the ServiceAccount (applies to all pods using it)
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app
namespace: default
automountServiceAccountToken: false
# Override on the Pod/Deployment (takes precedence over ServiceAccount setting)
spec:
template:
spec:
automountServiceAccountToken: false
When to keep it enabled:
- Operators and controllers that watch/modify Kubernetes resources
- Applications using in-cluster config (
rest.InClusterConfig()) - Service meshes that inject sidecars needing API access
When to disable it:
- Web applications, APIs, and stateless services
- Batch jobs without cluster API interaction
- Any workload where the token is not used
Complete Hardened Pod Template
Combine all patterns for a production-ready pod template:
spec:
serviceAccountName: my-app
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: my-app
image: my-app:1.0.0
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 15
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: http
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}