All skills
Skillintermediate

Kubernetes Security Patterns

Security hardening reference for all resources generated by this skill.

Claude Code Knowledge Pack7/10/2026

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: true is enforced at admission; the image's USER must be non-zero
  • fsGroup is required for writable volume mounts; without it, mounted volumes may not be writable by the process
  • seccompProfile: RuntimeDefault adds 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 + podSelector are ANDed
  • Always allow DNS egress (port 53 UDP+TCP) or pod DNS lookups will fail
  • Apply default-deny-all before adding allow rules

Minimal RBAC

When to use Role vs ClusterRole

ScenarioUse
Access resources in one namespaceRole + RoleBinding
Access resources across all namespacesClusterRole + ClusterRoleBinding
Reuse the same permissions in multiple namespacesClusterRole + RoleBinding (per namespace)
Node-level or non-namespaced resourcesClusterRole + 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: {}