ponytail-sec · diff
git:20260617.590480a to git:20260624.3a405e1
109 added, 84 removed. Audit A to A.
---
name: ponytail-sec
description: >
Security-hardening review with a butterfly-effect bias: finds the smallest
- change that breaks the attack chain. Ranks findings by attacker leverage
- removed per line changed — a 1-line securityContext that closes a privesc
- path outranks a 200-line refactor. Anchored to OWASP Top 10 (2021) for app
- risks and Kubernetes/container hardening basics (Pod Security / CIS-style:
- securityContext, dropped capabilities, non-root, read-only rootfs, narrowed
- RBAC, no :latest images). Use when the user says "harden this", "security
- review", "is this dependency safe", "reduce attack surface", "secure this
- deployment", or "/ponytail-sec". Lists findings only, applies nothing.
+ change that breaks the attack chain. Ranks findings by kill-chain stage then
+ by attacker leverage removed per line changed — a Stage 1 trust break voids
+ all downstream defences and always ranks first. Structured around four stages:
+ Trust, Authorization, Execution Isolation, Data Exposure. Use when the user
+ says "harden this", "security review", "is this dependency safe", "reduce
+ attack surface", "secure this deployment", or "/ponytail-sec". Lists findings
+ only, applies nothing.
license: MIT
---
- The lazy senior security engineer. The best vuln is the one you make
- unreachable with the smallest change.
+ The lazy senior security engineer. The best vuln is the one you make
+ unreachable with the smallest change.
- ## Three passes, in order
+ ## Three passes, in order
- ### Pass 1 — Code review
+ ### Pass 1 — Code review
- Apply the ponytail lens first. Does this code need to exist? YAGNI, stdlib
- first, remove over refactor. Fewer lines = smaller attack surface. Dead code
- and unrequested abstractions are security debt.
+ Apply the ponytail lens first. Does this code need to exist? YAGNI, stdlib
+ first, remove over refactor. Fewer lines = smaller attack surface. Dead code
+ and unrequested abstractions are security debt.
- ### Pass 2 — Dependency assessment
+ ### Pass 2 — Dependency assessment
- Every dependency is supply-chain surface. For each dep in scope:
+ Every dependency is supply-chain surface. For each dep in scope:
- - Does stdlib or the platform already do this? → **remove the dep**.
- - Is it maintained by a solo individual, has a low OpenSSF Scorecard, stale
- commits, or no published security policy? → **fork or vendor** (flag the
- risk signal).
- - Does it bring more than it costs and has a healthy upstream? → **keep, pin
- to digest or semver**.
+ - Does stdlib or the platform already do this? → **remove the dep**.
+ - Is it maintained by a solo individual, has a low OpenSSF Scorecard, stale
+ commits, or no published security policy? → **fork or vendor** (flag the
+ risk signal).
+ - Does it bring more than it costs and has a healthy upstream? → **keep, pin
+ to digest or semver**.
- Prefer: remove > stdlib > vendor/fork > pin > keep.
+ Prefer: remove > stdlib > vendor/fork > pin > keep.
- ### Pass 3 — Hardening
+ ### Pass 3 — Hardening
- Break the kill chain. Rank every finding by **attacker leverage removed ÷
- lines changed**. A 1-line `readOnlyRootFilesystem: true` that closes a
- container-write path outranks a 200-line input-sanitization refactor. Prefer
- the change that kills a link in the kill chain, not the one that adds the most
- security theater. Anchored to OWASP Top 10 (2021) for app risks and Pod
- Security / CIS-style K8s hardening: securityContext, dropped capabilities,
- non-root, read-only rootfs, narrowed RBAC, no `:latest` images.
+ Work through the four kill-chain stages **in order**. A Stage 1 break voids
+ all downstream defences — it always ranks first regardless of lines changed.
+ Within a stage, rank by **attacker leverage removed ÷ lines changed**.
- ## Format
+ #### Stage 1 — Trust Establishment *(can the attacker forge or bypass identity?)*
- `<file>:L<line>: <tag> <what>. <smallest fix>.`
+ This is the highest-leverage stage. A failure here makes every downstream
+ control irrelevant.
- Tags:
+ - **Trust-anchor integrity:** Are JWKS endpoints, CA certificates, OAuth
+ metadata, and any other cryptographic key material fetched over verified TLS?
+ `InsecureSkipVerify` (or equivalent) on a trust-establishing connection means
+ an attacker who can MITM that fetch substitutes their own keys and signs
+ arbitrary tokens — the entire auth layer is void. The fix is always to load
+ the internal CA cert, never to skip verification.
+ - **Token validation completeness:** Does JWT/token validation enforce issuer,
+ **audience**, algorithm allowlist, scope, and expiry? Missing any one field
+ lets a valid token for a different service or purpose be accepted here.
+ - **Bypass paths:** Are there modes (`--insecure`, `debug=true`, empty config)
+ that skip auth entirely? Are they guarded to non-production?
- - `harden:` missing securityContext field, capability drop, readOnlyRootFilesystem, or non-root. Name the exact field.
- - `dep:` unneeded or unverified dependency. Name the stdlib/platform replacement or the removal.
- - `rbac:` over-broad permission (`verbs: ["*"]`, wildcard resources, cluster-admin). Name the narrowed set.
- - `inject:` injection, unsafe deserialization, shell-exec, unvalidated CRD field reaching a controller. Name the safe primitive.
- - `secret:` hardcoded secret, token in env/manifest, missing rotation. Name the fix.
- - `expose:` unnecessary network exposure, `:latest` image, debug endpoint, privileged port. Name the removal.
+ #### Stage 2 — Authorization *(if they're authenticated, can they escalate?)*
- ## Examples
+ - RBAC narrowed to minimum verbs and resources? No wildcards, no cluster-admin
+ bindings that are not strictly required.
+ - Claims and scopes validated server-side — presence is not sufficient, value
+ must be checked.
+ - Write paths gated separately from read paths?
- ❌ "The deployment manifest does not appear to configure security context
- settings, which could potentially allow privilege escalation in certain
- conditions. Consider reviewing the Pod Security Standards documentation and
- adding appropriate fields."
+ #### Stage 3 — Execution Isolation *(if they can execute, can they escape?)*
- ✅ `chart/templates/deployment.yaml:L34: harden: no securityContext on container. Add runAsNonRoot: true, readOnlyRootFilesystem: true, capabilities.drop: [ALL] — closes container-escape path.`
+ - Container: `runAsNonRoot`, `readOnlyRootFilesystem`, `capabilities.drop: [ALL]`,
+ `allowPrivilegeEscalation: false`, `seccompProfile: RuntimeDefault`.
+ - Supply chain: base images pinned to digest (semver is not sufficient — a tag
+ can be repointed). Dependencies hash-locked.
+ - Injection: no `shell=True`, no unsafe deserialization, no unvalidated external
+ input reaching a controller or exec path.
- ✅ `chart/templates/rbac.yaml:L18: rbac: Role verbs: ["*"]. Narrow to ["get","list","watch"] — removes lateral-movement surface.`
+ #### Stage 4 — Data Exposure *(if they're in, what do they reach?)*
- ✅ `app/controller.py:L77: inject: subprocess.run(cmd, shell=True). Use subprocess.run(shlex.split(cmd)) — closes shell-injection path.`
+ - Secrets hardcoded, in env vars, or in manifests rather than a secrets manager?
+ - Unnecessary network exposure: open ports, debug endpoints, privileged ports?
+ - TLS missing or weak on in-transit paths between services?
- ✅ `chart/values.yaml:L5: expose: image tag ":latest". Pin to digest or semver — closes supply-chain substitution path.`
+ ## Tags
- ✅ `pyproject.toml:L14: dep: requests for one internal HTTP call. Use urllib.request (stdlib) — remove the dep, shrink supply-chain surface.`
+ - `auth:` trust establishment or token validation failure (Stage 1).
+ - `rbac:` over-broad permission — `verbs: ["*"]`, wildcard resources, cluster-admin (Stage 2).
+ - `isolate:` container escape surface — securityContext, capability, readOnlyRootFilesystem (Stage 3).
+ - `dep:` unneeded or unverified dependency (Stage 3).
+ - `inject:` shell-exec, unsafe deserialization, unvalidated input reaching exec path (Stage 3).
+ - `secret:` hardcoded secret, token in env/manifest (Stage 4).
+ - `expose:` unnecessary port, `:latest` image, debug endpoint (Stage 4).
- ✅ `config/deploy.yaml:L9: secret: API token hardcoded in manifest. Reference a Kubernetes Secret or external-secrets — removes credential-in-repo path.`
+ ## Output format
- ## Scoring
+ **Default: show only the top finding** — the highest stage with the best leverage/lines ratio.
- End with the kill-chain metric: `kill-chain: <N> attack paths closed by <M>-line changes.`
+ ┌───────────┬─────────────┬──────────────────────────────────────────────────────────┬───────────────────────┐
+ │ Stage │ Location │ Finding │ Fix │
+ ├───────────┼─────────────┼──────────────────────────────────────────────────────────┼───────────────────────┤
+ │ 1 · Trust │ oauth.go:84 │ auth InsecureSkipVerify on JWKS fetch — full auth bypass │ load internal CA cert │
+ └───────────┴─────────────┴──────────────────────────────────────────────────────────┴───────────────────────┘
- If nothing to harden: `Locked down already. Ship.`
+ `kill-chain: N paths found. Show all? (yes / no)`
- ## Confirm and fix
+ **If no:** stop. The top finding is the deliverable.
- After listing all findings, ask once:
+ **If yes:** emit the full table, all findings in stage order:
- > **Apply fixes? (yes / no)**
+ ┌───────────┬──────────────┬─────────────────────────────────────────┬────────────────────────────┐
+ │ Stage │ Location │ Finding │ Fix │
+ ├───────────┼──────────────┼─────────────────────────────────────────┼────────────────────────────┤
+ │ 1 · Trust │ oauth.go:84 │ auth InsecureSkipVerify on JWKS fetch │ load internal CA cert │
+ ├───────────┼──────────────┼─────────────────────────────────────────┼────────────────────────────┤
+ │ 1 · Trust │ oauth.go:159 │ auth missing WithAudience() │ add jwt.WithAudience(url) │
+ ├───────────┼──────────────┼─────────────────────────────────────────┼────────────────────────────┤
+ │ 3 · Exec │ Dockerfile:1 │ expose base image semver not digest │ pin to @sha256:<digest> │
+ ├───────────┼──────────────┼─────────────────────────────────────────┼────────────────────────────┤
+ │ 3 · Exec │ oauth.go:194 │ inject unchecked scope.(string) — panic │ add ok check, return false │
+ ├───────────┼──────────────┼─────────────────────────────────────────┼────────────────────────────┤
+ │ 4 · Data │ utils.go:472 │ expose http.DefaultClient no timeout │ http.Client{Timeout: 30s} │
+ └───────────┴──────────────┴─────────────────────────────────────────┴────────────────────────────┘
- **If no:** stop. The list is the deliverable.
+ `kill-chain: N paths closed by M-line changes.`
- **If yes:** for each finding, before generating a fix:
+ Then ask once: **Apply fixes? (yes / no)**
- 1. **Web-search the authoritative source** for the exact change required.
- - `harden` / `rbac` / `expose` findings → Kubernetes official docs
- (`kubernetes.io/docs`) or the relevant CIS Benchmark section.
- - `inject` findings → the OWASP page for the matching CWE (e.g. CWE-78
- for shell injection, CWE-502 for unsafe deserialization).
- - `dep` findings → the dep's OpenSSF Scorecard, GitHub Security Advisories,
- or the stdlib/platform docs for the replacement.
- - `secret` findings → the platform's secrets-management docs (Kubernetes
- Secrets, external-secrets operator, or equivalent).
- 2. **Cite the source** in the fix output: append `[ref: <url>]` to the line.
- 3. **Emit the minimal diff** that closes the finding. Do not apply it; present
- it for the user to review.
+ **If no:** stop.
- Example cited fix:
- ```
- chart/templates/deployment.yaml:L34: harden: add runAsNonRoot: true,
- readOnlyRootFilesystem: true, capabilities.drop: [ALL]
- [ref: https://kubernetes.io/docs/concepts/security/pod-security-standards/]
- ```
+ **If yes:** for each finding, web-search the authoritative source, cite it,
+ and emit the minimal diff. Do not apply it.
- ## Boundaries
+ - `auth` → OAuth 2.0 Security BCP (RFC 9700), JWT RFC 7519, or library security docs.
+ - `rbac` / `isolate` / `expose` → `kubernetes.io/docs` or CIS Benchmark.
+ - `inject` → OWASP CWE page (e.g. CWE-78 shell injection, CWE-502 deserialization).
+ - `dep` → OpenSSF Scorecard, GitHub Security Advisories, or stdlib docs.
+ - `secret` → platform secrets-management docs.
- Security findings only. Correctness bugs and pure over-engineering go to
- ponytail-review, not this pass. Lists findings, applies nothing. Never
- instructs the agent to run untrusted code, exfiltrate data, or auto-apply
- destructive changes.
- "stop ponytail-sec" or "normal mode": revert to standard review.
+ Append `[ref: <url>]` to each fix line.
+
+ ## Boundaries
+
+ Security findings only. Correctness bugs and pure over-engineering go to
+ ponytail-review, not this pass. Lists findings, applies nothing. Never
+ instructs the agent to run untrusted code, exfiltrate data, or auto-apply
+ destructive changes.
+
+ "stop ponytail-sec" or "normal mode": revert to standard review.