Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Reviews infrastructure-as-code and container config — Terraform, a Kubernetes manifest, a Dockerfile, a docker-compose file — for insecure defaults and misconfigurations: containers that run as root or privileged, host namespace/volume escapes, security groups open to the world, public object storage, storage left unencrypted, plaintext secrets, and metadata-service exposure. Names each finding and its fix. Use when asked to review or security-check an existing IaC or container config before it ships. Do NOT use for writing a fresh Dockerfile from scratch (that is a separate authoring task), for reviewing the breadth of an IAM/RBAC permission grant, or for debugging a container already running in production.
.claude/skills/iac-misconfig-review/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flashlowest | 99% | 377 |
| gemini-3.1-pro-preview | 100% | 2 |
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | — | — |
| case-08 | ✗→✓ | ▲ Improved | — | — |
| case-11 | ✗→✓ | ▲ Improved | — | — |
| case-12 | ✗→✓ | ▲ Improved | — | — |
| case-03 | ✗→✓ | ▲ Improved | — | — |
Infrastructure-as-code and container config fails safe review the same way a broad IAM policy does: it works. The pod schedules, the bucket serves, the instance boots — so the review waves it through. But "it works" only proves the config is permissive enough, never that it is locked down. The dangerous settings here are almost always insecure defaults: things that are wrong because a hardening field is absent, not because someone typed a bad value. A Pod with no securityContext runs as root with privilege-escalation allowed. An S3 bucket with no public-access block relies on ACLs to stay private. An instance with no http_tokens setting answers the metadata service over IMDSv1. None of these look wrong — they look empty. This skill reviews the config against a fixed catalog and, for each hit, names the misconfiguration, why it is exploitable, and the concrete field that fixes it.
Activate when the request hands you an existing IaC or container config to review or security-check before it ships — a Terraform file/module, a Kubernetes manifest (Pod/Deployment/DaemonSet/StatefulSet), a Dockerfile, or a docker-compose file — and asks whether it is safe or what to fix.
Do not activate for: writing a new Dockerfile or manifest from scratch (that is an authoring task with its own conventions); reviewing whether an IAM/RBAC policy grants broader permissions than a workload needs (that is a least-privilege grant review, a different axis than misconfiguration); or debugging a container that is already running and misbehaving in production.
Go through the catalog surface by surface. For each resource in the config, check every applicable item — and treat an absent hardening field as a finding, not a pass, whenever the default is insecure. Report each finding as: what the misconfiguration is, why it is exploitable in one clause, and the exact field/value that fixes it. Do not invent findings on config that is already hardened, and do not flag a public listener that is meant to be public (an HTTPS/443 or HTTP/80 ingress on a load balancer is intended exposure, not a misconfiguration) — reserve world-open findings for administrative and datastore ports.
Full table: see references/misconfig-catalog.md — the body below covers the method and the high-frequency defaults; that file is the deep per-provider corpus (AWS/Azure/GCP/K8s insecure-defaults mapped to the exact fixing field, CIS/checkov/trivy style). Consult it only when the config touches a resource the three inline catalogs do not name.
runAsNonRoot: true and no non-root runAsUser, or aDockerfile with no USER line, means the process runs as UID 0. A container breakout then lands as root on the node. Fix: set runAsNonRoot: true / a high runAsUser, and add a USER line in the image.
privileged: true disables almost all isolation — the container gets thehost's devices and capabilities and is effectively root on the node. Fix: remove it; grant only the specific capabilities actually needed.
allowPrivilegeEscalation is true unless setfalse, letting a process gain more privileges than its parent (e.g. via setuid binaries). Fix: set allowPrivilegeEscalation: false.
hostNetwork, hostPID, or hostIPC: true drop the isolationboundary — the container shares the node's network stack, process table, or IPC, enabling it to see other workloads' traffic or processes. Fix: remove unless the workload is a node agent that genuinely needs it.
hostPath volume mounts a node directory into the container;mounting /, /var/run/docker.sock, or /etc hands over the node. Fix: use a configMap/secret/emptyDir or a named volume instead.
readOnlyRootFilesystem: true an attackercan drop tools or tamper with the image at runtime. Fix: set it true and mount an emptyDir for the few paths that need writes.
SYS_ADMIN,NET_ADMIN, NET_RAW) is more power than most apps use. Fix: drop: ["ALL"], then add back only what is required.
:latest (or untagged) is mutable — the running codeis not the reviewed code, and a poisoned upstream tag ships silently. Fix: pin to a version tag or, better, a digest.
env value: (or in aConfigMap) is stored in plaintext and visible to anyone who can read the object. Fix: reference a Secret via secretKeyRef, or an external secret store.
cidr_blocks = ["0.0.0.0/0"] (or ::/0) on SSH (22), RDP (3389), a database port (5432/3306/6379/27017), or all ports exposes it to the entire internet. Fix: scope the CIDR to the office/VPN/VPC range, or front it with a bastion. (An HTTPS/HTTP listener open to the world is fine — do not flag it.)
acl = "public-read"/"public-read-write", or a bucketpolicy with Principal: "*", or a missing aws_s3_bucket_public_access_block (with all four block flags true) leaves the bucket reachable by anyone. Fix: set the ACL private and attach a public-access block that blocks public ACLs and policies.
publicly_accessible = true on an RDS instanceor database, or an instance placed in a public subnet with a public IP when it need not be, puts the datastore directly on the internet. Fix: set it false and reach it from within the VPC.
storage_encrypted = false (or absent) on RDS,encrypted = false on an EBS volume, or an S3 bucket with no server-side encryption leaves data unprotected if the media or a snapshot leaks. Fix: enable encryption and, where it matters, use a customer-managed key.
password, secret_key, access_key, or private keystring in a .tf file lands in version control and in the state file. Fix: source it from a variable, a secrets manager, or an environment reference — never a literal.
metadata_options with http_tokens = "optional"(the default) lets a server-side-request-forgery bug read the instance's credentials over the unauthenticated metadata service. Fix: set http_tokens = "required" to force IMDSv2.
missing audit trail (CloudTrail / access logging) on sensitive resources, removes the ability to recover or investigate. Flag when the resource is security-relevant; do not demand it on every bucket.
USER — runs as root. A Dockerfile that never drops from root runs the app as UID 0inside the container. Fix: create and switch to a non-root user before the entrypoint.
ENV or ARG, or COPYing a.env/key file, persists in the image history even if later removed. Fix: use build secrets/mounts and keep credentials out of the image.
/var/run/docker.sock grantsfull control of the host's Docker daemon — root-equivalent. Fix: remove it or use a scoped API proxy.
privileged: true, network_mode: host, orbinding a sensitive port to 0.0.0.0 in compose has the same exposure as its Kubernetes analogs above. Fix: drop privileged, keep the service on an internal network, and bind admin ports to 127.0.0.1.
Return a short list of findings, most severe first (host escape / world-open admin port / public datastore before missing-encryption or versioning), each with the fixing field named. If the config is already hardened on a surface, say so plainly rather than manufacturing a finding — a clean review is a valid result.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-04 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.5-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 16 cases were attempted. The headline lift of +38 percentage points is the difference between those two pass rates over the 16 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.