---
name: iac-misconfig-review
source: https://app.decimal.ai/s/iac-misconfig-review@1/SKILL.md
source_sha256: 3118ef6b6419
---

# IaC Misconfig Review

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.

## When to activate

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.

## How to review

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.

## Catalog A — Containers & Kubernetes

- **Runs as root (default).** No `runAsNonRoot: true` and no non-root `runAsUser`, or a
  Dockerfile 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.** `privileged: true` disables almost all isolation — the container gets the
  host's devices and capabilities and is effectively root on the node. Fix: remove it; grant
  only the specific capabilities actually needed.
- **Privilege escalation allowed (default).** `allowPrivilegeEscalation` is *true* unless set
  false, letting a process gain more privileges than its parent (e.g. via setuid binaries).
  Fix: set `allowPrivilegeEscalation: false`.
- **Host namespaces.** `hostNetwork`, `hostPID`, or `hostIPC: true` drop the isolation
  boundary — 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.
- **Host path mounts.** A `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.
- **Writable root filesystem (default).** Without `readOnlyRootFilesystem: true` an attacker
  can drop tools or tamper with the image at runtime. Fix: set it true and mount an
  `emptyDir` for the few paths that need writes.
- **Capabilities not dropped.** Keeping the default capability set (or adding `SYS_ADMIN`,
  `NET_ADMIN`, `NET_RAW`) is more power than most apps use. Fix: `drop: ["ALL"]`, then add
  back only what is required.
- **Unpinned image.** An image tagged `:latest` (or untagged) is mutable — the running code
  is not the reviewed code, and a poisoned upstream tag ships silently. Fix: pin to a
  version tag or, better, a digest.
- **Secrets in the manifest.** A password, token, or key as a literal `env` `value:` (or in a
  ConfigMap) is stored in plaintext and visible to anyone who can read the object. Fix:
  reference a `Secret` via `secretKeyRef`, or an external secret store.

## Catalog B — Terraform & cloud resources

- **World-open admin/datastore ports.** A security-group ingress with
  `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.)
- **Public object storage.** An `acl = "public-read"`/`"public-read-write"`, or a bucket
  policy 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 reachable datastore/instance.** `publicly_accessible = true` on an RDS instance
  or 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.
- **Unencrypted at rest (default).** `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.
- **Hardcoded secrets.** A literal `password`, `secret_key`, `access_key`, or private key
  string 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.
- **IMDSv1 allowed (default).** Instance `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.
- **Audit/versioning off where it matters.** Object storage with versioning disabled, or a
  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.

## Catalog C — Dockerfile & docker-compose

- **No `USER` — runs as root.** A Dockerfile that never drops from root runs the app as UID 0
  inside the container. Fix: create and switch to a non-root user before the entrypoint.
- **Secrets baked into layers.** A credential passed via `ENV` or `ARG`, or `COPY`ing 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.
- **Mounting the Docker socket.** A compose service that mounts `/var/run/docker.sock` grants
  full control of the host's Docker daemon — root-equivalent. Fix: remove it or use a scoped
  API proxy.
- **Privileged / host-mapped compose service.** `privileged: true`, `network_mode: host`, or
  binding 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`.

## Output

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.
