Runner isolation & protected runners
Keep feature branches off production runners.
The most dangerous pipeline scenario is untrusted code on a trusted runner. CI runs the code in a merge request, and if a fork or feature-branch MR executes on the same runner that holds your production deploy credentials, an attacker’s MR can simply print or exfiltrate those secrets. Runner isolation exists to make sure the runner that can reach production never runs code that has not been reviewed.
Protected runners
A runner can be marked "protected," which means it only runs jobs from protected branches and tags. Pair that with protected variables (only exposed on protected refs) and you get a clean split: untrusted MR pipelines run on ordinary runners with no production secrets, while the runner that holds deploy credentials only ever executes reviewed code from main. A feature-branch job physically cannot land on the production runner.
Ephemeral, isolated execution
Beyond who-runs-where, harden how jobs run. Use the docker or kubernetes executor so each job gets a fresh, disposable container/pod and nothing persists between jobs — a poisoned build cannot leave a backdoor for the next one. Avoid the shell executor (jobs share the host) and privileged mode. On the Kubernetes executor, apply the same pod hardening from the container course: non-root, no privilege escalation, dropped capabilities.
[[runners]]executor = "kubernetes"[runners.kubernetes]privileged = false[runners.kubernetes.pod_security_context]run_as_non_root = truerun_as_user = 10001# build images with Kaniko/BuildKit-rootless, not privileged DinD