Git, CI/CD & GitOpsFinal · Final AssessmentFinal Review
CI/CD architecture and runner security — recap
What you'll learn
- Separate the control plane (forge, orchestrator, secrets store) from the data plane (runners, registries, deploy targets)
- Match runner topology (hosted, self-hosted, ephemeral, autoscaled) to workload sensitivity and blast radius
- Identify the three runner risks — privileged mode, mounted Docker socket, long-lived credentials — and the mitigations for each
- Plan an OIDC federation that replaces long-lived cloud secrets with short-lived, job-scoped tokens
Prerequisites
- What CI is and is not — automation, not gatekeeping
- The runner and its environment — hosted, self-hosted, ephemeral, persistent
- The three-plane model — control plane, runner, and environment
- Control plane isolation — why orchestration is separate from execution
- Pipeline as code — the workflow file is committed
- Hosted runners — convenience, isolation, and the control you give up
- Self-hosted runners — control, operational cost, and the security cost
- Ephemeral runners — clean state every job, and the cost of throwing away state
- Runner autoscaling and Actions Runner Controller — scaling on Kubernetes
- The runner threat model — who attacks, how, and with what access
- The Docker socket risk — docker.sock mounted into a runner = root on the host; the escalation path
- Privileged containers and host mounts — what privileged means; the kernel surface
- Secret variables fundamentals — how CI platforms store and serve secrets
- OIDC federation basics — what OIDC is; the trust relationship between CI and cloud
- GitHub Actions OIDC in practice — id-token: write permission; the JWT issuance
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/CD system looks like a single product but is in fact two planes: a control plane that decides what should run, and a data plane that actually runs it. The control plane holds the secrets, the workflow definitions, and the identity of every job. The data plane holds the runners that execute the workflows and the registries that store the artefacts. Confusing the two — running a job on the control plane, mounting the secrets store on the data plane — is the proximate cause of most “the CI was the breach” incidents.
The three-plane model
flowchart TB
subgraph CP["Control plane"]
CF[Forge: GitHub / GitLab]
ORC[Orchestrator: Actions / GitLab CI]
SS[Secrets store]
end
subgraph DP["Data plane"]
R1[Hosted runner]
R2[Self-hosted runner]
R3[Ephemeral runner]
REG[Artifact registry]
end
subgraph T["Targets"]
T1[Cloud account]
T2[Kubernetes cluster]
T3[Terraform state]
end
CF -->|workflow yaml| ORC
ORC -->|job payload| R1
ORC -->|job payload| R2
ORC -->|job payload| R3
SS -->|short-lived secret| R1
SS -->|short-lived secret| R2
SS -->|short-lived secret| R3
R1 --> REG
R2 --> REG
R3 --> REG
R1 -->|OIDC token| T1
R2 -->|OIDC token| T2
R3 -->|OIDC token| T3
- Control plane. The forge (GitHub, GitLab), the orchestrator (the Actions or GitLab CI service), and the secrets store. Should be reachable from the public internet (engineers push from anywhere) but isolated from the data plane’s internal networks.
- Data plane. The runners (hosted, self-hosted, ephemeral) and the registries that hold the artifacts produced by the runners. Talks outbound to the control plane to fetch jobs and outbound to the deploy targets using credentials issued for the specific job.
- Targets. Cloud accounts, Kubernetes clusters, Terraform state buckets. Reachable from runners over the public internet or over a peering connection, with per-job, short-lived credentials only.
Runner topology
The four runner topologies, in order of blast radius:
- Hosted runners. Provided by the forge. Ephemeral per-job. No persistent state between jobs. Low blast radius because the runner is destroyed at the end of the job, but the network egress is shared with every other customer of the forge.
- Self-hosted runners. Persistent. Carry state between jobs unless explicitly cleaned. The classic foothold for lateral movement if one job is malicious.
- Ephemeral runners. Self-hosted but torn down at the end of every job. The default for any workload that handles production credentials or production deploy targets.
- Autoscaled runners. Ephemeral runners launched on demand by a controller (Actions Runner Controller, GitLab Runner autoscaler). The autoscaler decides how many runners to run; the runners themselves are destroyed when the queue empties. The production-grade default.
The three non-negotiable runner controls
The runner threat model collapses to three controls that must be in place for every runner that touches production:
- No privileged mode without justification. A privileged container can do almost anything the host can do, including mounting the host filesystem and breaking out of its namespace. Privileged mode is sometimes required (Docker-in-Docker without rootless alternatives) but it must be justified per-job, not enabled at the runner level.
- No mounted Docker socket. Mounting
/var/run/docker.sockinto a build container is equivalent to giving the container root on the host: any process inside the container can spawn a sibling container with no isolation. The only safe alternative is rootless Docker, BuildKit in rootless mode, or Kaniko in an unprivileged container. - Short-lived OIDC credentials, not long-lived secrets. A long-lived cloud access key in a CI secret is a key that, once leaked, works from any IP on the planet for as long as the key is valid. An OIDC token issued per-job, scoped to a single workflow, and expiring in an hour, is a token that is worthless the moment the job ends.
Production discipline
The five rules that recur across every runner configuration:
- Runners are cattle, not pets. Ephemeral and autoscaled by default. A long-lived self-hosted runner is a foothold waiting to be taken.
- Egress is the new perimeter. Hosted runners share egress IPs with every other customer. A workload that depends on IP allowlists cannot use hosted runners for the production deploy.
- Privileged mode is per-job, not per-runner. A
runner default of
privileged: falsewith explicit opt-in for the one job that requires it. - Docker socket is never mounted. Rootless, or BuildKit, or Kaniko. Every other option is a known escape path.
- OIDC first, secrets last. A long-lived cloud secret in CI is a control failure. OIDC federation is the default; secrets are reserved for the few cases where OIDC is not available.
Cross-course references
- Linux for Production Sysadmins — Parts on container isolation and capability dropping: the same discipline that applies to a production container applies to a build container.
- Kubernetes for Production Sysadmins — The service-account token volume projection pattern is the in-cluster analogue of OIDC federation in CI.
Quiz
Knowledge check · 4 questions
Q1. A production deploy job needs to write to an S3 bucket. The current configuration stores a long-lived AWS access key in the repository's CI secrets. Which change most reduces the blast radius if the secret leaks?
Q2. Mounting the host Docker socket into a build container is equivalent to giving that container root on the host.
Q3. Name the two planes of a CI/CD system and the single non-obvious property of the runner-to-target connection that the well-architected system enforces.
Q4. A self-hosted runner pool has been running the same VMs for six months. A security review finds that the runners are configured with privileged mode enabled by default, the Docker socket is mounted into every job, and a long-lived AWS access key is in the repository secrets. The pool is used for production Terraform applies. Walk through the controls that must be put in place, in order, before the pool is used to deploy anything new.
The pool is a Kubernetes-backed ARC deployment running 4 VMs in a dedicated subnet. Every VM has `privileged: true` in the runner config, mounts `/var/run/docker.sock` from the host, and shares a single AWS access key with `AdministratorAccess`. The Terraform state bucket is in the same AWS account.
Passing score: 75%. Answers are checked in this browser.