Your pipeline holds the keys to production and signs off on everything you ship, so harden both the pipeline itself and the artifacts it builds.
Your CI/CD pipeline is the most privileged thing in your engineering org. It holds credentials to production, runs code from every branch and pull request, and signs off on the artifacts your users trust. Compromise the pipeline and an attacker skips every other control. So securing CI/CD is two jobs at once: locking down the pipeline itself, and vouching for what it ships. This guide covers both, ordered by impact.
Three changes stop the majority of real-world pipeline attacks, and none of them take a week:
AWS_ACCESS_KEY_ID secrets with OIDC federation so jobs assume a role at runtime.@v4 is mutable; a SHA is not.Everything after this is worth doing, but these three move the needle first.
The worst secret is the one that never expires. A leaked long-lived cloud key works until someone notices, usually after the invoice or the incident.
Use OpenID Connect (OIDC) so your runner exchanges a short-lived signed token for a scoped cloud role. No static keys live in CI at all. Here is the GitHub Actions to AWS pattern:
permissions:
id-token: write # required to request the OIDC token
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
with:
role-to-assume: arn:aws:iam::123456789012:role/ci-deploy
aws-region: us-east-1
role-session-name: gha-${{ github.run_id }}
On the AWS side, the trust policy restricts which repo and branch can assume that role, so a token minted for another repo is useless. The credentials it hands back last minutes, not months.
For the secrets you still genuinely need (a third-party API token, a signing key), keep them scoped and short-lived, pull them from a vault at job time, and never echo them into logs. Then add secret scanning (Gitleaks, TruffleHog, or your platform's native scanner) on every push and pull request. Treat any hit as a rotation event, not a code-review comment. The secret is already in git history.
Default pipeline permissions are almost always too broad. Set the top-level token to read-only and grant write only on the specific jobs that need it. On GitHub that means permissions: contents: read at the workflow level, elevated per job. Give each job the narrowest cloud role it can do its work with, and separate build credentials from deploy credentials so a compromised test step cannot push to production.
Pull requests from forks are the classic poisoning vector. A fork can propose a workflow change or a malicious build script that runs with your secrets if you let it. Use pull_request (not pull_request_target) for untrusted contributions so the run executes without access to secrets, require manual approval for first-time contributors, and never let fork code reach a job that holds deploy credentials.
Pin third-party actions to a full commit SHA, not a tag or branch. Tags are mutable, and a maintainer compromise or a malicious retag turns @v3 into arbitrary code inside your privileged runner. Use Dependabot or Renovate to keep those SHAs current so pinning does not rot into stale, vulnerable versions.
Gates are where DevSecOps stops being a slogan. Each runs in CI and fails the build on findings above your threshold. Wire them so high and critical severities break the pipeline; leave medium and low as reported-but-passing so you do not train everyone to ignore red.
A scan-and-fail gate looks like this:
- name: Scan image, fail on high/critical
uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # v0.24.0
with:
image-ref: ${{ env.IMAGE }}:${{ github.sha }}
severity: HIGH,CRITICAL
exit-code: '1' # non-zero exit stops the pipeline
ignore-unfixed: true # don't block on CVEs with no fix yet
The exit-code: '1' is what turns a report into a gate. Without it, the scan is decoration.
Passing gates tells you the artifact was clean when it was built. Supply-chain integrity lets you prove, at deploy time, that the thing you are about to run is exactly that artifact.
Lock down where artifacts live. Require authentication and least-privilege access on your registry, enable immutable tags so a pushed digest cannot be silently replaced, and restrict who can publish.
On the source side, turn on branch protection for your main branches: required status checks (your gates above), required reviews, and no direct or force-pushes. Require signed commits where you can. Finally, keep audit logs for the pipeline, the registry, and cloud role assumptions, and ship them somewhere an attacker who owns the pipeline cannot edit. When something goes wrong, that trail is how you scope the blast radius.
For the application code flowing through all of this, our application security best practices guide covers the vulnerability classes your SAST and DAST gates are looking for.
If you are starting from a typical pipeline with static keys and no gates, spend your first week here: move to OIDC, add SCA and image scanning as build-breaking gates, and pin your actions to SHAs. That combination removes the credentials worth stealing, blocks the vulnerabilities most likely to be exploited, and closes the action-poisoning path.
Then invest in supply-chain integrity: SBOMs, cosign signing, and verify-before-deploy. It pays off later, during the incident where you need to prove exactly what ran in production. A pipeline that mints short-lived credentials, fails loudly on known-vulnerable dependencies, and refuses to deploy anything it cannot verify is a genuinely hard target.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
Explore more articles in this category
A service mesh solves real problems and creates new ones. This is the map: what it actually does, when it earns its cost, and how the options compare.
A practical, layer-by-layer checklist for securing Kubernetes clusters, workloads, networking, secrets, and the software supply chain.
A practical walkthrough to install Istio, turn on automatic mTLS, and run a canary traffic split on Kubernetes.
Evergreen posts worth revisiting.