All articles CI/CD

Securing Your CI/CD Pipeline: An AppSec Checklist

Yael Kochman

Securing Your CI/CD Pipeline

A CI/CD pipeline that deploys without security gates is a direct path from a developer's branch to production. The pipeline is where every change that ships passes through, which makes it both the most efficient place to enforce security controls and the most valuable target for supply chain attacks. Getting the controls right at the pipeline level protects everything downstream.

This checklist covers the controls we consider essential rather than aspirational. Not all of them apply to every stack, but each one addresses a real attack class that has been exploited in production environments.

Source Control Protections

Require signed commits for production branches. Commit signing with GPG or SSH keys allows the pipeline to verify that commits on main or release branches came from authorized contributors. Without signing, a pipeline trigger can be crafted from an account whose credentials were compromised. Configure the branch protection rule to reject unsigned commits rather than just flag them.

Protect branch rules against override. Branch protection rules that can be bypassed by repository administrators or pipeline service accounts reduce their own value. Verify that the rules cannot be modified by the same accounts that run deployments.

Review CODEOWNERS files regularly. CODEOWNERS files define who must approve changes to specific paths. Stale entries, especially those pointing to former employees or deprecated teams, create approval gaps on sensitive paths without obvious indication in the repository.

Secret Management

Scan for secrets before every commit and in every PR. Pre-commit hooks and CI-integrated secrets scanning are complementary. Pre-commit catches secrets before they enter version control. CI scanning catches secrets that bypass the hook, from machines where the hook is not installed, or committed via web editor. Tools should cover entropy-based detection (catching high-entropy strings that match credential patterns) in addition to regex-based detection of known patterns like AWS access key formats.

Rotate secrets found in history. When a secret is detected in git history, the rotation has to happen before the remediation commit. A secret that is no longer in the HEAD commit but remains in history is still exposed to anyone with repository read access. Git history rewriting (git filter-repo) removes the string from history, but rotation must precede it because the secret's exposure window starts from the commit that introduced it.

Use short-lived credentials wherever the platform supports them. OIDC-based authentication between the CI runner and cloud providers (GitHub Actions OIDC with AWS IAM, for example) eliminates static long-lived secrets for the most common case: deploying to cloud infrastructure. The pipeline receives a token valid for the duration of the job. There is no static secret to rotate or leak.

Dependency and Build Integrity

Pin dependency versions and lock hash verification. Floating version references (^1.4.0) resolve to whatever the latest minor version is at install time. A compromised package that introduces a malicious version in a minor range will be installed silently. Pinning to exact versions combined with lockfile hash verification ensures the build installs exactly what was tested. For package managers that support it (npm, pip with --require-hashes), enforce hash verification in CI.

Scan dependencies on every build, not only on weekly schedules. A dependency vulnerability disclosed on Tuesday affects every build that runs on Wednesday. Weekly or biweekly scans create disclosure windows where a known-vulnerable dependency ships to production. Integrating SCA scanning into the build step, with a policy that blocks promotion on critical or high severity findings, closes this window.

Use a private package registry or mirror for internal packages. Dependency confusion attacks exploit the package manager's preference for public registry packages over internal ones when both share a name. Using a private registry that explicitly scopes internal package names, combined with pip/npm configuration that prevents fetching internal package names from the public registry, removes the attack surface. At minimum, register internal package names in the public registry to block squatting.

Build Environment Isolation

Run builds in ephemeral environments. Reusing build runners across jobs creates opportunities for cross-job contamination: artifacts, credentials, or environment variables from a previous job persisting into the current one. Ephemeral runners (a new VM or container per job) eliminate this. Cloud-native CI platforms provide ephemeral runners by default; self-hosted runners require explicit configuration.

Restrict network access from build environments. A build environment with unrestricted outbound network access is a pivot point for supply chain attacks. Malicious code in a compromised dependency can exfiltrate secrets from the build environment, reach internal services, or download second-stage payloads. Egress restrictions to the known set of registries and endpoints the build requires limit what compromised code can do.

Audit third-party actions and pipeline integrations. GitHub Actions, GitLab CI includes, and similar pipeline extension mechanisms allow third-party code to run in the build environment with access to secrets. Pin third-party actions to a specific commit SHA rather than a mutable tag (uses: actions/checkout@abc123f rather than uses: actions/checkout@v4). A tag can be moved to point to a different commit; a SHA cannot.

Deployment Gates

Require security scan results before production promotion. A deployment pipeline that does not block on security findings produces a scan result that developers learn to ignore. The policy question is not whether to scan but what scan results block promotion. A reasonable starting policy: block on new critical or high severity findings introduced in the current change, while existing findings below that threshold require a tracked exception rather than a hard block.

Separate deployment credentials from development credentials. The service account or role that triggers production deployments should have no access to development or staging infrastructure and vice versa. Credential separation limits blast radius: a compromised development pipeline cannot promote to production, and a compromised deployment token cannot reach development environments where research data or test credentials might exist.

Log all deployment events with the triggering identity. Deployment logs that record which pipeline run triggered the deployment, which commit was deployed, and which service account authorized the action provide the audit trail needed to investigate unauthorized changes. These logs should be immutable and retained independently of the pipeline infrastructure itself.

What This Checklist Leaves Out

This list covers pipeline-level controls. It does not cover container image security (base image scanning, distroless builds, image signing), infrastructure-as-code security scanning, or runtime protection in the deployed environment. Each of those is a separate layer. Pipeline security is the point where code becomes deployable artifact. Securing that transition does not substitute for securing the artifact itself or the environment it runs in.

Tenzai

See findings and fixes together, not just findings.

Free plan available. Connect your first repository in under five minutes.

Start Free Trial

More from the Tenzai blog