Installs into .claude/skills of the current project.
Are you the author of Infra Audit?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/marcoguillermaz-infra-audit)
---
name: infra-audit
description: Infrastructure and CI/CD security audit - GitHub Actions workflows (pwn-request, secret logging, missing pinning, permissions overreach), Dockerfile (latest tag, USER root, ADD on URL), Kubernetes manifests (runAsNonRoot, privileged containers, hostNetwork), Terraform (IAM wildcards, state in git, module pinning), GitLab CI equivalent checks. Stack-agnostic.
user-invocable: true
model: sonnet
context: fork
argument-hint: [target:layer:<gha|docker|k8s|terraform|gitlab>|target:file:<glob>|mode:all]
allowed-tools: Read Glob Grep Bash(git ls-files:*) Bash(grep:*)
---
## Scope for v1
- **Static analysis only.** Parses workflow files, Dockerfiles, K8s manifests, Terraform files, and GitLab CI configs on disk. Does not execute workflows, does not connect to cloud providers, does not validate runtime state.
- **Five layers**: GitHub Actions, Dockerfile, Kubernetes manifest, Terraform, GitLab CI. Each layer runs only if its markers are detected (see Step 1) - no noisy N/A sections for layers that don't apply.
- **Stack-agnostic**: the detected backend language / framework does NOT affect which checks run. Infrastructure is orthogonal to application stack.
---
## Configuration (adapt before first run)
> Replace these placeholders:
> - `[WORKFLOWS_PATH]` - GitHub Actions workflows (default: `.github/workflows/`)
> - `[DOCKERFILE_PATH]` - Dockerfile location if non-standard (default: `Dockerfile` at repo root)
> - `[K8S_PATH]` - Kubernetes manifest directory (default: `k8s/`, `deploy/`, `manifests/` - auto-detect)
> - `[TERRAFORM_PATH]` - Terraform directory (default: `terraform/`, `infra/`, `iac/` - auto-detect)
> - `[GITLAB_CI_PATH]` - GitLab CI config (default: `.gitlab-ci.yml` at repo root)
---
## Step 0 - Target and mode resolution
Parse `$ARGUMENTS` for `target:` or `mode:` tokens.
| Pattern | Meaning |
|---|---|
| `target:layer:<gha\|docker\|k8s\|terraform\|gitlab>` | Audit only one layer |
| `target:file:<glob>` | Audit a specific file or glob |
| `mode:all` / no argument | **Full audit - every layer whose markers are detected in Step 1.** |
Announce: `Running infra-audit - scope: [FULL | target: <resolved>]`
---
## Step 1 - Layer detection
Detect which layers apply. Each has a marker - absence means the layer is skipped entirely with no findings (not "N/A flagged").
| Layer | Marker |
|---|---|
| **GHA** (GitHub Actions) | `.github/workflows/*.{yml,yaml}` exists |
| **Docker** | `Dockerfile` at repo root OR any `**/Dockerfile*` with content matching `^FROM ` |
| **K8s** | `.{yml,yaml}` files matching `apiVersion:` + `kind:` + common K8s kinds (`Deployment`, `Service`, `Pod`, `StatefulSet`, `DaemonSet`, `Job`, `CronJob`, `Ingress`, `ConfigMap`, `Secret`) |
| **Terraform** | `*.tf` files exist |
| **GitLab CI** | `.gitlab-ci.yml` at repo root |
Announce each detected layer: `Layers detected: [GHA × 4 workflows, Docker × 1, Terraform × 7 files]`.
If **no layers detected**: `No infrastructure files found. Expected: .github/workflows/, Dockerfile, k8s manifests, Terraform, or .gitlab-ci.yml.` Exit 0.
Stack-specific patterns for each layer live in `${CLAUDE_SKILL_DIR}/PATTERNS.md`.
---
## Step 2 - Per-layer static checks
Run the checks for each detected layer. Skip layers without markers. Record findings as SEVERITY / CHECK / FILE:LINE / EVIDENCE / IMPACT / FIX.
### Layer A - GitHub Actions (GHA-1 to GHA-7)
- **GHA-1 Pwn-request pattern** (**Critical**): `pull_request_target` trigger + `actions/checkout` with a ref from `${{ github.event.pull_request.head.sha }}` or no explicit ref. Runs untrusted PR code with write access to repository secrets. Patterns in PATTERNS.md → "GHA-1".
- **GHA-2 Secret logging** (**Critical**): `echo` / `printf` / `Write-Host` with a secret expansion: `${{ secrets.X }}` in an `echo` statement, or secret piped to `tee` / `cat`. Secret leaks into run logs indexable by anyone with repo read access.
- **GHA-3 Unpinned actions** (**High**): `uses: <action>@main` / `@master` / `@v<major>` without SHA pin. Supply-chain vulnerability - action author can push malicious commits to the tag and every workflow pulls it on next run.
- **GHA-4 Permissions overreach** (**High**): workflow without explicit top-level `permissions:` block AND `GITHUB_TOKEN` writes to the repo (push, PR comment, issue write, release). Default token has write-all; explicit `permissions:` should restrict.
- **GHA-5 Self-hosted runner on public PR** (**Critical**): `runs-on: self-hosted` (or group label) in a workflow triggered by `pull_request` on a public repo. Public contributors can execute code on your self-hosted runner.
- **GHA-6 Untrusted input in run command** (**High**): direct expansion of `${{ github.event.pull_request.title }}`, `${{ github.event.issue.body }}`, `${{ github.head_ref }}`, `${{ github.event.comment.body }}` into a `run:` step without `env:` quoting. Script injection.
- **GHA-7 Workflow modification permission** (**Medium**): `permissions: write-all` or `permissions.actions: write` without necessity - allows a compromised run to modify workflow files.
### Layer B - Dockerfile (D-1 to D-6)
- **D-1 Latest tag** (**Medium**): `FROM <image>:latest` or `FROM <image>` without tag. Build not reproducible; next rebuild may pull a different image.
- **D-2 Root user** (**High**): no `USER` directive, or `USER root` / `USER 0` in final image. Container process runs as root unnecessarily.
- **D-3 ADD with URL** (**High**): `ADD http(s)://...` statement. Prefer `RUN curl ... && ...` with explicit verification (checksum or signature).
- **D-4 Unpinned base image** (**Medium**): `FROM <image>:<tag>` without digest (`@sha256:...`). Tag-based pulls can be silently repointed by the registry.
- **D-5 Secret in build arg or env** (**Critical**): `ARG <name>` + `ENV <name>=$<name>` where `<name>` matches `(?i)(token|key|secret|password|pwd|credential)`. Secret baked into image layers.
- **D-6 Apt without cleanup** (**Low**): `apt-get install` without `rm -rf /var/lib/apt/lists/*` in the same RUN layer. Image bloat; not a security issue but noise.
### Layer C - Kubernetes (K-1 to K-7)
- **K-1 runAsNonRoot missing** (**High**): container / pod `securityContext` missing `runAsNonRoot: true`. If base image runs as root, container runs as root.
- **K-2 allowPrivilegeEscalation true** (**High**): `securityContext.allowPrivilegeEscalation: true`. Permits setuid escalation - rarely needed.
- **K-3 Privileged container** (**Critical**): `securityContext.privileged: true`. Full host access - near-equivalent to root on the node.
- **K-4 hostNetwork / hostPID / hostIPC** (**Critical**): pod specs with `hostNetwork: true`, `hostPID: true`, `hostIPC: true`. Namespace breakout risk.
- **K-5 Writable root filesystem** (**Medium**): `securityContext.readOnlyRootFilesystem` missing or `false`. Container can write anywhere; makes malware persistence easier.
- **K-6 Secret as env var** (**Medium**): `env` block with `valueFrom.secretKeyRef` → the secret appears as an environment variable. `volumeMounts` from a Secret volume is preferred (does not appear in process env).
- **K-7 ImagePullPolicy not Always for mutable tag** (**Low**): `image: ...:latest` with `imagePullPolicy: IfNotPresent` or missing. Tag updates won't propagate to running nodes.
### Layer D - Terraform (T-1 to T-6)
- **T-1 IAM wildcard action** (**Critical**): policy resource (`aws_iam_policy`, `aws_iam_role_policy`) with `Action: "*"` or `Action: "<service>:*"` granting full service access.
- **T-2 IAM wildcard resource** (**High**): same policies with `Resource: "*"`. Scope should be restricted to specific ARNs.
- **T-3 Public S3 bucket** (**High**): `aws_s3_bucket_public_access_block` with `block_public_acls = false` / `restrict_public_buckets = false`, OR `aws_s3_bucket_acl` with `acl = "public-read"` / `"public-read-write"`.
- **T-4 State file in git** (**High**): `terraform.tfstate` or `*.tfstate*` tracked in git (check `git ls-files`). State contains secrets in plain text.
- **T-5 Module without version pin** (**Medium**): `source = "..."` pointing to a git URL without `?ref=<tag|sha>`, or `source = "hashicorp/..."` without `version = "~> X.Y"`. Drift risk.
- **T-6 Hardcoded secret** (**Critical**): string literal matching `(?i)(password|secret|key|token)\s*=\s*"[^"]+"` in `.tf` files (exclude `data` blocks fetching from a secret manager).
### Layer E - GitLab CI (GL-1 to GL-4)
- **GL-1 Secret logging** (**Critical**): `echo $CI_*_TOKEN`, `echo $SECRET_*`, or explicit secret variable name expanded into `echo` / `tee` / stdout. Same principle as GHA-2.
- **GL-2 Unpinned image** (**Medium**): `image: <name>:latest` or `image: <name>` without tag in `.gitlab-ci.yml`.
- **GL-3 Unprotected runner tags** (**High**): job running on a shared runner with access to protected variables (`rules: - if: $CI_COMMIT_REF_PROTECTED == "true"` missing for jobs that consume `$CI_*_TOKEN`).
- **GL-4 Script injection via CI variable** (**High**): `script:` step containing direct expansion of user-controllable vars (`$CI_COMMIT_MESSAGE`, `$CI_MERGE_REQUEST_TITLE`) without quoting.
---
## Step 3 - Report
```
## Infrastructure Audit - [DATE] - [SCOPE] - layers: [LAYERS_DETECTED]
### Executive summary
[2-5 bullets. Critical + High findings only. Concrete facts: file:line, layer, impact. If nothing Critical/High: state "No critical or high findings across N layers."]
### Infrastructure maturity assessment
| Dimension | Rating | Notes |
|---|---|---|
| GHA security | strong / adequate / weak / N/A | [count of GHA-1..7 findings; N/A if GHA not detected] |
| Container hygiene | strong / adequate / weak / N/A | [D-1..6 findings; N/A if Docker not detected] |
| K8s posture | strong / adequate / weak / N/A | [K-1..7 findings; N/A if K8s not detected] |
| Terraform discipline | strong / adequate / weak / N/A | [T-1..6 findings; N/A if Terraform not detected] |
| GitLab CI hygiene | strong / adequate / weak / N/A | [GL-1..4 findings; N/A if GitLab CI not detected] |
| Release readiness | ready / conditional / blocked | [blocked = any Critical] |
### Check verdicts
[One row per layer detected, listing each check ID + verdict + finding count. Skip entire rows for layers not detected in Step 1.]
### Prioritized findings
For each finding with severity Medium or above:
[SEVERITY] [ID] [check] - [file:line] - [evidence excerpt] - [impact] - [fix] - [effort: S / M / L]
### Quick wins
[Findings meeting: (a) Medium or High, (b) effort S, (c) single-file fix]
Format: "INFRA-[n]: [one-line description]"
If none: state explicitly.
```
---
## Step 4 - Backlog decision gate
Present Medium-or-above findings as numbered decision list (Critical → High → Medium):
```
Found N findings at Medium or above. Which to add to backlog?
[1] [CRITICAL] INFRA-? - .github/workflows/ci.yml:42 - pwn-request pattern
[2] [HIGH] INFRA-? - Dockerfile:8 - running as root
[3] [MEDIUM] INFRA-? - k8s/deploy.yaml:24 - writable root filesystem
...
Reply with numbers to include (e.g. "1 2 4"), "all", or "none".
```
**Wait for explicit user response before writing anything.**
Persist approved entries via the backlog write-once protocol (`.claude/rules/backlog-protocol.md`) — session scratch in an active block, direct to `docs/refactoring-backlog.md` standalone:
- Assign ID: `INFRA-[n]` (next available)
- Add row to priority index
- Add full detail: issue, evidence, fix, effort, risk
### Severity guide
- **Critical**: pwn-request (GHA-1); secret logging in CI (GHA-2, GL-1); self-hosted runner on public PR (GHA-5); privileged container (K-3); hostNetwork/hostPID/hostIPC (K-4); IAM wildcard action (T-1); state file in git (skip if not the issue - see T-4 for exact scoping); hardcoded secret in Terraform (T-6); secret baked into Docker image (D-5)
- **High**: unpinned action (GHA-3); permissions overreach (GHA-4); untrusted input in run (GHA-6); Docker root user (D-2); ADD with URL (D-3); K8s runAsNonRoot missing (K-1); allowPrivilegeEscalation (K-2); IAM wildcard resource (T-2); public S3 bucket (T-3); GitLab unprotected runner (GL-3); GitLab script injection (GL-4)
- **Medium**: workflow modification permission (GHA-7); Docker latest tag (D-1); unpinned base image (D-4); K8s writable rootfs (K-5); K8s secret as env var (K-6); Terraform module without version pin (T-5); GitLab unpinned image (GL-2)
- **Low**: apt without cleanup (D-6); imagePullPolicy not Always (K-7)
---
## Execution notes
- Do NOT modify infrastructure files. Audit only.
- Do NOT connect to cloud providers or clusters. Static file analysis only.
- If a layer has markers but the check subset is not exhaustively applicable (e.g. a Dockerfile without `RUN apt-get`), skip the inapplicable checks silently - do not flag "N/A".
- This skill complements `/security-audit` (application-layer auth/validation) and `/migration-audit` (DB schema safety). Run in Phase 5d Track C on every block, especially blocks that modify workflow files, Dockerfiles, K8s manifests, or Terraform.
- After the report, ask: "Do you want me to prepare the corrections for the identified findings?" Reply with `yes` only after user sign-off.