---
title: "Kubernetes Security Patterns"
description: "Security hardening reference for all resources generated by this skill."
type: skill
canonical_url: https://claudary.paisolsolutions.com/skills/security-patterns
source: "Claudary"
difficulty: intermediate
author: "Claude Code Knowledge Pack"
date: 2026-07-10T11:46:34.954Z
license: CC-BY-4.0
attribution: "Kubernetes Security Patterns — Claudary (https://claudary.paisolsolutions.com/skills/security-patterns)"
---

# 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.

```yaml
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`.

```yaml
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):**
```yaml
securityContext:
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop:
    - ALL
```

**When `readOnlyRootFilesystem: true` requires temp space**, add an `emptyDir` volume:
```yaml
volumeMounts:
- name: tmp
  mountPath: /tmp
volumes:
- name: tmp
  emptyDir: {}
```

---

## NetworkPolicy Patterns

### Default deny all (apply first in every namespace)

```yaml
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

```yaml
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)

```yaml
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

```yaml
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

| 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:

```yaml
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

```yaml
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)

```yaml
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)

```yaml
spec:
  imagePullSecrets:
  - name: my-registry-secret
```

### Create the pull secret

```bash
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:

```yaml
# Disable on the ServiceAccount (applies to all pods using it)
apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-app
  namespace: default
automountServiceAccountToken: false
```

```yaml
# 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:

```yaml
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: {}
```

---

Source: [Claudary](https://claudary.paisolsolutions.com/skills/security-patterns) · https://claudary.paisolsolutions.com
