Git, CI/CD & GitOpsLXXIII · Push versus Pull DeploymentFoundations
The pull model — a controller inside the cluster reaches out to Git
What you'll learn
- Describe how the pull model moves state from Git to the cluster
- Identify the credentials a pull-based controller must hold
- Trace the request path of an Argo CD sync from Git to cluster
- Recognise why the pull model inverts the trust boundary of the push model
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
The pull model inverts the direction of the push model. The active agent is no longer the CI runner; the active agent is a long-running controller inside the cluster. The controller authenticates outward to Git (or to a Git proxy) on a read path, then applies the result inward to the cluster’s API server on a write path that never leaves the cluster’s network.
The shape of the model in one picture:
flowchart LR
Dev["Developer"] -->|"git push"| Repo["Git repository"]
Repo -->|"read by"| Ctrl["Controller in cluster"]
Ctrl -->|"read-only deploy key"| Repo
Ctrl -->|"in-cluster write"| API["Cluster API"]
API --> K8s["Desired state in cluster"]
The controller is the active agent. It is a pod (or set of pods) in the same cluster whose state it manages. It reads Git, computes a diff against the current cluster state, and applies the diff using credentials it already has inside the cluster.
What the controller holds
In a pull-based GitOps deployment the controller must hold:
- Read access to the Git repository. A deploy key (SSH), a fine-grained personal access token, or a credential issued by a Git proxy like a Cloud Source Repository access token. The key is read-only; the controller never needs to commit.
- Write access to the cluster API. A ServiceAccount token
bound to a tightly-scoped
RoleorClusterRole, IRSA-bound on EKS, Workload-Identity-bound on GKE. The token is held in the controller pod, where the cluster itself protects it. - A reconciliation interval. The cadence at which the controller re-reads Git and re-applies. Argo CD’s default is three minutes; Flux’s is one minute. The interval is the system’s reconciliation latency ceiling.
argocd app sync "${APP_NAME}"
This is the explicit-sync command. The controller is already reconciling on its polling cadence; this command forces an immediate reconciliation, useful for hot-fixes where waiting one polling interval is unacceptable.
Why the pull model is the GitOps default
Three reasons:
- The cluster’s API server can be sealed. No inbound write path is required; the controller reaches out. An attacker on the internet cannot apply a manifest - they have nowhere to send it.
- The credentials are scoped to the cluster. The kubeconfig
for the controller is a ServiceAccount the cluster itself
issues, with a
Rolethat names exactly the resources the controller manages. There is no human holding it; there is no runner that can leak it; there is no CI step that can echo it. - Drift is corrected continuously. The controller is not a one-shot pipeline; it is a long-running loop. Any drift - manual edit, broken rollback, hostile actor - is reverted on the next tick.
flowchart LR
subgraph Inside["Cluster network"]
C["Controller pod"] -->|"loop"| API2["Cluster API"]
end
subgraph Outside["Internet"]
A["Attacker"] -.->|"cannot reach"| API2
end
The arrow from “Attacker” to “Cluster API” is dotted because it does not exist: there is no inbound route. The controller is the only writer, and it only writes what Git says.
Where the trust boundary sits
In the pull model, the trust boundary is between Git and the controller. The controller must trust Git; Git must hold a deploy key. The cluster’s API server is no longer reachable from outside the cluster’s network - it is reachable only from inside the cluster, by the controller pod, on the in-cluster network.
The inversion is precise:
| Concern | Push model | Pull model |
|---|---|---|
| Active agent | CI runner | Controller in cluster |
| Direction | Outside -> in | Git -> inside (loop) |
| Inbound to API | Required | Not required |
| Cluster creds in | CI runner | Controller pod only |
| Git creds in | CI runner (R/W) | Controller (R/O) |
| Drift correction | On next pipeline | On next tick |
The pull model is not a universal replacement for the push model
- lessons in this part cover the cases where each is right - but it is the right default for clusters that can host their own controller and teams that have a Git repository they trust as a source of desired state.
Production discipline
- Controller credentials are cluster-scoped, not runner-scoped.
The ServiceAccount the controller uses must be a dedicated
identity, with a
Rolethat names exactly the resources the controller manages. Cluster-admin is a placeholder, not a production setting. - Git deploy keys are read-only. The controller never needs to push to Git. A read-only key (or a fine-grained token with read only) limits what a controller compromise can exfiltrate through the Git side.
- The reconciliation interval matches the change cadence. A three-minute poll is fine for hourly deploys; a thirty-second poll is required for hot-fixes that need to land within the hour. The interval is the latency ceiling.
Cross-course references
- Ansible for Production Sysadmins - Parts covering AWX / Ansible Tower describe the pull-mode analogue for configuration management.
- Terraform for Production Sysadmins - Parts on the operator pattern describe the pull analogue for infrastructure provisioning.
- Kubernetes for Production Sysadmins - Parts on controllers cover the in-cluster reconciliation loop pattern the GitOps controller relies on.
Quiz
Knowledge check · 4 questions
Q1. In a pull-based GitOps model, who holds the credentials that can write to the cluster API?
Q2. In the pull model, the GitOps controller typically has read-only access to the Git repository.
Q3. What three things does a pull-based GitOps controller hold or be configured with?
Q4. Identify which part of the trust boundary has shifted and what that means for the attacker.
Team Q migrated a workload from a push-based pipeline (CI runner authenticating to the cluster with a kubeconfig) to a pull-based GitOps model with Argo CD. The cluster's API server endpoint was disabled after the migration. An external attacker compromises the CI runner via a malicious dependency and exfiltrates its environment variables, which now contain only a GitHub token with read-write to the manifests repository. The attacker pushes a malicious manifest to a feature branch and opens a pull request.
Passing score: 75%. Answers are checked in this browser.