Git, CI/CD & GitOpsXXXVIII · CI ArchitectureCI Architecture
The three-plane model — control plane, runner, and environment
What you'll learn
- Name the three planes a CI run touches: control plane, runner, and environment
- Identify what data crosses each boundary and in which direction
- Recognise the trust boundary between the control plane and the runner
- Recognise the trust boundary between the runner and the target environment
- Use the three-plane model to reason about credential placement and network reachability
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
A CI run is not a single process. It is three processes, on three different machines, exchanging data across two trust boundaries. The control plane decides what to run. The runner runs it. The environment is what the runner changes. Conflating the three planes is the architectural mistake this part of the course exists to undo.
The three planes
flowchart LR
CP["Control plane\n(forge: GitHub, GitLab,\nworkflow state, secrets store,\nartifact store)"]
RUN["Runner\n(fresh VM or persistent agent,\npipeline file, checkout,\nstep outputs)"]
ENV["Environment\n(cloud account, k8s cluster,\nbare metal, target system)"]
CP -->|"job dispatch,\nstep log tail,\nsecret injection"| RUN
RUN -->|"plan/apply, push,\ndeploy, exec"| ENV
ENV -->|"apply result,\ndeploy log,\ndrift state"| RUN
RUN -->|"status report,\nartifact upload"| CP
- Control plane. The forge (GitHub, GitLab, Jenkins controller). It owns the workflow file, the trigger evaluation, the secret store, the artifact store, the runner pool, and the status UI. It does not run your code.
- Runner. A machine allocated for one job. It checks out the commit, executes the steps, holds the in-memory secrets for the duration of the job, and reports results. The runner is the only plane that runs your code.
- Environment. The system the change targets - a cloud account, a Kubernetes cluster, a server, a database. The runner reaches it over the network using credentials the job was given.
The pipeline file defines the contract between the three planes: what the runner pulls from the control plane, what it sends to the environment, what it reports back.
The two trust boundaries
flowchart LR
CP["Control plane\n(trusted forge)"]
B1["Boundary 1\n(forge -> runner):\ncode, secrets, dispatch"]
RUN["Runner\n(partially trusted)"]
B2["Boundary 2\n(runner -> environment):\nAPI calls, deploy payloads"]
ENV["Environment\n(production target)"]
Every CI run crosses two trust boundaries:
- Boundary 1: control plane to runner. The forge ships the pipeline file, the commit, the secrets (as environment variables), and the dispatch token. The runner trusts these because the forge signs them.
- Boundary 2: runner to environment. The runner ships the
Terraform apply, the
kubectl apply, theansible-playbookcall. The environment trusts the runner’s credentials because the identity was minted by the control plane or an OIDC provider.
The mistake is to assume the runner and the control plane are the same trust zone. A runner that processes untrusted pull requests is a runner that, for the duration of the job, is in the attacker’s trust zone. The control plane’s secrets are not safe on that runner unless scoped to jobs that do not process untrusted input.
Where the data lives
Each plane owns different data. The control plane owns the workflow definition, trigger history, encrypted secrets, artifacts, and OIDC signing keys. The runner owns the checkout, the step logs in memory, the secrets as environment variables, the build cache, and the artifact staging directory. The environment owns the desired state (Terraform state, k8s manifests), the credentials the runner used, and the audit trail (CloudTrail, kube-apiserver audit log).
The runner is ephemeral. The control plane is durable. The environment is the asset. The cross-boundary question is always: what crosses, in which direction, with which authentication, and which plane is the source of truth.
Production discipline
- Name the plane before naming the tool. Decide what each plane owns before picking the YAML keywords that wire them.
- Scope secrets to the plane that needs them. Secrets that cross more than one boundary have wider blast radius.
- Treat the runner as untrusted for untrusted jobs. A fork PR’s runner is in the attacker’s trust zone. Scope secrets and isolate the network.
Cross-course references
- Linux for Production Sysadmins - Part XXXIV (ConfigMgmt) applies the same model to package builds.
- Ansible for Production Sysadmins - Part XXXVII (RepoArch) treats molecule runner, controller, and target as three planes.
- Terraform for Production Sysadmins - Parts IX-XII (State) treat Terraform Cloud, CI runner, and cloud account as three planes.
Quiz
Knowledge check · 4 questions
Q1. A pipeline runs `terraform apply` against a production AWS account. Which plane holds the AWS access key during the run, and which plane validates it?
Q2. The runner and the control plane are the same trust zone, because the runner was allocated by the control plane.
Q3. Name the three planes a CI run touches, and identify which plane is the source of truth for the pipeline definition, for the deploy target's state, and for the in-job secret material.
Q4. Diagnose which trust boundary was crossed when an attacker exfiltrated a production AWS key from a CI run on a fork pull request.
An external contributor opens a pull request from a fork. The pipeline runs lint, terraform plan, and terraform apply. The job was scoped to use the production AWS account credentials because the team did not want to maintain separate plan and apply credentials. The fork PR's terraform plan step prints the AWS_SECRET_ACCESS_KEY environment variable to the build log. The attacker harvests the key from the public fork PR's CI log.
Passing score: 75%. Answers are checked in this browser.