{"id":4549,"date":"2026-07-09T12:12:26","date_gmt":"2026-07-09T12:12:26","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/07\/09\/how-to-build-a-devsecops-ci-cd-pipeline-on-azure-with-github-actions\/"},"modified":"2026-07-09T12:12:26","modified_gmt":"2026-07-09T12:12:26","slug":"how-to-build-a-devsecops-ci-cd-pipeline-on-azure-with-github-actions","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/07\/09\/how-to-build-a-devsecops-ci-cd-pipeline-on-azure-with-github-actions\/","title":{"rendered":"How to Build a DevSecOps CI\/CD Pipeline on Azure With GitHub Actions"},"content":{"rendered":"<div><img data-opt-id=798090309  fetchpriority=\"high\" decoding=\"async\" width=\"770\" height=\"330\" src=\"https:\/\/devops.com\/wp-content\/uploads\/2021\/01\/Future-of-DevOps-and-CI_CD.jpg\" class=\"attachment-large size-large wp-post-image\" alt=\"CI\/CD, gearset, future low-code CI\/CD release metrics CircleCI Future of DevOps and CI\/CD - Predict 2021\" \/><\/div>\n<p><img data-opt-id=624514377  fetchpriority=\"high\" decoding=\"async\" width=\"150\" height=\"150\" src=\"https:\/\/devops.com\/wp-content\/uploads\/2021\/01\/Future-of-DevOps-and-CI_CD-150x150.jpg\" class=\"attachment-thumbnail size-thumbnail wp-post-image\" alt=\"CI\/CD, gearset, future low-code CI\/CD release metrics CircleCI Future of DevOps and CI\/CD - Predict 2021\" \/><\/p>\n<p>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.<\/p>\n<p>That incident changed how I think about CI\/CD. The problem wasn\u2019t 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.<\/p>\n<p><a href=\"https:\/\/devops.com\/how-ai-is-revamping-devsecops-processes\/\" target=\"_blank\" rel=\"noopener\">DevSecOps is the answer to that question<\/a>, 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 \u201cwe\u2019ll fix it in the next sprint.\u201d<\/p>\n<p>This article builds exactly that kind of pipeline on Azure using GitHub Actions: Static analysis, secrets detection, dependency auditing, deployment gates and audit logging \u2014 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.<\/p>\n<h3>The Pipeline at a Glance<\/h3>\n<p>Here\u2019s the full sequence before we dive into each piece:<\/p>\n<ul>\n<li>Trigger: PR opened or push to main<\/li>\n<li>Job 1: SAST scan (CodeQL) \u2014 static analysis for code vulnerabilities<\/li>\n<li>Job 2: Secrets scan (Gitleaks) \u2014 catch leaked credentials before they land in the repo<\/li>\n<li>Job 3: Dependency check \u2014 npm audit or Snyk scanning third-party packages for CVEs<\/li>\n<li>Job 4: Build and test \u2014 standard compilation, unit tests, integration tests<\/li>\n<li>Job 5: Deploy to staging slot \u2014 push to Azure App Service staging, run smoke tests<\/li>\n<li>Job 6: Production gate \u2014 required reviewer must approve before anything touches prod<\/li>\n<li>Job 7: Slot swap and audit log \u2014 promote staging to production, write a deployment record to Azure Monitor<\/li>\n<\/ul>\n<p>Running the first three jobs in parallel, before the build starts, isn\u2019t 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.<\/p>\n<h3>SAST With CodeQL<\/h3>\n<p>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\u2019s CodeQL does this well, and it\u2019s free for public repos.<\/p>\n<p>Create .github\/workflows\/sast.yml:<\/p>\n<p><code>name: SAST \u2014 CodeQL<\/code><\/p>\n<p><code>on:<\/code><\/p>\n<p><code>  pull_request:<\/code><\/p>\n<p><code>    branches: [main]<\/code><\/p>\n<p><code>  push:<\/code><\/p>\n<p><code>    branches: [main]<\/code><\/p>\n<p><code>jobs:<\/code><\/p>\n<p><code>  analyze:<\/code><\/p>\n<p><code>    runs-on: ubuntu-latest<\/code><\/p>\n<p><code>    permissions:<\/code><\/p>\n<p><code>      security-events: write<\/code><\/p>\n<p><code>      contents: read<\/code><\/p>\n<p><code>    steps:<\/code><\/p>\n<p><code>      - uses: actions\/checkout@v4<\/code><\/p>\n<p><code>      - name: Initialize CodeQL<\/code><\/p>\n<p><code>        uses: github\/codeql-action\/init@v3<\/code><\/p>\n<p><code>        with:<\/code><\/p>\n<p><code>          languages: javascript<\/code><\/p>\n<p><code>      - name: Autobuild<\/code><\/p>\n<p><code>        uses: github\/codeql-action\/autobuild@v3<\/code><\/p>\n<p><code>      - name: Analyze<\/code><\/p>\n<p><code>        uses: github\/codeql-action\/analyze@v3<\/code><\/p>\n<p><code>        with:<\/code><\/p>\n<p><code>          category: '\/language:javascript'<\/code><\/p>\n<p><code>          fail-on-severity: high<\/code><\/p>\n<p>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\u2019s a pragmatic starting point. You can tighten it over time.<\/p>\n<p>One thing I found: CodeQL\u2019s JavaScript queries catch a surprising number of prototype pollution bugs and unsafe eval() patterns that conventional linting misses. It\u2019s not a replacement for a manual security review, but it catches the mechanical stuff reliably.<\/p>\n<h3>Secrets Detection With Gitleaks<\/h3>\n<p>Here\u2019s a number that should worry you: According to GitGuardian\u2019s 2024 report, a secret is exposed on GitHub every five seconds \u2014 API keys, database URLs, JWT signing secrets, OAuth tokens. Developers commit them accidentally more than you\u2019d think, often in a \u2018quick fix\u2019 commit made under pressure.<\/p>\n<p>Gitleaks scans your commits for credential patterns before they reach the remote. Add it as a required check on every PR:<\/p>\n<p><code>name: Secrets Scan \u2014 Gitleaks<\/code><\/p>\n<p><code>on:<\/code><\/p>\n<p><code>  pull_request:<\/code><\/p>\n<p><code>    branches: [main]<\/code><\/p>\n<p><code>  push:<\/code><\/p>\n<p><code>    branches: [main]<\/code><\/p>\n<p><code>jobs:<\/code><\/p>\n<p><code>  gitleaks:<\/code><\/p>\n<p><code>    runs-on: ubuntu-latest<\/code><\/p>\n<p><code>    steps:<\/code><\/p>\n<p><code>      - uses: actions\/checkout@v4<\/code><\/p>\n<p><code>        with:<\/code><\/p>\n<p><code>          fetch-depth: 0<\/code><\/p>\n<p><code>      - name: Run Gitleaks<\/code><\/p>\n<p><code>        uses: gitleaks\/gitleaks-action@v2<\/code><\/p>\n<p><code>        env:<\/code><\/p>\n<p><code>          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}<\/code><\/p>\n<p>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.<\/p>\n<p>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:<\/p>\n<p><code>[allowlist]<\/code><\/p>\n<p><code>  paths = [<\/code><\/p>\n<p><code>    '''^test\/fixtures\/'''<\/code><\/p>\n<p><code>    '''^docs\/examples\/'''<\/code><\/p>\n<p><code>  ]<\/code><\/p>\n<p>One hard-learned lesson: If Gitleaks flags a real secret, deleting the commit isn\u2019t enough. Rotate the credential immediately and assume it was already scraped. Bots index GitHub continuously, and there\u2019s no way to know how long the exposure window was.<\/p>\n<h3>Dependency Auditing<\/h3>\n<p>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\u2019t know about them until someone finds them in your app.<\/p>\n<p>npm audit covers the basics. Snyk adds richer CVE data and tracks transitive vulnerabilities more thoroughly. Here\u2019s a job that runs both:<\/p>\n<p><code>name: Dependency Audit<\/code><\/p>\n<p><code>on:<\/code><\/p>\n<p><code>  pull_request:<\/code><\/p>\n<p><code>    branches: [main]<\/code><\/p>\n<p><code>  push:<\/code><\/p>\n<p><code>    branches: [main]<\/code><\/p>\n<p><code>  schedule:<\/code><\/p>\n<p><code>    - cron: '0 6 * * 1'   # weekly Monday scan<\/code><\/p>\n<p><code>jobs:<\/code><\/p>\n<p><code>  audit:<\/code><\/p>\n<p><code>    runs-on: ubuntu-latest<\/code><\/p>\n<p><code>    steps:<\/code><\/p>\n<p><code>      - uses: actions\/checkout@v4<\/code><\/p>\n<p><code>      - uses: actions\/setup-node@v4<\/code><\/p>\n<p><code>        with:<\/code><\/p>\n<p><code>          node-version: '20'<\/code><\/p>\n<p><code>      - run: npm ci<\/code><\/p>\n<p><code>      - name: npm audit<\/code><\/p>\n<p><code>        run: npm audit --audit-level=high<\/code><\/p>\n<p><code>      - name: Snyk scan<\/code><\/p>\n<p><code>        uses: snyk\/actions\/node@master<\/code><\/p>\n<p><code>        env:<\/code><\/p>\n<p><code>          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}<\/code><\/p>\n<p><code>        with:<\/code><\/p>\n<p><code>          args: --severity-threshold=high<\/code><\/p>\n<p>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\u2019t find out about it from a penetration tester.<\/p>\n<h3>Azure Deployment Gates<\/h3>\n<p>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.<\/p>\n<p>GitHub Environments handle this cleanly. Go to your repo Settings &gt; Environments &gt; New environment, name it \u2018production\u2019 and add required reviewers. Any job targeting that environment pauses until someone on the list approves.<\/p>\n<p>Pair that with Azure App Service deployment slots and you get a safe promotion workflow:<\/p>\n<p><code>jobs:<\/code><\/p>\n<p><code>  deploy-staging:<\/code><\/p>\n<p><code>    runs-on: ubuntu-latest<\/code><\/p>\n<p><code>    steps:<\/code><\/p>\n<p><code>      - uses: azure\/login@v1<\/code><\/p>\n<p><code>        with:<\/code><\/p>\n<p><code>          creds: ${{ secrets.AZURE_CREDENTIALS }}<\/code><\/p>\n<p><code>      - name: Deploy to staging slot<\/code><\/p>\n<p><code>        uses: azure\/webapps-deploy@v3<\/code><\/p>\n<p><code>        with:<\/code><\/p>\n<p><code>          app-name: my-app<\/code><\/p>\n<p><code>          slot-name: staging<\/code><\/p>\n<p><code>          package: .\/dist<\/code><\/p>\n<p><code>      - name: Smoke tests<\/code><\/p>\n<p><code>        run: npm run test:smoke -- --url https:\/\/my-app-staging.azurewebsites.net<\/code><\/p>\n<p><code>  promote-to-production:<\/code><\/p>\n<p><code>    needs: deploy-staging<\/code><\/p>\n<p><code>    runs-on: ubuntu-latest<\/code><\/p>\n<p><code>    environment: production        # triggers reviewer gate<\/code><\/p>\n<p><code>    steps:<\/code><\/p>\n<p><code>      - uses: azure\/login@v1<\/code><\/p>\n<p><code>        with:<\/code><\/p>\n<p><code>          creds: ${{ secrets.AZURE_CREDENTIALS }}<\/code><\/p>\n<p><code>      - name: Swap staging to production<\/code><\/p>\n<p><code>        uses: azure\/cli@v1<\/code><\/p>\n<p><code>        with:<\/code><\/p>\n<p><code>          inlineScript: |<\/code><\/p>\n<p><code>            az webapp deployment slot swap <\/code><\/p>\n<p><code>              --name my-app <\/code><\/p>\n<p><code>              --resource-group my-rg <\/code><\/p>\n<p><code>              --slot staging <\/code><\/p>\n<p><code>              --target-slot production<\/code><\/p>\n<p>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 \u2014 no redeploy, no rollback drama.<\/p>\n<h3>Audit Logging<\/h3>\n<p>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 \u2014 not just GitHub.<\/p>\n<p>Add this to your deploy job:<\/p>\n<p><code>      - name: Write deployment record<\/code><\/p>\n<p><code>        uses: azure\/cli@v1<\/code><\/p>\n<p><code>        with:<\/code><\/p>\n<p><code>          inlineScript: |<\/code><\/p>\n<p><code>            az monitor activity-log alert create <\/code><\/p>\n<p><code>              --name \"deploy-${{ github.run_id }}\" <\/code><\/p>\n<p><code>              --resource-group my-rg <\/code><\/p>\n<p><code>              --description \"Actor: ${{ github.actor }} | SHA: ${{ github.sha }} | Ref: ${{ github.ref }}\"<\/code><\/p>\n<p>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 \u2014 from code commit to live behavior.<\/p>\n<h3>Mistakes I See Teams Make<\/h3>\n<p>A few patterns that consistently undermine DevSecOps pipelines, based on implementations I\u2019ve been part of or reviewed:<\/p>\n<ul>\n<li>Running Security Jobs After the Build: If your SAST scan comes last, engineers learn to ignore it. When it\u2019s slow \u2014 and it sometimes is \u2014 it becomes the step that gets skipped under deadline pressure. Move security to the front.<\/li>\n<li>Storing Credentials in GitHub Variables Instead of Secrets: Environment variables show up in run logs. GitHub Actions encrypted secrets don\u2019t. This is not a subtle distinction.<\/li>\n<li>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.<\/li>\n<li>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.<\/li>\n<li>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.<\/li>\n<\/ul>\n<h3>Where to Go From Here<\/h3>\n<p>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.<\/p>\n<p>Start with CodeQL and Gitleaks. They\u2019re free, they integrate cleanly with GitHub Actions and they\u2019ll 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 \u2014 neither requires significant setup on Azure.<\/p>\n<p>The shift-left principle isn\u2019t complicated: Fix security problems when they\u2019re cheap to fix, which is before the code is deployed. A pipeline that enforces this automatically is what makes that principle real.<\/p>\n<p><a href=\"https:\/\/devops.com\/how-to-build-a-devsecops-ci-cd-pipeline-on-azure-with-github-actions\/\" target=\"_blank\" class=\"feedzy-rss-link-icon\">Read More<\/a><\/p>\n<p>\u200b<\/p>","protected":false},"excerpt":{"rendered":"<p>Three years ago, two days before a production push to Azure, I was working through a pull request on a [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":4550,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[5],"tags":[],"class_list":["post-4549","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/4549","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=4549"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/4549\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media\/4550"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=4549"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=4549"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=4549"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}