DevOps
Set project structure
Scaffold a Next.js web app foundation with Neon services, Drizzle schema, and design tokens—stops before building UI.
@devopsdaily / audit-kubernetes-manifests-for-security-and-reliability
DevOps
Review Kubernetes YAML for security holes, reliability gaps, and production-readiness issues—no cluster access needed.
---
name: kubernetes-manifest-auditor
description: Audit Kubernetes YAML manifests for security, reliability, and production-readiness issues, then report findings by severity with concrete fixes. Use when the user asks to review, audit, lint, or harden Kubernetes manifests (Deployments, StatefulSets, DaemonSets, Pods, Services, Ingress, RBAC, NetworkPolicy), asks "is this manifest production ready", "why is my pod insecure", or wants a security/best-practice check of Kubernetes YAML or Helm-rendered output.
---
# Kubernetes Manifest Auditor
Review Kubernetes manifests the way an experienced platform engineer would in a pull request: catch the security holes, the reliability gaps, and the "this will page someone at 3am" mistakes, and hand back a fix for each one.
This skill does not deploy anything and does not need cluster access. It reads YAML and reasons about it.
## When to use this
Trigger on requests like:
- "Review / audit / lint this Kubernetes manifest"
- "Is this Deployment production ready?"
- "Harden this pod spec" or "what's wrong with this YAML security-wise"
- "Check these manifests before I apply them"
- Auditing Helm-rendered output (`helm template ... | ...`) or `kustomize build` output
## How to run the audit
1. **Collect the manifests.** They may be one file, several files, or multiple documents in one file separated by `---`. Render Helm/Kustomize first if given a chart (`helm template`, `kustomize build`) so you are auditing the real objects. Parse each document and note its `kind` and `metadata.name`.
2. **Walk every workload and object against the checklist.** The full rubric lives in `references/checklist.md` (load it before auditing). It is grouped into six areas:
- **Security context** (root user, privilege escalation, capabilities, read-only rootfs, host namespaces)
- **Supply chain** (image tags vs digests, `imagePullPolicy`, registry trust)
- **Resource management** (requests/limits, QoS, ephemeral storage)
- **Reliability** (probes, replicas, `PodDisruptionBudget`, anti-affinity, `terminationGracePeriodSeconds`, rollout strategy)
- **Networking and access** (`NetworkPolicy`, `Service` exposure, `Ingress` TLS, RBAC scope, ServiceAccount token automounting)
- **Secrets and config** (secrets in env vs mounted, plaintext data, config drift)
3. **Judge severity honestly.** Use three levels:
- **Critical**: exploitable or outage-causing as written (privileged container, `hostPath` to `/`, cluster-admin binding, no resource limits on a public workload, secret value in plaintext).
- **Warning**: a real best-practice gap that will bite under load or in an incident (no probes, `:latest` tag, single replica for a stateful service, automounted SA token that is unused).
- **Info**: a nice-to-have or a note to confirm intent (missing labels, no `PodDisruptionBudget` on a batch job where it does not matter).
Do not inflate severity to pad the report, and do not flag something that is genuinely fine for the workload's context (a `Job` legitimately has one "replica"; a debug tool may need extra capabilities). When a finding depends on intent, say so.
4. **Give a fix for every finding.** A finding without a fix is noise. Show the corrected YAML snippet, not just prose. Keep snippets minimal and paste-ready.
## Output format
Lead with a one-line verdict and a severity tally, then the findings grouped Critical → Warning → Info. For each finding:
- **`<kind>/<name>`: short title** `[Critical|Warning|Info]`
- One or two sentences on what is wrong and why it matters.
- A fenced YAML snippet with the fix.
End with a short "looks good" list of the things the manifest already does right (so the report is fair and the reader trusts it), and a one-line next step.
### Example shape
```
Audit: api-gateway Deployment. 2 Critical, 3 Warnings, 1 Info. Not production ready.
## Critical
### Deployment/api-gateway: container runs as root [Critical]
No securityContext, so the container runs as UID 0. A container escape lands as root on the node.
```yaml
securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
seccompProfile:
type: RuntimeDefault
```
...
```
A full worked example (input manifest and the report it should produce) is in `examples/`.
## Principles
- **Read the whole set, not one object in isolation.** A `Deployment` with no `NetworkPolicy` in the namespace is a finding you only see by looking across documents.
- **Prefer the fix the reader can copy.** Real field names, real values, minimal context.
- **Be specific about blast radius.** "Runs as root" matters more when paired with a `hostPath` mount or a mounted ServiceAccount token. Call out the combination.
- **Stay defensive.** This skill hardens configuration; it does not write exploits.
---
Part of [devops-daily-skills](https://github.com/The-DevOps-Daily/devops-daily-skills) by [DevOps Daily](https://devops-daily.com). For deeper background on the checks, see the Kubernetes and Security sections on [devops-daily.com](https://devops-daily.com/categories/kubernetes).
# A deliberately flawed manifest used to demonstrate the auditor.
# See sample-report.md for the audit this should produce.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-gateway
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: api-gateway
template:
metadata:
labels:
app: api-gateway
spec:
containers:
- name: api-gateway
image: mycompany/api-gateway:latest
ports:
- containerPort: 8080
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: db-creds
key: password
---
apiVersion: v1
kind: Service
metadata:
name: api-gateway
namespace: default
spec:
type: LoadBalancer
selector:
app: api-gateway
ports:
- port: 80
targetPort: 8080
# Example audit report
This is the kind of report the skill produces for `sample-deployment.yaml`.
---
**Audit: api-gateway (Deployment + Service). 2 Critical, 4 Warnings, 1 Info. Not production ready.**
## Critical
### Service/api-gateway: internal service exposed to the internet [Critical]
`type: LoadBalancer` gives the api-gateway a public IP. If this is meant to be reached only from inside the cluster or via an ingress, this is accidental internet exposure of your gateway.
```yaml
spec:
type: ClusterIP # front external traffic with an Ingress instead
```
### Deployment/api-gateway: no memory limit on a shared node [Critical]
No `resources` block at all. A memory leak here can OOM the node and evict every other pod on it, and the scheduler has no request to place the pod against.
```yaml
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
```
## Warning
### Deployment/api-gateway: container runs as root [Warning]
No `securityContext`, so the process runs as UID 0. Harden the pod and container:
```yaml
securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: api-gateway
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
```
### Deployment/api-gateway: mutable `:latest` image tag [Warning]
`mycompany/api-gateway:latest` is non-deterministic; two rollouts can run different code. Pin a version or a digest.
```yaml
image: mycompany/api-gateway:1.4.2
# or: mycompany/api-gateway@sha256:...
```
### Deployment/api-gateway: no health probes [Warning]
Without a readiness probe, the Service sends traffic before the app is ready; without liveness, a wedged process never restarts.
```yaml
readinessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 5
livenessProbe:
httpGet: { path: /livez, port: 8080 }
initialDelaySeconds: 15
```
### Deployment/api-gateway: single replica, no PodDisruptionBudget [Warning]
`replicas: 1` means any node drain or rollout is downtime. Run at least two and add a PDB.
```yaml
spec:
replicas: 2
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: api-gateway }
spec:
minAvailable: 1
selector: { matchLabels: { app: api-gateway } }
```
## Info
### Deployment/api-gateway: DB password injected as an env var [Info]
`DATABASE_PASSWORD` comes in as an environment variable. Env vars leak through crash dumps, `/proc/<pid>/environ`, and any RCE that dumps `env`. Consider mounting the secret as a file instead. Also, no `NetworkPolicy` is present in `default`, so confirm one restricts access to this workload.
---
**Already good:** labels and selector match, container port is named and consistent with the Service `targetPort`.
**Next step:** apply the Critical fixes before shipping, then the Warnings. If you want the reasoning behind any check, the Kubernetes and Security guides on https://devops-daily.com go deeper.
# Kubernetes Manifest Audit Checklist
The rubric the auditor works through. Each item lists what to look for, why it matters, a default severity, and the fix. Severity is a starting point; adjust for the workload's real context and say why when you do.
---
## 1. Security context
### Runs as root
- **Check:** `securityContext.runAsNonRoot: true` is set (pod or container), and `runAsUser` is a non-zero UID. Absent means the container likely runs as UID 0.
- **Why:** A container escape or app RCE lands as root on the node. Non-root shrinks that blast radius dramatically.
- **Severity:** Critical if also privileged or mounting host paths; otherwise Warning.
- **Fix:**
```yaml
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
```
### Privilege escalation allowed
- **Check:** `allowPrivilegeEscalation: false` on every container. Default is `true`.
- **Why:** Blocks `setuid` binaries and similar from gaining more privileges than the parent process.
- **Severity:** Warning (Critical when combined with added capabilities).
- **Fix:** `allowPrivilegeEscalation: false`.
### Privileged container
- **Check:** `securityContext.privileged: true`.
- **Why:** A privileged container is effectively root on the host with all capabilities and device access. Almost never needed by application workloads.
- **Severity:** Critical unless it is a known node-agent (CNI, CSI, monitoring) that documents the need.
- **Fix:** Remove `privileged: true`; grant only the specific capability required.
### Capabilities not dropped
- **Check:** `securityContext.capabilities.drop: ["ALL"]`, with only the specific caps added back.
- **Why:** Containers inherit a default capability set most apps never use (`NET_RAW`, `SETUID`, etc.). Dropping all and adding back the few you need is least privilege.
- **Severity:** Warning.
- **Fix:**
```yaml
securityContext:
capabilities:
drop: ["ALL"]
# add: ["NET_BIND_SERVICE"] # only if binding a low port
```
### Writable root filesystem
- **Check:** `readOnlyRootFilesystem: true`, with `emptyDir` volumes for the paths that genuinely need writes (e.g. `/tmp`).
- **Why:** Stops an attacker from writing tools or modifying binaries inside the container.
- **Severity:** Warning.
- **Fix:** set `readOnlyRootFilesystem: true` and mount writable dirs explicitly.
### seccomp not set
- **Check:** `seccompProfile.type: RuntimeDefault` (pod or container).
- **Why:** Restricts the syscalls the container can make, removing whole classes of kernel-level attack surface.
- **Severity:** Warning.
- **Fix:** `seccompProfile: { type: RuntimeDefault }`.
### Host namespaces
- **Check:** `hostNetwork`, `hostPID`, `hostIPC` are not `true`.
- **Why:** Sharing host namespaces breaks isolation: `hostPID` exposes every process on the node, `hostNetwork` exposes the node's interfaces and skips NetworkPolicy.
- **Severity:** Critical unless it is a node-level agent that needs it.
- **Fix:** remove the host namespace flags.
### hostPath volumes
- **Check:** any `hostPath` volume, especially to sensitive paths (`/`, `/var/run/docker.sock`, `/etc`, `/var/lib/kubelet`).
- **Why:** A hostPath mount is a direct line to the node filesystem; `/var/run/docker.sock` or the containerd socket is instant node takeover.
- **Severity:** Critical for sensitive paths, Warning otherwise.
- **Fix:** replace with a proper volume type (PVC, `emptyDir`, `configMap`, CSI) or remove.
---
## 2. Supply chain
### Mutable image tag
- **Check:** images pinned by digest (`@sha256:...`) or at least a specific version tag, never `:latest` or no tag.
- **Why:** `:latest` is non-deterministic: two rollouts can run different code, and you cannot reason about what is deployed. Digests are immutable.
- **Severity:** Warning (Critical for anything security-sensitive where reproducibility matters).
- **Fix:** `image: myapp:1.7.3` or `image: myapp@sha256:...`.
### imagePullPolicy with mutable tags
- **Check:** if using a tag like `:latest`, `imagePullPolicy: Always`; if pinned by digest, `IfNotPresent` is fine.
- **Why:** avoids running a stale cached image while thinking you shipped a new one.
- **Severity:** Info.
### Untrusted or implicit registry
- **Check:** images come from a known registry, not an unpinned public image where a typo or takeover could substitute malware.
- **Why:** supply-chain substitution. Prefer your own registry or a trusted mirror with digest pinning.
- **Severity:** Info to Warning depending on exposure.
---
## 3. Resource management
### No resource requests/limits
- **Check:** every container sets `resources.requests` and `resources.limits` for CPU and memory.
- **Why:** No memory limit means one leaking pod can OOM the whole node and take neighbors with it. No requests means the scheduler cannot place the pod sensibly and it has no guaranteed share.
- **Severity:** Critical for memory limits on a shared node; Warning for CPU.
- **Fix:**
```yaml
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi # cap memory to protect the node
# cpu limits are optional and often better left unset to avoid throttling
```
### QoS class
- **Check:** understand the resulting QoS. Requests == limits gives `Guaranteed`; requests only gives `Burstable`; nothing gives `BestEffort` (first to be evicted).
- **Why:** `BestEffort` pods are killed first under node pressure. Critical workloads should be `Burstable` or `Guaranteed`.
- **Severity:** Info, escalate for critical services.
---
## 4. Reliability
### Missing probes
- **Check:** `livenessProbe` and `readinessProbe` (and `startupProbe` for slow starters).
- **Why:** Without a readiness probe, traffic hits a pod before it is ready. Without a liveness probe, a wedged process never restarts. Startup probes stop liveness from killing slow-booting apps.
- **Severity:** Warning.
- **Fix:**
```yaml
readinessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet: { path: /livez, port: 8080 }
initialDelaySeconds: 15
periodSeconds: 20
```
### Single replica for a serving workload
- **Check:** `replicas >= 2` for anything that serves traffic; note that a bare Pod has no controller at all.
- **Why:** One replica means any node drain, crash, or rollout is downtime.
- **Severity:** Warning (Info for genuinely single-instance or batch work).
- **Fix:** `replicas: 2` or more, plus anti-affinity below.
### No PodDisruptionBudget
- **Check:** a `PodDisruptionBudget` exists for multi-replica serving workloads.
- **Why:** Without a PDB, a node drain can evict all replicas at once. A PDB keeps a minimum available during voluntary disruptions.
- **Severity:** Warning.
- **Fix:**
```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: api-gateway }
spec:
minAvailable: 1
selector: { matchLabels: { app: api-gateway } }
```
### No anti-affinity / topology spread
- **Check:** `topologySpreadConstraints` or pod anti-affinity so replicas do not all land on one node/zone.
- **Why:** Three replicas on one node is one node failure away from full outage.
- **Severity:** Warning.
- **Fix:** a `topologySpreadConstraints` block over `kubernetes.io/hostname` (and `topology.kubernetes.io/zone`).
### Rollout strategy / grace period
- **Check:** `strategy.rollingUpdate` with sane `maxUnavailable`/`maxSurge`, and a `terminationGracePeriodSeconds` that fits the app's shutdown.
- **Why:** Aggressive rollouts drop capacity; too-short grace periods cut connections mid-flight.
- **Severity:** Info.
---
## 5. Networking and access
### No NetworkPolicy
- **Check:** a `NetworkPolicy` (default-deny plus explicit allows) exists for the namespace/workload. Remember policies are additive and only enforced if the CNI supports them.
- **Why:** With no policy, every pod can talk to every other pod. One compromised pod reaches your databases, internal APIs, and control-plane-adjacent services. (See the Argo CD repo-server RCE for a real example of this default biting hard.)
- **Severity:** Warning (Critical for namespaces with sensitive services).
- **Fix:** a default-deny ingress policy plus targeted allows.
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny-ingress }
spec:
podSelector: {}
policyTypes: ["Ingress"]
```
### Service exposure
- **Check:** `Service` type. A `LoadBalancer` or `NodePort` on an internal service exposes it publicly.
- **Why:** Accidental internet exposure of an admin or database service.
- **Severity:** Critical if it exposes something sensitive, Info otherwise.
- **Fix:** use `ClusterIP` for internal services; front public traffic with an Ingress.
### Ingress without TLS
- **Check:** `Ingress` defines a `tls` block.
- **Why:** plaintext traffic to a public endpoint.
- **Severity:** Warning.
### Over-broad RBAC
- **Check:** `Role`/`ClusterRole` rules. Flag `verbs: ["*"]`, `resources: ["*"]`, `apiGroups: ["*"]`, and any binding to `cluster-admin`.
- **Why:** A compromised ServiceAccount inherits these rights. Wildcards and cluster-admin turn a small foothold into cluster takeover.
- **Severity:** Critical for cluster-admin/wildcards, Warning for broad-but-scoped.
- **Fix:** enumerate the specific `apiGroups`, `resources`, and `verbs` the workload actually needs.
### Automounted ServiceAccount token
- **Check:** `automountServiceAccountToken: false` on pods/ServiceAccounts that do not call the Kubernetes API.
- **Why:** A mounted token is a credential an attacker can use against the API server. Most app pods never need it.
- **Severity:** Warning.
- **Fix:** `automountServiceAccountToken: false`.
---
## 6. Secrets and config
### Secret values in plaintext
- **Check:** `Secret` objects with real-looking values in `stringData`/`data`, or secrets committed alongside manifests.
- **Why:** Secrets in Git or manifests are leaked the moment the repo is shared or cloned.
- **Severity:** Critical.
- **Fix:** use a secrets manager (External Secrets Operator, Sealed Secrets, SOPS, cloud secret stores) and reference, never embed.
### Secrets injected as env vars
- **Check:** sensitive values via `env`/`envFrom` from a Secret, versus mounted as files.
- **Why:** Env vars leak easily: a crash dump, a `/proc/<pid>/environ` read, a logging middleware, or an RCE that dumps `env` hands them over. (Exactly the first step in the Argo CD repo-server exploit.) Mounted files are harder to exfiltrate wholesale and can be rotated.
- **Severity:** Warning.
- **Fix:** mount the Secret as a volume and read from the file path.
### Config in the image
- **Check:** environment-specific config baked into the image instead of `ConfigMap`/env.
- **Why:** forces a rebuild per environment and encourages drift.
- **Severity:** Info.
---
## Cross-cutting checks
- **Labels and selectors:** recommended labels (`app.kubernetes.io/name`, `.../instance`, `.../version`) present; selectors actually match pod labels (a mismatch means the Service routes to nothing).
- **Namespace:** workloads are not defaulting into `default`; sensitive workloads are isolated.
- **Deprecated APIs:** flag removed/deprecated `apiVersion`s (e.g. old `extensions/v1beta1`, `policy/v1beta1` PDBs).
- **Immutable field foot-guns:** note changes that would require object recreation (e.g. `Service.spec.clusterIP`, some `selector` changes).
Excerpt — the full skill ships with the install.
This skill audits Kubernetes manifests (Deployments, StatefulSets, DaemonSets, Pods, Services, Ingress, RBAC, NetworkPolicy) the way an experienced platform engineer would in a pull request. Use it when someone asks to review, lint, harden, or check if a manifest is production-ready. It works on raw YAML, Helm-rendered output (helm template), or Kustomize builds (kustomize build). The skill does not deploy anything and requires no cluster access.
Gather all manifests first—they may be one file, several files, or multiple documents separated by ---. Parse each document and walk it against the checklist covering six areas: security context (root user, privilege escalation, capabilities, read-only rootfs, host namespaces), supply chain (image tags vs digests, imagePullPolicy), resource management (requests and limits), reliability (probes, replicas, PodDisruptionBudget, anti-affinity), networking and access (NetworkPolicy, Service exposure, RBAC), and secrets and config (how secrets are injected).
Judge findings honestly into three severity levels: Critical (exploitable or outage-causing as written), Warning (real best-practice gaps), and Info (nice-to-have). Every finding gets a concrete YAML fix you can copy and paste. The report leads with a one-line verdict and a severity tally, then groups findings by severity, ends with a "looks good" list of things the manifest already does right, and closes with a next step.
More from the community
The general store →DevOps
Scaffold a Next.js web app foundation with Neon services, Drizzle schema, and design tokens—stops before building UI.
Design
Build the interactive dashboard UI (calendar, modals, chat, settings) on top of a working backend, consuming its contracts without re-implementing logic.
DevOps
Six-digit codes via email, one sign-in form for login and signup, hashed at rest and rate-limited. Works offline. Uses smtpfa.st.