Practical Linux, Windows Server and cloud guides for IT pros.

A Practical DevSecOps Rollout with GitHub Actions

A tested 30-day DevSecOps rollout for GitHub Actions: secrets, SAST, container and Kubernetes checks, with sensible release gates.

Filed under

Published

Written by

Last updated

DevSecOps feedback loop from code and build checks through deployment

Reviewed and updated: 22 June 2026. Tool versions and documentation were checked against their official projects on that date.

My practical definition of DevSecOps is simple: give developers trustworthy security feedback while they can still act on it.

A credential found before merge is a manageable interruption. The same credential discovered after misuse is an incident.

Pipelines can fail in both directions. Some teams perform almost no automated security testing until a release review. Others add so many noisy scanners that developers learn to ignore the red crosses.

Neither approach creates an effective security programme. One identifies risk too late; the other creates warning fatigue.

The rollout below is designed for a small engineering team using GitHub Actions. It starts with a few checks that are understandable, reasonably fast and assigned to named owners. It does not assume that installing more scanners automatically improves security.

DevSecOps in one table

CheckWhen to run itInitial policyPrimary owner
Secret detectionEvery pull request and pushBlock high-confidence findingsRepository team
Static analysisEvery pull requestStart advisory; later block a small reviewed rulesetRepository team
Dependency scanningPull requests and scheduled scansAdvisory first; block policy-defined high-risk findingsRepository team
Container scanningAfter building the candidate imageBlock relevant, fixable critical findingsApplication and platform teams
SBOM generationFor release artifactsGenerate and retain with the releaseRelease or platform team
Kubernetes manifest scanningBefore deploymentBlock explicit unsafe configurationsPlatform team
DASTAgainst staging or a preview environmentAdvisory until scope and authentication are reliableApplication security and repository team

Scan the thing you will actually ship

A source-tree scan and a release-artifact scan answer different questions.

Static analysis looks for risky patterns in source code. Dependency scanning examines declared or resolved software dependencies. A container scan reports packages and known vulnerabilities found inside an image. An SBOM records the components associated with an artifact. Manifest scanning checks the runtime configuration encoded in Kubernetes resources.

These should not be collapsed into one vaguely named security-scan job.

Each check needs:

  • a clear purpose;
  • an owner;
  • an expected response;
  • a documented exception process;
  • a defined condition for blocking a merge or release.

When a job fails, the developer should be able to determine what failed, why it matters and what action is expected.

Pin the security pipeline itself

Security scanners execute with access to source code, workflow data and, in some jobs, credentials. The scanners therefore form part of the software supply chain.

GitHub recommends referencing actions by full commit SHA because that is the only immutable action reference. This is particularly important after the March 2026 Trivy incident, during which numerous action tags were moved to malicious commits.

The workflow below uses full commit SHAs for GitHub Actions. The comments retain the corresponding release versions so Dependabot can propose reviewed updates. GitHub supports updating actions referenced by SHA when the version comment appears on the same line.

A baseline GitHub Actions workflow

This baseline performs three jobs:

  1. makes complete Git history available to Gitleaks;
  2. runs Semgrep in advisory mode;
  3. builds a candidate image and scans that exact local image with Trivy.
name: devsecops-baseline

on:
  pull_request:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: devsecops-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  secret-scan:
    name: secret-scan
    runs-on: ubuntu-24.04
    timeout-minutes: 10

    steps:
      - name: Check out the repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Scan for committed secrets
        uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0
        env:
          GITHUB_TOKEN: ${{ github.token }}
          GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
          GITLEAKS_ENABLE_COMMENTS: "false"

  sast-advisory:
    name: sast-advisory
    runs-on: ubuntu-24.04
    timeout-minutes: 15

    container:
      image: semgrep/semgrep:1.167.0

    steps:
      - name: Check out the repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Run advisory Semgrep scan
        run: semgrep scan --config auto

  container-scan:
    name: container-scan
    runs-on: ubuntu-24.04
    timeout-minutes: 20

    steps:
      - name: Check out the repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Build the candidate image
        run: docker build --tag app:${{ github.sha }} .

      - name: Record the local image identifier
        run: |
          docker image inspect \
            --format 'Scanned local image ID: {{.Id}}' \
            app:${{ github.sha }}

      - name: Scan the candidate image
        uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
        with:
          image-ref: app:${{ github.sha }}
          format: table
          exit-code: "1"
          ignore-unfixed: true
          severity: CRITICAL
          scanners: vuln

The action SHAs above correspond to the stated releases.

The Semgrep image is pinned to a numbered version for readability. Before adopting the workflow in a controlled production environment, resolve and record the reviewed OCI image digest for the runner architecture rather than relying only on a mutable image tag.

Gitleaks organisation and fork considerations

GITLEAKS_LICENSE is required for organisation-owned repositories using Gitleaks Action. It is not required for a personal repository.

GitHub does not expose ordinary repository secrets to pull requests from forks. Public organisation repositories that accept external contributions therefore need a separate Gitleaks CLI or container scan that does not require a secret. Do not solve this by using pull_request_target to check out and execute untrusted pull-request code.

Make failed checks enforceable

A failed GitHub Actions job does not by itself prevent a merge.

Create a repository ruleset or branch-protection rule for main and require the checks intended to act as gates, for example:

secret-scan
container-scan

Leave sast-advisory visible but not required while the rules are being evaluated.

GitHub prevents a merge only when the configured required status checks have completed successfully. Job names should also remain unique across workflows to avoid ambiguous required checks.

Move Semgrep from advisory to blocking deliberately

The baseline command is advisory:

semgrep scan --config auto

It reports findings but does not fail solely because findings were detected. Scan errors still fail the command.

Use this phase to determine:

  • which languages and frameworks need coverage;
  • which rules identify genuine defects;
  • which generated or vendored directories should be excluded;
  • which findings have a clear remediation path.

Once the team trusts a small ruleset, store it in the repository and use it for the blocking job:

semgrep scan --config .semgrep/blocking.yml --error

The checked-in ruleset should be reviewed like application code. Avoid turning the changing results of --config auto directly into a mandatory gate. Semgrep distinguishes registry-selected automatic rules from explicit policy configurations, and --error is what makes detected findings produce exit code 1.

Understand what the image job proves

The example scans the exact local image built earlier in the same job. That is useful for pull-request feedback, but it is not yet release provenance.

A production release workflow should:

  1. build the image once;
  2. push that image to the registry;
  3. obtain its immutable registry digest;
  4. scan or verify that digest;
  5. generate the SBOM from the same artifact;
  6. deploy by digest;
  7. avoid rebuilding the image between scanning and deployment.

A tag such as:

app:<git-commit>

helps trace an image back to source, but a tag remains a name. The registry digest identifies the image content.

Treat ignore-unfixed as a policy choice

This example begins with:

ignore-unfixed: true
severity: CRITICAL

That can reduce early noise, but it must not be interpreted as “all remaining risk is acceptable.”

Trivy’s ignore-unfixed option also suppresses findings classified as will_not_fix, fix_deferred and end_of_life, in addition to currently affected findings.

Compensating controls should include:

  • detecting end-of-life base images separately;
  • recording suppressed findings;
  • allowing emergency review of severe unfixed vulnerabilities;
  • adding expiry dates and reasons to approved exceptions;
  • periodically reconsidering the initial severity threshold.

A remotely exploitable critical vulnerability without a vendor patch may still justify delaying a release. The scanner supplies evidence; it does not make the risk decision.

What to block first

Begin with findings that have high confidence, meaningful impact and an obvious response.

High-confidence secret findings

Treat a credible committed credential as exposed. The normal response is to:

  1. revoke or disable it;
  2. rotate affected credentials;
  3. inspect relevant audit logs;
  4. remove it from history where appropriate;
  5. correct the process that allowed it to be committed.

Deleting the string from the newest commit is not remediation.

Relevant, fixable critical image vulnerabilities

Confirm that the affected package exists in the runtime image, that the vendor provides a fix and that the finding is applicable to the deployed environment.

Do not use severity alone as a substitute for context.

Explicitly unsafe Kubernetes settings

Good initial blocking controls include unapproved:

  • privileged containers;
  • host namespace access;
  • dangerous Linux capabilities;
  • writable host filesystem mounts;
  • workloads that violate the organisation’s non-root policy.

Prefer explicit rules over a broad requirement such as “every Kubesec score must be positive.”

A reviewed SAST ruleset

Block only rules that the team has evaluated against its own languages and frameworks. Each blocking rule should have a clear explanation, owner and exception process.

A 30-day rollout

Week 1: secret detection and incident response

Add Gitleaks locally and in CI.

Before making the check mandatory, document the response to a credible finding:

  • who revokes the credential;
  • how it is rotated;
  • who reviews audit logs;
  • when history rewriting is appropriate;
  • how affected deployments are identified;
  • how false positives are suppressed and reviewed.

Configure secret-scan as a required status check after testing the workflow against representative repositories.

Week 2: candidate image scanning and artifact identity

Build the candidate image and scan it with Trivy.

Review every critical finding before widening the policy. Record the local image identifier in pull-request jobs. In the release workflow, publish once and retain the registry digest so the scanned artifact can be connected to the deployed artifact.

Generate an SBOM from that same release image rather than rebuilding it separately.

Week 3: tune static and dependency analysis

Run Semgrep in advisory mode.

Identify the rules that consistently find real defects and promote only those rules into a checked-in blocking configuration.

Add dependency analysis separately. Container scanning and source dependency scanning overlap, but they do not inspect exactly the same inputs or lifecycle stages.

Week 4: rendered manifests and staging tests

Scan the Kubernetes resources that will actually be deployed.

For Helm, render the selected values before scanning:

helm template -f values.yaml ./chart | kubesec scan /dev/stdin

Kubesec supports named-rule scans and warns against submitting sensitive manifests to its public service, so sensitive deployment configuration should be scanned locally.

Add a lightweight DAST job against staging or a short-lived preview environment. Keep it advisory until authentication, test data, destructive actions and scan scope have been configured reliably.

DAST does not automatically belong in the critical path of every pull request. Stable smoke scans may fit preview environments; broader authenticated scans may be better suited to scheduled staging runs.

Keep pinned actions current

Commit a Dependabot configuration for GitHub Actions:

version: 2

updates:
  - package-ecosystem: github-actions
    directory: /
    schedule:
      interval: weekly

Dependabot can propose updates for actions referenced by full commit SHA. The version comment beside each SHA keeps the reviewed release visible to maintainers.

Review the proposed release, source changes and commit before merging the update. Pinning prevents an existing workflow reference from moving; it does not establish that every future update is trustworthy.

Container-image dependencies, including the Semgrep image, need a separate digest-update process.

Measure whether the rollout is working

Scanner totals are a poor success metric. A growing number of findings may mean risk is increasing, but it may also mean coverage improved.

More useful measures include:

  • median time to resolve confirmed findings;
  • false-positive rate by rule;
  • repeated classes of defect;
  • findings discovered before merge rather than after release;
  • exceptions past their expiry date;
  • percentage of releases deployed from the scanned digest;
  • percentage of repositories using required checks;
  • time taken to revoke a leaked credential;
  • scanner failures that prevent results from being produced.

The important question is not:

How many security tools are installed?

It is:

Did the engineer receive a trustworthy signal early enough to change the outcome?

Frequently asked questions

Is DevSecOps just DevOps plus scanners?

No. Scanners provide evidence. DevSecOps also requires ownership, response procedures, secure defaults, enforceable policy, exception management and feedback that developers trust.

Should every finding fail a build?

No. A finding should block only when it is sufficiently reliable, significant and actionable under the organisation’s policy.

Other findings should remain visible while rules are tuned and a baseline is established.

Does a red GitHub Actions job prevent merging?

Not necessarily. The job must be configured as a required status check through a repository ruleset or branch-protection rule.

Is a Gitleaks finding a verified live credential?

No. It is a secret detection. Validate the context, but treat a credible committed credential as exposed until it has been revoked or shown to be harmless.

Do I need DAST on every pull request?

Not always. DAST requires a running target, predictable test data, controlled scope and often authentication. Staging or a preview environment is normally the appropriate target; the right frequency depends on scan duration and reliability.

Does scanning an image tag prove that the deployed image was scanned?

No. To establish that connection, publish the image once, record its registry digest and deploy that same digest without rebuilding it.

Leave a Reply

Your email address will not be published. Required fields are marked *

Find more on the site

Keep reading by topic.

If this post was useful, the fastest way to keep going is to pick the topic you work in most often.

Want another useful post?

Browse the latest posts, or support TurboGeek if the site saves you time regularly.