Diagram of Azure DevSecOps pipeline stages including Plan & Develop, Build & Test, Deploy, and Operate with security gate and feedback loop

How to Build a DevSecOps CI/CD Pipeline on Azure With GitHub Actions

A DevSecOps pipeline like this is less about adding tools and more about changing control points. Once security checks become first-class jobs in GitHub Actions, the team has to treat failed scans as release blockers, not advisory output. That shifts responsibility from a manual review habit to an enforced delivery policy, which is only effective if the rules are consistent across pull requests, merges and production-bound releases.

The architecture also depends on job ordering and isolation. Running SAST, secrets detection and dependency auditing before the build reduces wasted compute and shortens feedback loops, but it also means the pipeline must be fast enough that developers do not bypass it in practice. For Node.js workloads, shallow clones, package-manager behavior and transitive dependencies all influence what gets inspected and when findings appear.

On Azure, deployment slots and GitHub Environments introduce a useful separation between verification and promotion. That separation matters operationally: staging can absorb smoke testing, while production requires an explicit approval gate and a controlled slot swap. The trade-off is added workflow complexity, but it is usually cheaper than recovering from a bad release or a leaked secret.

The strongest long-term value is in traceability. Linking workflow history with Azure Monitor creates an audit trail from commit to deployment, which helps incident response, change management and postmortems. The main maintenance burden is keeping the policy aligned with the codebase: tune severity thresholds, reduce false positives, rotate exposed secrets immediately and keep the pipeline lightweight enough that teams will actually use it.




 

 

Three years ago, two days before a production push to Azure, I was working through a pull request on a Node.js service. Routine stuff. Then I spotted something in a commit that had been merged two weeks earlier. Already past review, already on the main branch. A Stripe secret key. Not obfuscated, not in a config file. Plain text, right there in the source. Build was green. Tests passed. Nobody had caught it.

That incident changed how I think about CI/CD. The problem wasnโ€™t that my team was careless. The problem was that our pipeline had no opinion about security. It checked code quality. It ran tests. It deployed. But it never asked whether what we were shipping was safe.

DevSecOps is the answer to that question, but the term gets thrown around loosely. At its core, it just means one thing: Security checks run automatically, in the pipeline, on every change, the same way unit tests do. If a security check fails, the build fails. No exceptions, no โ€œweโ€™ll fix it in the next sprint.โ€

This article builds exactly that kind of pipeline on Azure using GitHub Actions: Static analysis, secrets detection, dependency auditing, deployment gates and audit logging โ€” all wired into one workflow that runs on every PR and push to main. The YAML here is real; swap tool names to match your stack and it deploys.

The Pipeline at a Glance

Hereโ€™s the full sequence before we dive into each piece:

  • Trigger: PR opened or push to main
  • Job 1: SAST scan (CodeQL) โ€” static analysis for code vulnerabilities
  • Job 2: Secrets scan (Gitleaks) โ€” catch leaked credentials before they land in the repo
  • Job 3: Dependency check โ€” npm audit or Snyk scanning third-party packages for CVEs
  • Job 4: Build and test โ€” standard compilation, unit tests, integration tests
  • Job 5: Deploy to staging slot โ€” push to Azure App Service staging, run smoke tests
  • Job 6: Production gate โ€” required reviewer must approve before anything touches prod
  • Job 7: Slot swap and audit log โ€” promote staging to production, write a deployment record to Azure Monitor

Running the first three jobs in parallel, before the build starts, isnโ€™t arbitrary. Waiting on a slow build only to hit a security failure at the end is the worst possible timing. Usually, it hits when a release window is closing and the pressure to just push it anyway is at its highest. Security failures caught early get fixed. Security failures caught late get deferred.

SAST With CodeQL

SAST works on source code before anything is executed. No running server, no network call. It scans for coding patterns behind vulnerabilities such as SQL injection, XSS and insecure deserialization. These are genuinely hard to catch in a code review, especially when the team is under release pressure. GitHubโ€™s CodeQL does this well, and itโ€™s free for public repos.

Create .github/workflows/sast.yml:

name: SAST โ€” CodeQL

on:

pull_request:

branches: [main]

push:

branches: [main]

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

- name: Autobuild

uses: github/codeql-action/autobuild@v3

- name: Analyze

uses: github/codeql-action/analyze@v3

with:

category: '/language:javascript'

fail-on-severity: high

Results land in the Security tab under Code Scanning Alerts. The fail-on-severity: high setting means the pipeline blocks on critical and high findings but surfaces medium and low as warnings you can triage later. Thatโ€™s a pragmatic starting point. You can tighten it over time.

One thing I found: CodeQLโ€™s JavaScript queries catch a surprising number of prototype pollution bugs and unsafe eval() patterns that conventional linting misses. Itโ€™s not a replacement for a manual security review, but it catches the mechanical stuff reliably.

Secrets Detection With Gitleaks

Hereโ€™s a number that should worry you: According to GitGuardianโ€™s 2024 report, a secret is exposed on GitHub every five seconds โ€” API keys, database URLs, JWT signing secrets, OAuth tokens. Developers commit them accidentally more than youโ€™d think, often in a โ€˜quick fixโ€™ commit made under pressure.

Gitleaks scans your commits for credential patterns before they reach the remote. Add it as a required check on every PR:

name: Secrets Scan โ€” Gitleaks

on:

pull_request:

branches: [main]

push:

branches: [main]

jobs:

gitleaks:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

with:

fetch-depth: 0

- name: Run Gitleaks

uses: gitleaks/gitleaks-action@v2

env:

GITHUB_TOKEN: $

The fetch-depth: 0 is important. By default, actions/checkout does a shallow clone. Gitleaks needs the full commit history to scan past commits. A secret from six weeks ago in an old branch is still a secret.

Add a .gitleaks.toml at the repo root to handle false positives. Test fixtures that deliberately contain fake credentials are a common source of noise:

[allowlist]

paths = [

'''^test/fixtures/'''

'''^docs/examples/'''

]

One hard-learned lesson: If Gitleaks flags a real secret, deleting the commit isnโ€™t enough. Rotate the credential immediately and assume it was already scraped. Bots index GitHub continuously, and thereโ€™s no way to know how long the exposure window was.

Dependency Auditing

Open your package-lock.json and scroll for a moment. A typical Node.js project carries several hundred packages, most of which you never explicitly installed. Every package you depend on is a potential attack surface, and new CVEs are disclosed every week. Without automated auditing, you wonโ€™t know about them until someone finds them in your app.

npm audit covers the basics. Snyk adds richer CVE data and tracks transitive vulnerabilities more thoroughly. Hereโ€™s a job that runs both:

name: Dependency Audit

on:

pull_request:

branches: [main]

push:

branches: [main]

schedule:

- cron: '0 6 * * 1' # weekly Monday scan

jobs:

audit:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- uses: actions/setup-node@v4

with:

node-version: '20'

- run: npm ci

- name: npm audit

run: npm audit --audit-level=high

- name: Snyk scan

uses: snyk/actions/node@master

env:

SNYK_TOKEN: $

with:

args: --severity-threshold=high

The schedule trigger is the part most teams skip. It catches CVEs disclosed after your last deployment. A package you installed three months ago might have a critical vulnerability published yesterday. The weekly scan makes sure you donโ€™t find out about it from a penetration tester.

Azure Deployment Gates

Code that passes every automated check still needs a human decision before it goes to production. Not for every deployment, that would kill velocity, but for production-bound changes, a required reviewer is your last line of defense.

GitHub Environments handle this cleanly. Go to your repo Settings > Environments > New environment, name it โ€˜productionโ€™ and add required reviewers. Any job targeting that environment pauses until someone on the list approves.

Pair that with Azure App Service deployment slots and you get a safe promotion workflow:

jobs:

deploy-staging:

runs-on: ubuntu-latest

steps:

- uses: azure/login@v1

with:

creds: $

- name: Deploy to staging slot

uses: azure/webapps-deploy@v3

with:

app-name: my-app

slot-name: staging

package: ./dist

- name: Smoke tests

run: npm run test:smoke -- --url Original Post>

promote-to-production:

needs: deploy-staging

runs-on: ubuntu-latest

environment: production # triggers reviewer gate

steps:

- uses: azure/login@v1

with:

creds: $

- name: Swap staging to production

uses: azure/cli@v1

with:

inlineScript: |

az webapp deployment slot swap \

--name my-app \

--resource-group my-rg \

--slot staging \

--target-slot production

The slot swap is near-instant and zero-downtime. If something breaks in production after the swap, you can reverse it in the Azure portal in about 30 seconds โ€” no redeploy, no rollback drama.

Audit Logging

After an incident, two questions come up immediately: What changed and who did it? GitHub Actions answers this for free through its workflow run history. Centralizing that data in Azure Monitor gives you a single place to query across your entire infrastructure โ€” not just GitHub.

Add this to your deploy job:

- name: Write deployment record

uses: azure/cli@v1

with:

inlineScript: |

az monitor activity-log alert create \

--name "deploy-$" \

--resource-group my-rg \

--description "Actor: $ | SHA: $ | Ref: $"

This creates a timestamped record in Azure Monitor for every production deployment: Who triggered it, from which commit and which branch. Pair it with Application Insights for runtime telemetry and you have end-to-end traceability โ€” from code commit to live behavior.

Mistakes I See Teams Make

A few patterns that consistently undermine DevSecOps pipelines, based on implementations Iโ€™ve been part of or reviewed:

  • Running Security Jobs After the Build: If your SAST scan comes last, engineers learn to ignore it. When itโ€™s slow โ€” and it sometimes is โ€” it becomes the step that gets skipped under deadline pressure. Move security to the front.
  • Storing Credentials in GitHub Variables Instead of Secrets: Environment variables show up in run logs. GitHub Actions encrypted secrets donโ€™t. This is not a subtle distinction.
  • Treating Low-Severity Findings as Permanent To-Dos: Low-severity findings pile up fast and teams mute them eventually. A vulnerability sitting in the backlog for eight months tends to show up during a live incident. 30 minutes a week to clear the queue stops it getting there.
  • Single-Environment Deployments: If staging and production use the same service principle or the same deployment workflow without gates, a misconfigured commit goes straight to users. Separate credentials, separate environments, separate gates.
  • Shallow Git Clones in Secrets Scanning: The default actions/checkout behavior is fetch-depth: 1. Gitleaks scanning only the latest commit will miss anything committed earlier. Always use fetch-depth: 0 for secrets scans.

Where to Go From Here

The pipeline described here covers the fundamentals. Done consistently, they prevent most incidents. No SIEM budget required, no AppSec hire. What actually changes the outcome is automated gates that run on every change and stop the build when something fails.

Start with CodeQL and Gitleaks. Theyโ€™re free, they integrate cleanly with GitHub Actions and theyโ€™ll catch real problems within the first week. Add Snyk once you have a handle on your dependency surface. Deployment gates and audit logging can come in parallel โ€” neither requires significant setup on Azure.

The shift-left principle isnโ€™t complicated: Fix security problems when theyโ€™re cheap to fix, which is before the code is deployed. A pipeline that enforces this automatically is what makes that principle real.

How to Build a DevSecOps CI/CD Pipeline on Azure With GitHub Actions

Enjoyed this article? Sign up for our newsletter to receive regular insights and stay connected.