Securing Repositories and CI/CD — GitHub, GitHub Enterprise Server, GitLab and Azure DevOps
Merge controls, pipeline tokens and OIDC, fork and injection risks, deployment gates and build runners on GitHub, GHES, GitLab and Azure DevOps, with the plan each feature needs and how to recover from mistakes.
Overview
A repository and its pipeline together hold the keys to production. Whoever can merge to the default branch, edit a workflow file, or run a job on a privileged runner can usually ship code or read deployment credentials. Most real incidents are not exotic: an admin merges past a failing check, a workflow runs a fork's code with secrets in scope, a third-party action tag is moved to a malicious commit, a token can reach every repository in the organization, or a cloud key sits in a secret for years.
This page covers four platforms and is careful about which feature exists where: GitHub.com (Free, Pro, Team, Enterprise Cloud), GitHub Enterprise Server (GHES, self-hosted), GitLab (Free, Premium, Ultimate) and Azure DevOps (Services and Server). The same ideas repeat on each: gate the merge, give the pipeline the least identity it needs, keep untrusted code away from secrets, gate the deploy, and treat build machines as hostile.
> [!IMPORTANT] Plan and version matter. Rulesets, environments on private repositories, push protection, GitLab approval settings and protected environments all depend on the plan or tier. Each section names the plan the vendor documents.
Symptom to first move
- A commit reached main without review — GitHub activity view, filtered by activity type such as Force pushes; audit log protected_branch.policy_override — Merge controls on GitHub
- remote: error: GH006: Protected branch update failed for refs/heads/main. — Read the second line: which required check or review is missing — Merge controls on GitHub
- A developer approved their own change in GitLab or Azure Repos — Check Prevent approvals by users who add commits or Prohibit the most recent pusher from approving their own changes — Merge controls on GitLab and Azure DevOps
- A workflow on a fork PR could write to the repo or read secrets — Look for pull_request_target plus a checkout of the PR head — Untrusted code in pipelines
- A secret was printed in a log or pushed — Revoke and rotate the credential first, then clean up — Mistakes
- A pipeline in one Azure DevOps project read another project's repo — Check Limit job authorization scope to current project — Pipeline identity
- Self-hosted runner left with old files or processes between jobs — Move to ephemeral or JIT runners — Build agents and runners
- Long-lived cloud keys stored as CI secrets — Replace with OIDC or workload identity federation — Pipeline identity
What you will be able to do
- Configure merge controls so that no single person can push or merge to the default branch alone, on GitHub, GHES, GitLab and Azure Repos, and name the plan each control needs.
- Choose between GitHub branch protection rules and rulesets, and explain how they combine.
- Cut pipeline tokens down to least privilege and replace stored cloud keys with OIDC or workload identity federation.
- Spot the pull_request_target pwn request and script injection patterns in a workflow and fix them.
- Run self-hosted runners and agents safely: ephemeral or JIT, grouped, isolated, kept up to date.
- Recover from mistakes such as a committed secret, a deleted branch, an admin bypass or a leaked runner token.
Key concepts
- Branch protection rule
- GitHub per-branch settings such as required reviews and checks; only one rule applies to a branch, and admins bypass it unless the rule includes them.
- Ruleset
- A named, layered list of GitHub rules with an enforcement status and an explicit bypass list; several can apply, and the most restrictive version of each rule wins.
- Most recent push approval
- A rule that the last push to a pull request must be approved by someone other than the pusher; GitLab and Azure Repos have equivalents.
- Job authorization scope
- The Azure Pipelines setting that limits a job token to the current project instead of the whole organization.
- OIDC federation
- A CI job presents a short-lived signed token to a cloud, which checks claims such as sub and aud and returns temporary credentials, so no long-lived key is stored.
- pull_request_target
- A GitHub trigger that runs in the base repository context with secrets and a write token; it must never build the pull request head code.
- Script injection
- Untrusted text such as a pull request title expanded into a shell script; fixed by passing the value through an environment variable.
- SHA pinning
- Referencing an action or reusable workflow by full commit SHA, the only immutable reference; tags can be moved.
- JIT runner
- A just-in-time GitHub self-hosted runner that performs at most one job and is then removed.
- Protected environment
- A deploy target that only allowed users can deploy to, with approvals and scoped secrets; GitHub environments, GitLab protected environments and Azure environments with checks.
When to use it
- You are setting up or auditing branch protection, rulesets, protected branches or branch policies so nobody can merge alone.
- Your pipelines store long-lived cloud keys and you want to move to OIDC or workload identity federation.
- You run workflows on pull requests from forks, or use third-party actions, and need to keep secrets away from untrusted code.
- You operate self-hosted runners or agents and need to decide between hosted, static and ephemeral autoscaled machines.
- A secret leaked, an admin bypassed a rule, or a runner token was exposed, and you need a recovery plan.
- You are preparing for a DevOps, platform or security interview that covers GitHub, GitLab or Azure DevOps.
Interview questions
- Securing the git repo and CICD pipelines for ADO, gitlab, GitHub and GitHub enterprise server
- How to secure merges and pull requests so anyone cannot merge it - GitHub Security
- Best practices in GitHub workflow
- Managing build agents, and what's the experience around that area
- A developer pushed a cloud access key to the default branch an hour ago. Walk me through your response.
- What is a pwn request in GitHub Actions, and how do you safely run automation on pull requests from forks?
- How do you replace long-lived cloud credentials in CI with OIDC, and what do you restrict in the trust policy?
- In Azure DevOps, a pipeline in one project was able to clone repositories from other projects. Why, and how do you fix it?
- What happened in the tj-actions/changed-files compromise, and which controls limit the damage from a compromised third-party action?
- A self-hosted runner's registration or authentication token was posted in a chat channel. What do you do?
Sign up free to read the full topic
More DevOps topics
- Repository Structure and Multi-Tenancy
- Running AI/ML Workloads on Kubernetes
- Runtime Security
- Sanitizers and Memory Debugging
- SAST, DAST, SCA & IAST
- Secrets Management in GitOps
- Security and Supply Chain
- Security, Secrets and Supply Chain
- Self-Hosted CI Runners at Scale
- Serverless Patterns
- Service Mesh
- Service Networking Errors — DNS, Timeouts, Port Mismatch and Mesh Sidecars
Start free on Cariara · Coding problems · System design studio