Pipeline Examples
Reference pipelines showing where each security control belongs. Calibrate thresholds against your own backlog before enabling blocking behaviour.
GitLab DevSecOps Pipeline
End-to-end pipeline covering code scanning, image scanning, SBOM generation, DAST and a manual production gate.
- Source→
- SAST→
- SCA→
- Secret Scan→
- Build→
- Container Scan→
- SBOM→
- DAST→
- Security Gate→
- Deploy
stages: [scan, build, verify, dast, deploy] variables: IMAGE: $CI_REGISTRY_IMAGE/app .scan_defaults: &scan_defaults stage: scan allow_failure: false interruptible: true sast: <<: *scan_defaults image: returntocorp/semgrep script: - semgrep --config auto --sarif --output semgrep.sarif . artifacts: reports: { sast: semgrep.sarif } secret_detection: <<: *scan_defaults image: zricethezav/gitleaks:latest script: - gitleaks detect --source . --redact --report-path gitleaks.json dependency_scan: <<: *scan_defaults image: aquasec/trivy:latest script: - trivy fs --exit-code 1 --ignore-unfixed --severity HIGH,CRITICAL . build_image: stage: build image: quay.io/buildah/stable script: - buildah bud -t "$IMAGE:$CI_COMMIT_SHA" . - buildah push "$IMAGE:$CI_COMMIT_SHA" container_scan: stage: verify image: aquasec/trivy:latest script: - trivy image --exit-code 1 --ignore-unfixed --severity CRITICAL "$IMAGE:$CI_COMMIT_SHA" sbom: stage: verify image: anchore/syft:latest script: - syft "$IMAGE:$CI_COMMIT_SHA" -o cyclonedx-json=sbom.cdx.json artifacts: paths: [sbom.cdx.json] expire_in: 400 days dast_baseline: stage: dast image: ghcr.io/zaproxy/zaproxy:stable script: - zap-baseline.py -t "$STAGING_URL" -r zap.html -I artifacts: paths: [zap.html] deploy_production: stage: deploy environment: { name: production } when: manual rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH script: - ./deploy.sh "$IMAGE:$CI_COMMIT_SHA"- Use protected variables for registry and deployment credentials.
- Run scheduled full DAST scans separately from merge-request pipelines.
GitHub Actions Secure Build
Least-privilege workflow with digest-pinned actions, OIDC cloud access, image scanning and build provenance.
- Checkout→
- SAST→
- SCA→
- Build→
- Scan→
- Attest→
- Push
name: releaseon: push: branches: [main] permissions: contents: read jobs: secure-build: runs-on: ubuntu-latest permissions: contents: read id-token: write packages: write attestations: write steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Static analysis uses: returntocorp/semgrep-action@v1 with: { config: auto } - name: Dependency scan run: | curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \ | sh -s -- -b /usr/local/bin trivy fs --exit-code 1 --ignore-unfixed --severity HIGH,CRITICAL . - name: Build image id: build run: | IMAGE=ghcr.io/${{ github.repository }} docker build -t "$IMAGE:${{ github.sha }}" . echo "image=$IMAGE:${{ github.sha }}" >> "$GITHUB_OUTPUT" - name: Scan image run: trivy image --exit-code 1 --ignore-unfixed --severity CRITICAL "${{ steps.build.outputs.image }}" - name: Push and capture digest id: push run: | docker push "${{ steps.build.outputs.image }}" echo "digest=$(docker inspect --format='{{index .RepoDigests 0}}' \ "${{ steps.build.outputs.image }}" | cut -d@ -f2)" >> "$GITHUB_OUTPUT" - name: Generate build provenance uses: actions/attest-build-provenance@v1 with: subject-name: ghcr.io/${{ github.repository }} subject-digest: ${{ steps.push.outputs.digest }}- Pin third-party actions by commit SHA, not by tag.
- Never expose secrets to workflows triggered by forked pull requests.
Jenkins Declarative Security Pipeline
Declarative Jenkinsfile with parallel security stages, credential binding and an approval step before deployment.
- Checkout→
- Parallel scans→
- Build→
- Image scan→
- Approval→
- Deploy
pipeline { agent { kubernetes { yamlFile 'ci/agent.yaml' } } options { timeout(time: 45, unit: 'MINUTES') } environment { IMAGE = "registry.example.com/app" TAG = "${env.GIT_COMMIT.take(12)}" } stages { stage('Security scans') { parallel { stage('SAST') { steps { sh 'semgrep --config auto --error .' } } stage('Secrets') { steps { sh 'gitleaks detect --source . --redact' } } stage('Dependencies') { steps { sh 'trivy fs --exit-code 1 --ignore-unfixed --severity HIGH,CRITICAL .' } } } } stage('Build') { steps { sh 'buildah bud -t "$IMAGE:$TAG" .' } } stage('Image scan') { steps { sh 'trivy image --exit-code 1 --ignore-unfixed --severity CRITICAL "$IMAGE:$TAG"' } } stage('Approval') { when { branch 'main' } steps { timeout(time: 8, unit: 'HOURS') { input message: "Deploy ${IMAGE}:${TAG} to production?", submitter: 'release-managers' } } } stage('Deploy') { when { branch 'main' } steps { withCredentials([string(credentialsId: 'kube-token', variable: 'KUBE_TOKEN')]) { sh './deploy.sh "$IMAGE:$TAG"' } } } } post { always { archiveArtifacts artifacts: '**/*.sarif', allowEmptyArchive: true } }}- Use ephemeral Kubernetes agents so build state is not shared between jobs.
- Restrict the approval submitter list to a named group.
Ansible Automation Pipeline
Validation, linting, security scanning, testing and approval before content reaches the automation controller.
- Git→
- Validation→
- Security Scan→
- Ansible Lint→
- Test→
- Approval→
- Automation Controller→
- Deployment
stages: [validate, scan, test, deploy] syntax: stage: validate script: - ansible-playbook --syntax-check playbooks/site.yml lint: stage: validate script: - ansible-lint --profile production playbooks/ secrets: stage: scan script: - gitleaks detect --source . --redact iac_scan: stage: scan script: - kics scan -p . -t Ansible --report-formats json molecule: stage: test script: - molecule test rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" launch_job_template: stage: deploy environment: { name: production } when: manual rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH script: - > curl -sS -X POST -H "Authorization: Bearer $AAP_TOKEN" -H "Content-Type: application/json" -d '{"extra_vars":{"git_ref":"'"$CI_COMMIT_SHA"'"}}' "$AAP_URL/api/v2/job_templates/$TEMPLATE_ID/launch/"- Store the AAP token as a protected, masked CI variable scoped to the default branch.
- Keep execution environment images scanned and version-pinned.
GitOps Deployment with Verification
Manifests reconciled from git with admission-time signature verification and policy enforcement.
- Manifest change→
- Review→
- Policy test→
- Merge→
- Reconcile→
- Admission verify→
- Running
# CI: test manifests against policy before mergepolicy_test: stage: validate image: openpolicyagent/conftest script: - conftest test --policy policies/ k8s/overlays/production ---apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: payments namespace: argocdspec: project: production source: repoURL: https://git.example.com/acme/deploy.git targetRevision: main path: k8s/overlays/production destination: server: https://kubernetes.default.svc namespace: payments syncPolicy: automated: { prune: true, selfHeal: true }- Give the reconciler namespace-scoped permissions where possible.
- Document a break-glass procedure.
Terraform Plan and Apply Pipeline
Separated plan and apply jobs with policy scanning of the plan and different credentials per phase.
- Validate→
- Plan→
- Policy scan→
- Approval→
- Apply
stages: [validate, plan, policy, apply] image: hashicorp/terraform:1.9 validate: stage: validate script: - terraform init -backend=false - terraform validate - terraform fmt -check -recursive plan: stage: plan script: - terraform init - terraform plan -out tfplan.binary - terraform show -json tfplan.binary > tfplan.json artifacts: paths: [tfplan.binary, tfplan.json] expire_in: 1 day policy: stage: policy image: bridgecrew/checkov script: - checkov -f tfplan.json --compact --soft-fail-on LOW apply: stage: apply environment: { name: production } when: manual rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH script: - terraform init - terraform apply -auto-approve tfplan.binary- Never publish tfplan.json to a public artefact store.
- Use OIDC federation instead of static cloud keys.
Container Promotion Pipeline
Images are built into a staging repository and promoted to production only after scanning and signing.
- Build→
- Push staging→
- Scan→
- Sign→
- Promote→
- Deploy by digest
promote: stage: promote image: alpine:3.20 script: - apk add --no-cache skopeo cosign - | DIGEST=$(skopeo inspect --format '{{.Digest}}' \ "docker://$STAGING_REPO/app:$CI_COMMIT_SHA") - cosign verify --certificate-identity-regexp "^https://git.example.com/acme/" \ --certificate-oidc-issuer "$OIDC_ISSUER" "$STAGING_REPO/app@$DIGEST" - skopeo copy "docker://$STAGING_REPO/app@$DIGEST" \ "docker://$PROD_REPO/app@$DIGEST" rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH- Enable tag immutability on the production repository.
- Keep continuous rescanning enabled in the registry.
SBOM and Release Attestation Pipeline
Generates an SBOM per release, signs it as an attestation and publishes it with the release artefacts.
- Build→
- SBOM→
- Sign→
- Attest→
- Publish
sbom: runs-on: ubuntu-latest permissions: { contents: write, id-token: write, packages: read } steps: - uses: anchore/sbom-action@v0 with: image: ghcr.io/${{ github.repository }}@${{ needs.build.outputs.digest }} format: cyclonedx-json output-file: sbom.cdx.json - run: | cosign attest --yes --type cyclonedx \ --predicate sbom.cdx.json \ "ghcr.io/${{ github.repository }}@${{ needs.build.outputs.digest }}" - uses: softprops/action-gh-release@v2 with: files: sbom.cdx.json- Retain SBOMs at least as long as the release is supported.
Scheduled Security Scan Pipeline
Nightly deep scans that are too slow for merge requests: full DAST, cloud posture assessment and artefact rescanning.
- Schedule→
- Full DAST→
- Cloud posture→
- Artifact rescan→
- Report
workflow: rules: - if: $CI_PIPELINE_SOURCE == "schedule" stages: [scan, report] full_dast: stage: scan image: ghcr.io/zaproxy/zaproxy:stable script: - zap-full-scan.py -t "$STAGING_URL" -r zap-full.html -I artifacts: { paths: [zap-full.html] } cloud_posture: stage: scan image: toniblyx/prowler script: - prowler aws --severity critical high --output-formats json-ocsf rescan_artifacts: stage: scan image: aquasec/trivy script: - for tag in $(cat release-tags.txt); do trivy image --severity CRITICAL "$tag" || true; done publish_report: stage: report script: - ./scripts/push-findings-to-tracker.sh- Only run full active DAST against a dedicated, resettable environment.
Pre-production Verification Gate
A single policy job that evaluates all collected scan evidence and decides whether the release may be promoted.
- Collect evidence→
- Evaluate policy→
- Record decision→
- Promote or block
release_gate: stage: gate image: openpolicyagent/opa:latest script: - > opa eval --format pretty --data policy/release.rego --input evidence/release.json "data.release.allow" - > opa eval --fail-defined --data policy/release.rego --input evidence/release.json "data.release.deny[_]" artifacts: paths: [evidence/release.json] expire_in: 400 days- Version the policy alongside the application so decisions are reproducible.