DEV Community

Cover image for Shift Left Security Practices Developers Like
Fatih Koç
Fatih Koç

Posted on • Originally published at fatihkoc.net

Shift Left Security Practices Developers Like

Security is often treated as a late stage gate. In a cloud native world, that's a tax on velocity. Shift Left Security flips the script. We integrate security earlier. During design, coding, and CI—so developers get fast, actionable feedback without leaving their flow.

In this guide, I'll share developer friendly practices I've used across teams, plus ready to copy code examples you can paste into repos today. I'll also call out common traps and how to avoid "security theater."

A quick story

On a microservices project, a customer had layered on too many security tools. Builds slowed, false positives spiked while observability lagged. We shifted feedback to the IDE and pre commit, moved deep scans to nightly, and added auto fix hints. Two sprints later: faster merges, fewer vulnerabilities reaching staging, and a happier team. Developer experience and timing beat raw coverage.

What developer friendly means

  • Fast feedback: seconds, not minutes, for inner loop checks.
  • Low noise: start with high signal rules; phase in stricter ones.
  • In flow: IDE, pre-commit, PR checks—no context switching.
  • Transparent: policies as code; exceptions time bound and auditable.
  • Learning oriented: every failure teaches the fix.

For broader context, see OWASP ASVS and NIST SSDF.

Also see the OWASP DevSecOps Guidelines for practical ways to align velocity with safety.

Shift Left Security in practice (with code)

Below are plug and play snippets that respect the inner loop. Start small, pick two and expand.

1) Pre‑commit essentials: secrets + basic SAST

We’ve all seen the “oops, someone committed a token” alert. By the time PR or nightly scans catch it, it’s already in history. Pre‑commit hooks are fast, local, and stop the embarrassing stuff early. Keep them under a few seconds in duration and gate only on high‑confidence issues so developers don't disable them.

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.24.2
    hooks:
      - id: gitleaks
        args: ["detect", "--redact", "--no-banner"]
  - repo: https://github.com/semgrep/pre-commit
    rev: 'v1.136.0'
    hooks:
      - id: semgrep
        entry: semgrep
        args: ["--config", "p/ci", "--quiet"]  # start with high-signal rules
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: check-yaml
      - id: end-of-file-fixer
      - id: trailing-whitespace
Enter fullscreen mode Exit fullscreen mode

Tip: Gate only on critical or high-confidence findings at commit time. Expand to medium/low in PR or nightly.

Optional: tighten Gitleaks with custom allow lists.

# .gitleaks.toml (example)
[allowlist]
  description = "allow test tokens"
  regexes = ["GH_TEST_[0-9A-F]{20}"]
Enter fullscreen mode Exit fullscreen mode

2) Fast PR checks + deeper nightly scans

Pull requests should answer one question: Is this safe to merge right now? That’s it. Fast jobs for secrets, lightweight SAST, and basic IaC checks keep PRs flowing. Then at night, when no one’s waiting on feedback, run deep scans. Full rule sets, dependency scans, and container/IaC checks. That way, developers aren’t stuck waiting 15 minutes just to land a comment fix.

# .github/workflows/secure-pr.yml
name: secure-pr
on:
  pull_request:
    branches: [ main ]
jobs:
  fast-guardrails:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    steps:
      - uses: actions/checkout@v4
      - name: Secrets scan (Gitleaks)
        uses: gitleaks/gitleaks-action@v2
      - name: Install Semgrep
        run: pip install --upgrade semgrep
      - name: SAST (Semgrep high-signal)
        run: semgrep --config p/ci --sarif --output semgrep.sarif --quiet --metrics=off
        continue-on-error: true
      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: semgrep.sarif
Enter fullscreen mode Exit fullscreen mode

Nightly: Run deeper SAST, SCA, container/IaC scans.

# .github/workflows/nightly-security.yml
name: nightly-security
on:
  schedule:
    - cron: "0 0 * * *"
jobs:
  deep-scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    steps:
      - uses: actions/checkout@v4
      - name: Install Semgrep
        run: pip install --upgrade semgrep
      - name: SAST (Semgrep full)
        run: |
          semgrep \
            --config p/r2c-security-audit \
            --config p/secrets \
            --config p/docker \
            --sarif --output semgrep.sarif --quiet --metrics=off
      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: semgrep.sarif
      - name: SCA (npm/yarn example)
        run: |
          if [ -f package-lock.json ]; then npm audit --audit-level=high; fi
          if [ -f yarn.lock ] && command -v yarn >/dev/null; then \
            YVER=$(yarn -v | cut -d. -f1); \
            if [ "$YVER" -ge 2 ]; then yarn npm audit --audit-level=high; else yarn audit --level high; fi; \
          fi
      - name: Container & IaC scan (Trivy)
        uses: aquasecurity/trivy-action@0.28.0
        with:
          scan-type: "fs"
          format: "table"
          severity: "CRITICAL,HIGH"
Enter fullscreen mode Exit fullscreen mode

3) Policy as Code with OPA: block risky images in CI

Unwritten security rules only surface during review. OPA turns them into testable, versioned policy. A small Rego rule like “only signed images from our registry” makes the decision explicit and produces clear pass/fail reasons.

# policy/image.rego
package ci.image

# Reasons to deny; empty -> allowed
deny[msg] {
  not startswith(input.image, "registry.example.com/")
  msg := "image must come from registry.example.com"
}

deny[msg] {
  not input.signature_verified
  msg := "image signature not verified"
}

allow {
  count(deny) == 0
}
Enter fullscreen mode Exit fullscreen mode

Wire it into a small CI step:

# scripts/opa_check.sh
set -euo pipefail
image="${1:?image required}"
sig_ok="${2:?signature_verified required}"  # true/false

# Create JSON input for OPA and evaluate policy
violations="$(jq -n --arg i "$image" --argjson s "$sig_ok" \
  '{image: $i, signature_verified: $s}' | \
  opa eval -i - -d policy/ -f json 'data.ci.image.deny' | \
  jq -r '.result[0].expressions[0].value[]?')"

if [ -n "$violations" ]; then
  echo "Policy violations:"
  echo "$violations" | sed 's/^/ - /'
  exit 1
fi

echo "Policy passed"
Enter fullscreen mode Exit fullscreen mode

Usage:

./scripts/opa_check.sh registry.example.com/app@sha256:... true
Enter fullscreen mode Exit fullscreen mode

If any violations are returned, print them and fail the job. For more on secure CI pipelines, see the CNCF blog on OPA best practices.

4) Kubernetes: Pod Security Standards via labels (quick win)

Kubernetes defaults allow risky capabilities like privileged pods, host mounts, running as root. Most apps don't need them. Namespace level Pod Security Admission labels that enforce the Pod Security Standards are the fastest way to shut off bad defaults. Label the namespace and whole classes of risk disappear. Some workloads will need exceptions, but those become explicit decisions.

# namespaces/restricted.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: prod
  labels:
    pod-security.kubernetes.io/enforce: "restricted"
    pod-security.kubernetes.io/audit:   "restricted"
    pod-security.kubernetes.io/warn:    "baseline"
Enter fullscreen mode Exit fullscreen mode

Lock down common Pod risks with a default template.

5) Safer Dockerfile (small changes, big impact)

Many Dockerfiles run as root and include unnecessary packages. Prefer a distroless runtime and a non‑root user to ship a smaller, safer image. You’ll cut CVEs and attack surface, reduce registry storage and network transfer, and speed image pulls. Build times also drop when you prune dev dependencies, shrink the build context, and leverage layer caching. Distroless itself doesn’t make builds faster. Debugging is harder without a shell, so keep a separate -debug image for staging.

  • Distroless runtime → smaller image, fewer CVEs, faster pulls, lower registry storage.
  • USER non‑root → safer by default.
  • Multi‑stage build + prune dev deps → smaller runtime and better cache reuse (faster builds).
  • Note: native modules build faster on node:22-slim than Alpine; still use distroless for runtime. BuildKit cache mounts speed npm/yarn installs.
# Dockerfile
# Build stage
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev

# Runtime (distroless)
FROM gcr.io/distroless/nodejs22-debian12
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package*.json ./
USER nonroot:nonroot
ENV NODE_ENV=production
CMD ["dist/server.js"]
Enter fullscreen mode Exit fullscreen mode

6) Threat modeling as code (lightweight)

Threat models drift when they live outside the repo. Keep a small YAML file next to the code so it evolves with each change. When an API or trust boundary changes, update the model in the same PR. It won’t cover everything, but it keeps risks visible and makes design decisions explicit.

# docs/threat-model.yaml
service: payments-api
version: 1
context: public-api
assets:
  - id: A001
    name: card-data
    classification: sensitive
trust_boundaries:
  - from: api-gateway
    to: payments-api
  - from: payments-api
    to: bank-gateway
threats:
  - id: T001
    title: SQL injection on /charge
    category: STRIDE.Tampering
    risk: High
    status: Mitigated
    mitigations: [ parameterized-queries, input-validation, waf-rule-123 ]
  - id: T002
    title: Secrets leakage via logs
    category: STRIDE.InformationDisclosure
    risk: Medium
    status: Open
    mitigations: [ structured-logging, log-scrubbers, disable-debug-in-prod ]
owners:
  - role: security-champion
    name: alice
  - role: tech-lead
    name: bob
Enter fullscreen mode Exit fullscreen mode

Render it in CI (to HTML/diagram) for visibility and require a short rationale when accepting risk.

7) Minimum viable SBOM + signature

You can’t patch what you can’t find, and you can’t trust what you can’t verify. An SBOM (via Syft or similar) inventories what’s in your image, and a Cosign signature + SBOM attestation proves who built it and with what. When “Are we affected by CVE‑XXXX?” arrives, this turns hours into minutes.

# sbom+sign.sh
set -euo pipefail
IMAGE="registry.example.com/app:${GITHUB_SHA}"

# Build image
docker build -t "$IMAGE" .
docker push "$IMAGE"

# SBOM (SPDX via Syft)
syft packages "$IMAGE" -o spdx-json > sbom.spdx.json

# Sign image + attest SBOM (Cosign)
cosign sign "$IMAGE"
cosign attest --predicate sbom.spdx.json --type spdx "$IMAGE"

# Verify signature and attestation (adjust identity/issuer for your CI)
cosign verify "$IMAGE" \
  --certificate-identity "${GITHUB_REPOSITORY:-}" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" >/dev/null

cosign verify-attestation --type spdx "$IMAGE" >/dev/null
echo "Signature and SBOM attestation verified"
Enter fullscreen mode Exit fullscreen mode

Then extend your OPA policy to require a valid attestation.

8) IaC guardrails: Terraform checks in PRs

Cloud misconfigurations are the sneakiest bugs. They look harmless in code review, then suddenly you’ve got a public S3 bucket in prod. Running tfsec on the Terraform plan catches those before apply. It’s cheap insurance, and it makes reviewers more confident: “yep, this plan doesn’t open the blast doors.” Sure, you’ll have to tune a few noisy rules, but the net is positive.

# .github/workflows/tf-guardrails.yml
name: tf-guardrails
on: [ pull_request ]
jobs:
  tfsec:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - name: Validate & plan
        run: |
          terraform init -input=false
          terraform validate -no-color
          terraform plan -out plan.out -no-color
      - name: tfsec (critical only)
        uses: aquasecurity/tfsec-action@v1.0.11
        with:
          tfsec_args: "--severity CRITICAL --format sarif --out tfsec.sarif"
      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: tfsec.sarif
Enter fullscreen mode Exit fullscreen mode

Common pitfalls (and fixes)

  • False positive fatigue → start with high confidence rules; add suppressions with context.
  • Slow pipelines → parallelize; cache dependencies; schedule deep scans nightly.
  • Opaque decisions → keep policies as code; require rationale on exceptions.
  • “Security says no” culture → create security champions within dev teams.
  • Late requirements → add threat modeling to planning; codify standards in templates.

Tools & when to use them

Problem Fast inner loop PR/CI guardrail Scheduled/deep
Secrets leakage pre-commit + Gitleaks Gitleaks Action Org/repo-wide secret scanning
Code vulns Semgrep targeted rules Semgrep CI (SARIF upload) Semgrep full rulesets + CodeQL
Dependencies npm/pnpm audit; pip-audit Audit in CI (fail-on=high) Renovate/Dependabot + license allowlists
Containers Trivy (fs) Trivy (image) in CI Trivy + Cosign/Sigstore attestations
IaC (Terraform) tfsec or Checkov locally tfsec/Checkov in CI Conftest/OPA (Rego) against terraform plan

FAQ

What’s the difference between Shift Left Security and DevSecOps?
Shift Left is the practice (earlier checks), DevSecOps is the culture/process shift enabling it.

Does Shift Left Security slow developers down?
Only if you push heavy checks into the inner loop. Keep fast checks local/PR; move heavy ones to nightly. Most teams recoup time via fewer hotfixes and less rework.

Do developers need to be security experts?
No. They need sharp guardrails and actionable feedback. Security champions and short, focused trainings beat long policy docs.

How do we handle false positives?
Tune rules with suppressions and allowlists in-repo; require justification in PRs; review exceptions monthly and prune stale ones.

What if a tool blocks a release?
Use severity thresholds (e.g., fail on high/critical). Allow time bound waivers with an owner and due date; track them in issues and audit regularly.

Conclusion

Shift Left Security succeeds when it respects developer time. Keep fast checks in the inner loop, move heavy analysis to nightly, and encode policy so decisions are visible and auditable. Favor modular, open source pieces so any tool can be swapped without lock in; upgrade to enterprise where it clearly pays off.

Enterprise options to evaluate: Prisma Cloud, SonarQube/SonarCloud, Snyk, Wiz, Aqua, Lacework, GitHub Advanced Security/GitLab Ultimate.

Curious how to apply this to your platform? Ping me via the Contact page—I'm happy to tailor a developer friendly rollout for your stack.

This article originally published at fatihkoc.net.

Top comments (0)