BrickellTechnologies

Service

CI/CD and Pipeline Security

Your build system can read every secret, deploy to production, and publish what your customers install. It runs unattended, and it is configured in a file that gets less review than the code it ships.

The shape of the problem

A build pipeline is a remote code execution service that you built on purpose, gave credentials to, and pointed at production. That is not a criticism; it is what a pipeline is for. The trouble is that everything else with that much privilege gets change control, access review and an audit trail, while the pipeline gets a YAML file and a green tick.

So the questions worth asking are not really about the build. They are the same questions you would ask about any privileged system. Who can make it run? What can they make it do? What does it hold that they should not reach?

What we test

What a pull request can make it do

The interesting findings live here. A workflow triggered on pull_request_target runs with write permissions and access to secrets, in the context of the base repository, and it is very easy to write one that then checks out and executes code from the fork. That is a straight path from "anyone on the internet opened a PR" to "arbitrary code with your deploy credentials". We look for that shape and for the quieter variants: a build script sourced from the branch, a linter config that can execute, a cache key an attacker controls.

OIDC trust policies

Federating your CI into a cloud role instead of holding long-lived keys is the right move, and it moves the risk into the trust policy. The condition needs to name the branch or the environment, rather than only the repository. Scoped to the repository alone, every workflow can assume the role, which includes whatever a contributor adds. We read those conditions line by line, because this is the single most common serious finding in this category.

Secrets

Which jobs can read which secrets, whether the scoping matches what those jobs actually need, and whether any of them end up somewhere durable. Masking in logs is not a control; it is a courtesy that fails on anything transformed, base64'd or split. We check the history too, because a rotated secret that is still reachable in a build log is still a secret.

Runners

Self-hosted runners are where isolation quietly disappears. If the same machine runs both trusted internal builds and jobs from public pull requests, one contaminates the other, and ephemeral runners exist precisely because persistent ones accumulate state an attacker can leave behind. We check what is shared, what persists between jobs, and what the runner's own identity can reach on your network.

Dependencies and actions

Every third-party action you reference by a mutable tag is code you have agreed to run, sight unseen, whenever its author changes it. Pinning to commit SHAs removes that in exchange for an update chore. We map what is pinned, what is not, and which of the unpinned ones sit on a path that reaches secrets or production.

The build-it half

Assessment alone tends to produce a spreadsheet nobody works through, so most of this engagement is usually construction: hardened workflows written and reviewed, permissions reduced to what each job needs, branch protection and required checks configured, secret scanning and dependency review switched on and tuned so the alerts are worth reading, and signing with provenance attached to what you ship.

If the same pipeline also deploys into a cloud account, this pairs directly with cloud security posture, since the pipeline's identity is usually one of the more privileged principals in the account.

Questions we get asked

Why does the pipeline need its own assessment? It is just build scripts.

Because it is the most privileged thing you own. It can read every secret it is given, write to production, publish artefacts your customers install, and it does all of that unattended. Meanwhile the code that drives it lives in a YAML file that gets reviewed less carefully than application code, and it is frequently editable by anyone who can open a pull request. That combination does not exist anywhere else in your estate.

What do you actually look at?

Who can trigger a run and with what inputs; what a pull request from a fork can make it execute; where secrets are stored, which jobs can read them, and whether any of them reach logs; the trust policy on OIDC federation, which is the one people get wrong most; what a compromised dependency or action would inherit; whether self-hosted runners are shared between trusted and untrusted work; and whether anything is signed on the way out.

What is the mistake you find most often?

OIDC trust policies scoped to a repository rather than to a branch or environment. The intent is "our deploy pipeline can assume this role"; the effect is "any workflow in that repository can, including one added in a pull request". It is a two-line fix and it is nearly always wrong on the first look.

We use GitHub Actions with third-party actions everywhere. Is that a problem?

It is a real exposure and it is manageable. An action referenced by a mutable tag runs whatever that tag points at today, inside your pipeline, with your secrets. Pinning to a commit SHA removes that, and it costs you an occasional update chore. We check what is pinned, what is not, and which of the unpinned ones actually sit on a path that can reach anything sensitive.

Do you only do the assessment, or can you build the thing?

Both, and most clients want the second half. Findings are easy to produce and tedious to act on, so we will write the hardened workflows, set up the branch protections and required checks, wire in secret scanning and dependency review, and get artefact signing and provenance working. You end up with a pipeline that is hard to abuse rather than a report saying the old one was.

Know what an attacker would reach first.

Tell us what you run and what worries you. We will come back with a scope, a fixed price and the earliest week we can start.