DevSecOps

Building a DevSecOps Pipeline: Security Gates from Commit to Production

A practical guide to integrating security checks at every stage of the software development lifecycle—from pre-commit hooks to production monitoring—covering fail-open vs fail-closed gate design and keeping security friction low enough that developers don't route around it.

November 15, 20258 min readShipSafer Team

The Shift-Left Principle

"Shift left" means moving security checks earlier in the development lifecycle—toward the left side of the timeline from design to deployment. The economic case is compelling: NIST data shows a defect found in production costs 30x more to fix than one found during design. A vulnerability caught by a pre-commit hook costs a developer 30 seconds. The same vulnerability found after deployment to production requires a hotfix, a deployment, potential customer notification, and incident investigation.

But shift-left only works if the security checks are integrated into the developer workflow rather than being an external process that developers must remember to run. The goal is to make the secure path the path of least resistance.

Security at Each SDLC Stage

Stage 1: Integrated Development Environment (IDE)

Security feedback in the IDE is the earliest possible intervention. Developers see issues the moment they write the code, before a commit is even made.

Tools:

  • Semgrep for IDE: The Semgrep VS Code extension runs SAST rules on save
  • SonarLint: Connects to your SonarQube/SonarCloud instance and shows issues in the editor
  • Snyk IDE plugins: Available for VS Code, JetBrains, and Visual Studio; highlights vulnerable dependencies and code issues inline

IDE checks must be fast (under 2 seconds for feedback) and low false-positive to avoid alert fatigue. Configure them to run only on changed files, not the entire codebase.

Stage 2: Pre-Commit Hooks

Pre-commit hooks run on the developer's machine before a commit is created. They are the last line of defense before code enters shared version control.

Use the pre-commit framework to manage hooks declarably:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: detect-private-key
      - id: detect-aws-credentials
      - id: check-merge-conflict
      - id: trailing-whitespace

  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.4
    hooks:
      - id: gitleaks
        name: Detect hardcoded secrets

  - repo: https://github.com/pre-commit/mirrors-eslint
    rev: v9.0.0
    hooks:
      - id: eslint
        files: \.(js|ts|jsx|tsx)$
        additional_dependencies:
          - '@typescript-eslint/eslint-plugin'

Pre-commit hooks should be fail-closed for secrets detection—a commit with a detected secret should always be blocked. Secrets in git history are a permanent problem that requires rotation of the credential and ideally a git history rewrite (expensive and disruptive for shared repositories).

Pre-commit hooks must be installed per-developer machine. Automate installation via your project setup script or npm prepare lifecycle script:

{
  "scripts": {
    "prepare": "pre-commit install"
  }
}

Stage 3: Pull Request / Code Review

When a developer pushes a branch and opens a pull request, a richer set of automated checks becomes practical. These checks run on the CI server, not the developer's machine, so they can be more thorough.

Checks to run on every PR:

Secret scanning: Beyond the pre-commit check, run a full-history scan with Gitleaks or Trufflehog to catch secrets that may have been committed before hooks were installed.

Static Application Security Testing (SAST): Semgrep, CodeQL (GitHub Code Scanning), or Checkmarx. Configure these to post results as PR review comments so developers see issues in context.

Software Composition Analysis (SCA): Snyk or Dependabot check for vulnerable dependencies and post findings to the PR.

Infrastructure as Code scanning: If the PR touches Terraform, Kubernetes manifests, or Dockerfiles, run Checkov or Trivy for misconfigurations.

License compliance: Ensure new dependencies do not introduce incompatible licenses (AGPL in a commercial product, for example). FOSSA and Snyk both provide license scanning.

A GitHub Actions workflow for PR security checks:

name: Security Gates
on:
  pull_request:
    branches: [main]

jobs:
  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Semgrep
        uses: returntocorp/semgrep-action@v1
        with:
          config: p/owasp-top-ten p/nodejs

  sca:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Snyk SCA
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high --fail-on=all

  secrets:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for secret scanning
      - name: Run Gitleaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Stage 4: Build

The build stage produces the artifact that will be deployed. Security checks here verify the artifact itself, not just the source code.

Container image scanning: Build the Docker image, then scan it with Trivy or Grype before pushing to the registry.

  container-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        run: docker build -t app:${{ github.sha }} .
      - name: Scan image
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: app:${{ github.sha }}
          format: sarif
          output: trivy-results.sarif
          severity: HIGH,CRITICAL
          exit-code: 1  # Fail the build on HIGH/CRITICAL
      - name: Upload to Code Scanning
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy-results.sarif

Image signing: Sign the container image with Sigstore/cosign to establish provenance. Only signed images should be deployable to production.

cosign sign --key cosign.key app:${{ github.sha }}

SBOM generation: Generate a Software Bill of Materials (Syft, Anchore) and attach it to the build artifact. This enables rapid impact assessment when new CVEs are disclosed.

Stage 5: Staging / Pre-Production

With a working deployment in a staging environment, dynamic testing becomes possible.

Dynamic Application Security Testing (DAST): OWASP ZAP, Burp Suite Enterprise, or Nuclei scan the running application for vulnerabilities that are only visible at runtime (SQL injection, XSS, authentication bypasses).

Integration security tests: Automated tests that specifically verify security controls—ensure that authenticated endpoints return 401 for unauthenticated requests, that IDOR protections are in place, that rate limiting is enforced.

Example OWASP ZAP baseline scan in CI:

  dast:
    runs-on: ubuntu-latest
    needs: deploy-staging
    steps:
      - name: ZAP Baseline Scan
        uses: zaproxy/action-baseline@v0.12.0
        with:
          target: ${{ vars.STAGING_URL }}
          fail_action: true
          issue_title: ZAP Scan Alert

Penetration testing: For major releases (new authentication flows, new payment integrations), schedule a focused penetration test in staging before production deployment.

Stage 6: Production Deployment Gate

Before code reaches production, require a final sign-off gate:

Policy as Code: Tools like OPA (Open Policy Agent) or Conftest can enforce deployment policies:

  • Only signed images can be deployed
  • Images must have passed vulnerability scanning within the last 24 hours
  • No critical vulnerabilities in the deployed artifact
  • SBOM must be present
# OPA policy: deployment_policy.rego
package deployment

deny[msg] {
  not input.image_signed
  msg := "Image must be signed before deployment"
}

deny[msg] {
  input.vulnerability_scan_age_hours > 24
  msg := "Vulnerability scan is too old; rescan required"
}

deny[msg] {
  input.critical_vulnerability_count > 0
  msg := sprintf("Found %d critical vulnerabilities", [input.critical_vulnerability_count])
}

Change approval: For production, require human approval from a security-aware reviewer before deployment proceeds. GitHub Environments with required reviewers implement this natively.

Stage 7: Production Monitoring

Security does not stop at deployment. Continuous monitoring closes the loop:

  • Runtime Application Self-Protection (RASP): Contrast Security, Sqreen (now part of Datadog) instrument the application at runtime to detect and block attacks
  • Web Application Firewall (WAF): Cloudflare WAF, AWS WAF, or similar block common attack patterns
  • Anomaly detection: Alert on unusual API usage patterns, authentication anomalies, data access spikes
  • Dependency monitoring: Receive alerts when new CVEs are published for your production dependencies (Dependabot security alerts, Snyk monitoring)

Fail-Open vs Fail-Closed Gate Design

Every security gate needs a policy for what happens when it fails, is unavailable, or produces an error.

Fail-closed (block on failure): The deployment or commit is blocked if the security check fails or cannot run. This is the right default for high-severity findings: a build with a critical CVE in a production-facing service should not deploy.

Fail-open (allow on failure): The deployment proceeds but the finding is reported. Appropriate for informational findings, low-severity issues, and situations where the check has high false-positive rates that have not yet been tuned.

Practical guidance:

CheckPolicy
Detected secretsFail-closed always
Critical CVE in direct dependencyFail-closed
High CVE in transitive dependencyFail-open with alert
SAST critical severityFail-closed
SAST high severityFail-open with PR comment
Container critical CVEFail-closed
DAST critical findingFail-closed in staging
License violationFail-open with alert

Add an escape hatch for emergencies: allow a designated security lead to bypass specific gates with a documented justification. This prevents security gates from becoming a blocker for critical hotfixes while maintaining accountability. Implement this via a labeled PR override mechanism:

  sca:
    if: ${{ !contains(github.event.pull_request.labels.*.name, 'security-bypass') }}

Require the security-bypass label to be applied by a specific team with reviewer rights, and log all bypass events.

Developer Experience

Security gates only work if developers use them rather than routing around them. Invest in developer experience:

Fast feedback: Gates that take 20 minutes to run will be ignored or bypassed. Optimize CI parallelization so the total PR security check time is under 5 minutes.

Clear, actionable findings: A finding that says "potential SQL injection at src/api/users.ts:142" with a link to the specific code and a suggested fix is useful. A finding that says "SQL injection detected" with no context is noise.

Security champions: Embed security-aware developers in each engineering team. They can answer questions about findings in context, participate in threat modeling, and advocate for the security program within their team.

Metrics and visibility: Show teams their security debt trend over time. A team that sees their critical finding count going from 15 to 3 over a quarter has concrete evidence that the program is working.

The best DevSecOps programs are ones where developers forget they have a security program because the checks are seamlessly integrated into the tools they already use. The goal is not a security wall that developers must scale, but a security guardrail that guides them toward the safe path.

DevSecOps
CI/CD
shift-left
security-gates
SDLC
pipeline-security

Check Your Security Score — Free

See exactly how your domain scores on DMARC, TLS, HTTP headers, and 25+ other automated security checks in under 60 seconds.