Files
command-center/agent/managed-skills/infra-repo-security-review/SKILL.md
T

4.6 KiB

name, description
name description
infra-repo-security-review Full-repo security review of an infra/config repo (serverAppConfigs-shaped: shell deployers, quadlets, systemd, Caddy edge, bats/Playwright) — slice fan-out, mechanical censuses, and subagent-claim verification.

Whole-repo security review (infra/config repo)

For "full security review of this repo" on a deployment-config repo (shell scripts + podman quadlets + systemd units + Caddy + tests). NOT for a diff review — that is code-review. NOT for a secrets-only question — that is git-repo-secret-audit.

1. Slice, then fan out in ONE task batch

Five security-reviewer slices, each with the repo's own laws pre-injected in context so no agent re-derives them (AGENTS.md policy text + exclusions: docs prose, .AGENTTRASH/**, binaries, package-lock.json):

  1. Shell — all *.sh + .githooks/ + tests/run.sh: injection, quoting, mktemp vs /tmp/fixed, set -e, curl -f, secrets in argv, rm/git clean/systemctl disable.
  2. Python — every *.py: subprocess(shell=True), verify=False, unbounded read(), path traversal on request-supplied filenames, fixed /tmp writes, which HTTP servers exist and which are unauthenticated.
  3. Container + systemd posture — every *.container/.network/.service/Containerfile.
  4. Edge + web — Caddyfile vhost inventory, Anubis/Authentik chain, response headers, static-asset XSS sinks, service configs (limiter.toml, prometheus.yml, alertmanager.yml).
  5. Tests — vacuous assertions + credentials/secrets in fixtures + harness safety.

Set a yield deadline in the prompt. Subagents here get killed at task.maxRuntimeMs (30 min) and the dead ones produced zero output. When one runs long, write early: write agent://<id> "yield now, unconfirmed items prefixed UNVERIFIED" — the resumed agent returns its partial report instead of losing it. Two of four needed this.

2. Verify every citation before reporting

verify-subagent-review-findings applies in full: re-grep each cited file:line and compare text. Expect ~40% of lines to be off (e.g. an agent described a 13-line hook; the file is 6) and several files to be hallucinated entirely (.githooks/compose/*.container did not exist). Discard mismatches; publish a Discarded subagent claims section. Re-grep your own citations too — two of mine were off by two lines and had to be corrected in an addendum.

3. Mechanical censuses (cheap, high yield — run these yourself)

  • Port binding: every ^PublishPort= line, classify 127.0.0.1: / 192.168.0.x: / no address prefix (= implicit 0.0.0.0). The unprefixed set is the auth-bypass surface: a LAN client hits the container port directly and skips the Caddy→Anubis→Authentik chain that the vhost enforces. Report as one consolidated finding listing all unprefixed lines, and cite the repo's own correct examples (127.0.0.1:9090, 192.168.0.143:8080) as the fix pattern.
  • Image pinning: count ^Image= lines with vs without @sha256: (this repo: 4 of 54 pinned) — floating tags + Pull=never = unreproducible edge.
  • Secret modes: grep every chmod 600 in the deployer and diff against every file it writes from an .env; a > redirect without the chmod is the finding (the Caddy/Porkbun env file).
  • Systemd hardening: User=/ProtectSystem/PrivateTmp/NoNewPrivileges per unit, and ExecStartPre=+ lines — a + hook on a root unit running a checkout-writable script is a local privesc.
  • Vhost table: vhost → upstream → guarded? → exemption documented? Cross-check each upstream against the quadlet PublishPort list; unguarded vhost with no exemption comment is a policy violation even when the service self-authenticates.
  • Hook wiring: git config core.hooksPath + ls "$(git rev-parse --git-path hooks)". A repo can ship .githooks/pre-commit that git never executes — and its own test can assert the file is executable and still miss that it isn't installed.
  • Secret sweep: git cat-file --batch-all-objects --batch (all history + dangling), match known values read from ~/.config/secrets; report prefix…(len N, sha 10) only, never the value. Watch for the split-concat disguise (PW="as""set"): their own regex can't see it, but grep -F on a fragment finds it and history keeps it forever → rotation required.

4. Report

Severity-ranked table (Critical→Low, one line each: location + flaw), then prose detail for Critical/High only, then the Discarded-claims section, then remaining coverage gaps, then an ordered fix set and an offer to branch. Do not create a report file unless asked — chat is the deliverable.