Security gates in a CI/CD pipeline: what to check, and where
Every pipeline I build does the same things in the same order. It checks for leaked secrets, scans for vulnerabilities, runs the application's tests, and deploys to OpenShift, and every image gets scanned with Trivy and signed with cosign. On the developer platform I built, a developer clicks through a window 3 times and that whole workflow runs. Nobody has to remember the security steps, because they aren't optional.
This page walks through each gate, what it catches and where it goes wrong. For federal teams, the last section maps the gates to the practices in NIST SP 800-218, the Secure Software Development Framework (SSDF).
The gates, in order
1. Secret-leak check
Scan the change for credentials before anything else runs. It's the cheapest gate, and it stops one of the most expensive mistakes. If a secret gets through, rotate it first and clean the history second. Getting secrets out of Git covers where they should live instead.
2. Vulnerability scan
Scan the built image and its dependencies, and fail the build on the severities you've agreed on. I use Trivy. The details, including the March 2026 Trivy compromise and how to run the scanner so it can be trusted, are in scanning images with Trivy in GitHub Actions.
3. Tests
The application's own tests run against the same image that will ship. A security gate that passes an image the tests never ran against isn't worth much.
4. Sign
Once the image passes, sign it. I sign images with cosign. The signature says this exact image, by digest, came out of this pipeline.
Anyone can then check it with the public key:
cosign verify --key cosign.pub "registry.example.com/my-app@sha256:${DIGEST}"
5. Verify at admission
A signature only helps if something checks it. Where you own the cluster, a policy engine such as Kyverno can check the signature when a pod is created and refuse images your pipeline didn't sign. Where you don't own the cluster, find out what the platform team already enforces before you promise an auditor anything.
A Kyverno policy that refuses unsigned images from your registry looks like this. Field names have moved between Kyverno versions, so check it against the docs for the version you run. It sets the failure action on the rule, since the older policy-wide setting is deprecated. It only checks images from registry.example.com. Images from anywhere else pass this rule untouched, so pair it with a rule that only allows your registry.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
spec:
webhookTimeoutSeconds: 30
rules:
- name: verify-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- failureAction: Enforce
imageReferences:
- "registry.example.com/*"
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
<your cosign public key>
-----END PUBLIC KEY-----
The gates as a workflow
Here are the first four gates as GitHub Actions steps. It assumes Podman, Trivy and cosign are on the runner and the image registry login is already done. Swap the test command for your own.
jobs:
build:
runs-on: my-runners
steps:
- uses: actions/checkout@<full-commit-sha> # v4
- name: Secret-leak check
run: trivy fs --scanners secret --exit-code 1 .
- name: Build and push
id: build
run: |
podman build -t registry.example.com/my-app:${{ github.sha }} .
podman push --digestfile digest.txt registry.example.com/my-app:${{ github.sha }}
echo "image=registry.example.com/my-app@$(cat digest.txt)" >> "$GITHUB_OUTPUT"
- name: Vulnerability scan
run: trivy image --severity HIGH,CRITICAL --exit-code 1 --ignore-unfixed "${{ steps.build.outputs.image }}"
- name: Tests
run: podman run --rm "${{ steps.build.outputs.image }}" npm test
- name: Sign
run: cosign sign --yes --key env://COSIGN_PRIVATE_KEY "${{ steps.build.outputs.image }}"
env:
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
Around the gates
- Non-root containers. OpenShift enforces this by default, and it's worth enforcing everywhere.
- Runners you control. The pipeline holds deploy credentials, so where it runs matters. Self-hosted runners on OpenShift covers runners that shut down after each build.
- Pin what you depend on. Actions by full commit SHA, images by digest.
- The pipeline is code. The workflow lives in the repository and changes through review like everything else.
What goes wrong
- Too strict on day one. Every build fails, and people route around the pipeline. Start in report-only mode, then turn the gates on one at a time.
- Findings with no path to a fix. On my platform the developer sees the scan results and can update to the recommended versions in one step. Without something like that, findings pile up.
- A gate that can be skipped. If a developer can deploy around the pipeline, the gates are only advice. The deploy path should accept only what the pipeline produced.
- Signing without verifying. Covered above, and very common.
Mapping to NIST SP 800-218 (SSDF)
NIST SP 800-218, SSDF version 1.1 (February 2022) describes practices, not tools. Below is where each gate lines up with a practice, quoting the document. Treat it as a starting point for your own mapping, not as an attestation.
- Security tools built into the pipeline, and the pipeline as code: PO.3, "Use automation to reduce human effort and improve the accuracy, reproducibility, usability, and comprehensiveness of security practices throughout the SDLC." Task PO.3.2 gives code-based configuration for toolchains, such as pipelines-as-code, as an example.
- Locked-down runners that shut down after each build: PO.5, "Ensure that all components of the environments for software development are strongly protected," and task PO.5.1, "Separate and protect each environment involved in software development."
- Secret-leak check: PW.7, "Help identify vulnerabilities so that they can be corrected before the software is released." A credential in the code is one of those.
- Pinned actions and images, protected repositories: PS.1, Protect All Forms of Code from Unauthorized Access and Tampering.
- Vulnerability scan: PW.4.4, "Verify that acquired commercial, open-source, and all other third-party software components comply with the requirements," whose examples include building automatic detection of known vulnerabilities into the toolchain. Also RV.1, Identify and Confirm Vulnerabilities on an Ongoing Basis.
- Scan-to-fix and accepted exceptions: RV.2, Assess, Prioritize, and Remediate Vulnerabilities, and task RV.2.2, "Plan and implement risk responses for vulnerabilities."
- Signing: PS.2, Provide a Mechanism for Verifying Software Release Integrity, and task PS.2.1, "Make software integrity verification information available to software acquirers."
When it makes sense to get help
If you have one pipeline and one app, add the gates yourself in the order above. Bring in help when there are many teams and pipelines to bring to one standard, when an audit or a federal customer wants evidence that the gates run on every build, or when the gates already exist but nobody fixes what they find.