Git, CI/CD & GitOpsLXXIII · Push versus Pull DeploymentSecurity
Blast radius — what a CI compromise affects, what a controller compromise affects
What you'll learn
- Enumerate the actions a compromised CI runner can take in each model
- Enumerate the actions a compromised controller can take in each model
- Identify the resources neither holder can reach directly
- Recognise why the pull model narrows but does not eliminate the blast radius
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
Blast radius is the answer to one question: if this credential is compromised, what can the attacker do?. The push model and the pull model have different answers, and the difference is the single most important reason the pull model exists.
This lesson is the precise accounting: for each model, what a compromised CI runner can do, what a compromised controller can do, and what neither holder can reach directly.
Blast radius of a CI compromise
Push model. A compromised CI runner holds cluster credentials (typically a kubeconfig with the scope the pipeline needs) and Git credentials. The attacker can:
flowchart LR
Att["Compromised CI runner"] --> A1["Write to cluster API directly"]
Att --> A2["Push commits to Git repository"]
Att --> A3["Issue cloud-provider API calls if creds present"]
Att --> A4["Pivot to other systems holding shared secrets"]
The first arrow - write to cluster API directly - is the one that makes the push model’s blast radius severe. The attacker does not need to wait for any defensive layer; the credential is a direct write path.
Pull model. A compromised CI runner holds Git credentials (read-write) but no cluster credentials. The attacker can:
flowchart LR
Att2["Compromised CI runner"] --> B1["Push commits to Git repository"]
Att2 --> B2["Wait for controller to reconcile"]
Att2 --> B3["Cannot reach cluster API directly"]
The third arrow is the inversion. The cluster API is no longer reachable from the runner’s network; the attacker has to go through Git, through branch protection, through review, and through the controller’s reconciliation cadence. Each step is a gate.
The pull model narrows the CI compromise blast radius from “direct write to cluster” to “push commits to Git and wait for the controller to do its job”.
Blast radius of a controller compromise
Pull model. A compromised controller holds cluster credentials (a ServiceAccount token scoped to a tight Role) and a read-only Git deploy key. The attacker can:
flowchart LR
Att3["Compromised controller"] --> C1["Write to cluster API within its Role scope"]
Att3 --> C2["Read Git repository"]
Att3 --> C3["Cannot write to Git repository"]
Att3 --> C4["Cannot reach other clusters unless federated"]
The attacker can drive cluster state, but only within the Role’s
scope. A controller scoped to one namespace cannot pivot to
another. A controller scoped to deployments.apps cannot
create new RoleBindings. The Role is the blast boundary.
The controller cannot write to Git. The deploy key is read-only by design. This means a controller compromise cannot hide its tracks in the Git history - the commits it tries to push will be rejected by Git itself.
Push model. A push-based deployment typically does not have a controller - it has a CI runner that fires on a webhook and applies. The “controller compromise” framing does not apply. If the CI runner is compromised, the attacker has the cluster credentials; if the CI runner is not, there is no controller to compromise.
What neither holder can do
In either model, certain resources are unreachable by a single credential compromise:
- Audit logs from other systems. The cluster’s audit log records API calls; an attacker who can write to the cluster cannot rewrite the audit log of a separate system (CloudTrail, GitHub audit log) without a separate credential.
- Resources in other clusters. A controller scoped to cluster A cannot drive cluster B unless cluster B trusts cluster A’s identity (federation, signed attestations).
- Resources in offline backups. A controller or runner can apply new state but cannot retroactively rewrite a backup taken before the compromise began.
- The history prior to the compromise. Git history is content-addressed and immutable; an attacker who can push a new commit cannot rewrite a commit that already exists without being detected by SHA mismatch.
flowchart LR
subgraph Unreachable["Reachable only with separate credentials"]
U1["CloudTrail / cloud audit log"]
U2["Other clusters"]
U3["Offline backups"]
U4["Pre-compromise Git history"]
end
These are the layers that make a compromise recoverable. A cluster compromise can be reverted; an audit-log compromise would be much harder to revert, which is why the audit log lives in a separate system.
Comparing the two models
A compact comparison:
| Compromise | Push model | Pull model |
|---|---|---|
| CI runner | Direct cluster | Push commits only; |
| write; full | wait for controller; | |
| cluster blast | repo controls gate | |
| Controller | N/A | Cluster write within |
| Role scope; cannot | ||
| write Git | ||
| Git repo | CI can drive | Both CI and the |
| cluster via CI | controller’s view of | |
| Git are inputs to the | ||
| next reconcile |
The pull model is strictly narrower in both rows. The price is operational: a controller to deploy, a polling interval to tune, a Git repository trusted as the source of desired state.
Production discipline
- Treat the blast radius as the design constraint. The credential scope is the answer to “what is the worst case if this holder is compromised?”. If the answer is “everything”, the scope is wrong.
- Layer independent defences. The pull model is one layer. Branch protection, signed commits, signed tags, admission policies, audit logs in separate systems - each is a layer that an attacker has to defeat independently.
- Rehearse the rollback. Every deployment system needs a rehearsed path to revert state. The pull model’s rollback is “revert the commit in Git and let the controller reconcile”; the push model’s rollback is “rerun the pipeline with a reverted manifest”. Both paths must work without the compromised credential.
Cross-course references
- Kubernetes for Production Sysadmins - Parts on RBAC cover the Role-scoping discipline that constrains controller blast radius.
- Terraform for Production Sysadmins - Parts on blast radius cover the same framing for infrastructure provisioning.
- Linux for Production Sysadmins - Parts on incident response cover the operational discipline of rehearsed rollback.
Quiz
Knowledge check · 4 questions
Q1. In the pull model, what can a compromised CI runner NOT do?
Q2. In the pull model, a compromised controller can write commits to the Git repository.
Q3. Name two kinds of resources a single credential compromise cannot reach in either deployment model.
Q4. Compare the blast radius of the same attacker action under the two models.
A malicious dependency executes in CI. In Team S (push model), the runner holds a kubeconfig with namespace-scoped write access and a GitHub token. In Team T (pull model, Argo CD), the runner holds only a GitHub token; the controller holds a ServiceAccount scoped to namespace 'prod' and a read-only deploy key. Both teams' runners are compromised by the same dependency. Compare what the attacker can do in each team.
Passing score: 75%. Answers are checked in this browser.