Why DevSecOps?
According to the 2024 DORA report, high-performing engineering teams detect security issues 60 times faster when those checks are integrated into the CI/CD pipeline rather than handled as a separate gate at the end of the release cycle. The cost of fixing a vulnerability discovered in production is roughly 100 times higher than fixing the same issue at commit time. DevSecOps is not an additional burden — it is a simultaneous acceleration and risk reduction.
Snyk's 2024 State of Open Source Security report found that third-party dependencies account for over 70% of the attack surface in modern applications, and that the average time to exploit a publicly disclosed vulnerability dropped below 12 days in 2024. Waiting for a quarterly security audit is no longer a viable strategy.
GitHub Actions is the ideal platform for implementing DevSecOps because it is natively integrated with GitHub (where your code lives), offers a rich marketplace of security-focused actions, and allows you to surface security findings directly inside pull requests — the exact moment when a developer can act on them with full context.
Layer 1: Static Application Security Testing (SAST) with CodeQL
SAST analyses source code without executing it, looking for vulnerability patterns: SQL injections, cross-site scripting (XSS), insecure deserialisations, hardcoded credentials, and improper authentication logic. GitHub CodeQL is free for public repositories and included in GitHub Advanced Security for private ones.
# .github/workflows/codeql-analysis.yml
name: CodeQL Security Analysis
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 2 * * 1' # Weekly full scan on Monday at 2am
jobs:
analyze:
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: javascript, python, java
queries: security-extended # Includes OWASP Top 10 queries
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
upload: true # Results appear in GitHub Security tab
CodeQL findings appear as inline comments on the pull request, pointing directly to the vulnerable line of code with a description of the flaw and suggested remediation. Engineers see the issue in context — not in a separate security portal days later.
Layer 2: Dependency Scanning (SCA) with Snyk
Software Composition Analysis (SCA) verifies every direct and transitive dependency against known CVE databases (NVD, OSV, GitHub Advisory). A single vulnerable transitive dependency several levels deep can expose your entire application.
- name: Audit npm dependencies
run: npm audit --audit-level=high
- name: Snyk vulnerability scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high --fail-on=upgradable
- name: OWASP Dependency Check
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'my-app'
format: 'SARIF'
out: 'reports'
- name: Upload dependency scan results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: reports/dependency-check-report.sarif
The --fail-on=upgradable flag on Snyk is intentional: it only fails the pipeline for vulnerabilities where a fixed version is already available. This avoids blocking teams on vulnerabilities with no upstream fix, while ensuring that fixable issues cannot be ignored.
Layer 3: Secret Detection with Gitleaks and TruffleHog
Hardcoded secrets — API keys, database passwords, JWT signing keys, cloud credentials — are one of the most common and highest-impact vulnerability categories. Once a secret reaches a repository (even a private one), you must assume it is compromised. Detection must happen before the commit is pushed.
- name: Scan for secrets with Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: TruffleHog deep scan (verified secrets only)
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
extra_args: --debug --only-verified
TruffleHog's --only-verified flag is critical: it actively calls the detected credential against its provider (AWS, GitHub, Stripe, etc.) to confirm it is a live secret before failing the build. This eliminates false positives from test fixtures and example configurations.
For proactive prevention, configure a .gitleaks.toml allowlist for known false positives (test tokens, example values in documentation), and enforce the Gitleaks pre-commit hook locally using Husky or pre-commit.
Layer 4: Container Scanning with Trivy
If your application runs in Docker containers, the image itself can carry vulnerabilities in its base OS packages, language runtime, or application libraries — independently of your own code. Aqua Security's Trivy performs a comprehensive scan of the entire image filesystem.
- name: Build Docker image
run: docker build -t app:${{ github.sha }} .
- name: Scan container image with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: app:${{ github.sha }}
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
exit-code: 1 # Fail the pipeline on CRITICAL findings
ignore-unfixed: true # Skip CVEs with no available fix
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v3
if: always() # Upload even if the scan step failed
with:
sarif_file: trivy-results.sarif
The exit-code: 1 on CRITICAL severity is a hard gate: no image with a critical, unpatched vulnerability reaches your registry. Setting ignore-unfixed: true prevents blocking on CVEs where the upstream vendor has not released a patch — a situation that otherwise generates unavoidable noise in base images like node:20-slim.
Layer 5: Supply Chain Attestation (SLSA)
Supply chain attacks (SolarWinds, Codecov, 3CX) compromise the build process itself rather than the application code. SLSA (Supply-chain Levels for Software Artifacts) is a framework that establishes verifiable provenance for build artifacts — proof that a specific image was built from a specific commit by a specific pipeline, and that no intermediate step was tampered with.
- name: Build and push image
id: push
uses: docker/build-push-action@v5
with:
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
- name: Attest build provenance (SLSA Level 3)
uses: actions/attest-build-provenance@v1
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
The attestation is stored alongside the image in the registry as a signed OCI artifact. Any deployment system can verify it before running the image: gh attestation verify oci://registry/image@sha256:... --owner your-org.
Blocking Policy: What Stops the Pipeline vs. What Warns
Not every finding should block a merge with equal force. An overly aggressive policy creates alert fatigue and pressure to bypass checks; a permissive policy means security debt accumulates silently. The following policy balances both concerns:
| Severity | Pipeline action | Notification | Resolution |
|---|---|---|---|
| CRITICAL | Hard block — merge prevented | Slack #security + auto GitHub Issue | Must be fixed before merge — no exceptions |
| HIGH | Soft block — merge prevented | Security team notified | Must be fixed or explicitly accepted with justification |
| MEDIUM | Warning only — merge allowed | Visible PR comment | Documented justification required in PR comment |
| LOW | No action | Security Dashboard only | Tracked passively, addressed in backlog |
OIDC-Based Secret Management
The secrets used inside the pipeline (cloud credentials for deployment, registry tokens) must themselves be managed securely. The industry best practice is to eliminate long-lived stored credentials entirely using OpenID Connect (OIDC) federation.
With OIDC, GitHub Actions presents a short-lived signed token to the cloud provider (AWS, Azure, GCP), which exchanges it for temporary credentials scoped to the exact permissions needed for that pipeline run. No credentials are ever stored in GitHub Secrets, no credentials can be stolen from a fork, and no credentials need rotation.
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeployer
aws-region: eu-west-1
# No stored keys — authenticated via short-lived OIDC token
# Token is valid for 1 hour and scoped to this specific workflow run
The IAM role trust policy restricts which GitHub organisation, repository, and branch (or environment) can assume the role — a leaked OIDC token from one repository cannot be used to assume a role configured for another.
Conclusion
A complete DevSecOps pipeline with GitHub Actions — SAST, SCA, secret detection, container scanning, and supply chain attestation — can be implemented in one to two engineering days. The return on investment is immediate: vulnerabilities found at commit time cost a fraction of those found in production, and the automated paper trail satisfies compliance requirements (SOC 2, ISO 27001, PCI-DSS) with zero additional effort.
The cultural shift matters as much as the tooling: security checks embedded in the pull request workflow become part of the normal development loop, not an external audit. Engineers who see security findings in context — on their own code, at the moment they wrote it — fix them. Security integrated into the process is security that is actually respected.
