GitHub Actions CI/CD for .NET 2026 — OIDC, Egress Firewall, and Immutable Actions for Production Pipelines

Posted on: 4/17/2026 3:11:11 PM

Every time a developer pushes code, a complex chain of events has to happen: build, test, static analysis, container packaging, deploy to staging, smoke test, then promote to production. Manually? 45 minutes and easy to miss a step. Automated with GitHub Actions? Under 8 minutes and never skips a step. In 2026, GitHub Actions is no longer just a CI/CD runner — it has become a full-fledged DevOps platform with enterprise-grade security, parallel steps, OIDC authentication, and an egress firewall — all natively integrated into GitHub.

This article digs into a CI/CD pipeline architecture for .NET 10 on GitHub Actions: from NuGet/Docker caching that cuts 80% of build time, reusable workflows for multi-repo setups, to supply-chain security with immutable actions and zero-credential deploys to Azure via OIDC.

39% Runner price drop vs 2024
<8 min Full .NET 10 pipeline (with cache)
0 Stored credentials (OIDC)
Layer 7 Egress Firewall on runners

CI/CD pipeline architecture for .NET 10

A production-grade .NET pipeline must clearly separate CI (Continuous Integration — every push) from CD (Continuous Deployment — only on merges to main). That line prevents accidental deploys and keeps the developer feedback loop fast.

graph LR
    subgraph CI["CI — Every Push/PR"]
        direction TB
        A["Checkout + Cache Restore"] --> B["dotnet restore"]
        B --> C["dotnet build"]
        C --> D["dotnet test"]
        D --> E["Code Analysis"]
    end
    subgraph CD["CD — Merge into main"]
        direction TB
        F["Docker Build + Push"] --> G["Deploy Staging"]
        G --> H["Smoke Test"]
        H --> I["Deploy Production"]
    end
    CI --> CD
    style CI fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style CD fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50
    style A fill:#e94560,stroke:#fff,color:#fff
    style I fill:#4CAF50,stroke:#fff,color:#fff

Figure 1: A clean CI/CD split for .NET 10

CI on every push, CD only on main

The golden rule: CI runs on every push and pull request to catch errors early. CD only triggers on merges to main — completely preventing accidental deploys from feature branches. This is the pattern officially recommended by GitHub for every production project.

Caching strategy — 80% build time saved

Cache is the single biggest lever on pipeline speed. On GitHub Actions there are two caches that matter for .NET: the NuGet package cache and the Docker BuildKit layer cache.

NuGet package caching

Without a cache, dotnet restore re-downloads every NuGet package each run — typically 60–90 seconds for a mid-sized project. With actions/cache keyed off the hash of .csproj files, a cache hit brings restore under 5 seconds.

- name: Cache NuGet packages
  uses: actions/cache@v4
  with:
    path: ~/.nuget/packages
    key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }}
    restore-keys: |
      nuget-${{ runner.os }}-

The cache key hashes every .csproj — any dependency change correctly invalidates the cache. restore-keys provides a fallback to the newest cache for the same OS, still saving a lot compared to a cold download.

Docker BuildKit layer caching

For container-packaging pipelines, Docker BuildKit layer caching is a game-changer. Using the GitHub Actions cache backend (type=gha), build time drops from 4–5 minutes to under 60 seconds on cache hit.

- name: Build and push Docker image
  uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: ${{ steps.meta.outputs.tags }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

The mode=max option caches every layer, including intermediates — ideal for Dockerfiles with many stages. Combine with docker/metadata-action to auto-generate tags: sha-abc1234 for traceability and latest for the main branch.

Cache technique No cache Cache (hit) Savings
NuGet Restore 60–90s <5s ~93%
Docker Build 240–300s 30–60s ~80%
dotnet build (incremental) 45–60s 10–15s ~75%
Total pipeline ~8–10 min ~2–3 min ~70%

A complete workflow YAML for .NET 10

Below is a production-ready workflow combining CI + CD, NuGet cache, Docker build, and deploy via OIDC — with zero long-lived secrets.

name: CI/CD .NET 10

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

permissions:
  contents: read
  id-token: write     # Required for OIDC
  packages: write     # Push containers to GHCR

env:
  DOTNET_VERSION: '10.0.x'
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}

      - name: Cache NuGet
        uses: actions/cache@v4
        with:
          path: ~/.nuget/packages
          key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }}
          restore-keys: nuget-${{ runner.os }}-

      - name: Restore
        run: dotnet restore

      - name: Build
        run: dotnet build --no-restore -c Release

      - name: Test
        run: dotnet test --no-build -c Release --logger trx --results-directory results

      - name: Upload test results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results
          path: results/*.trx

  deploy:
    needs: build-and-test
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Login to Azure (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Deploy to Azure Container Apps
        uses: azure/container-apps-deploy-action@v2
        with:
          containerAppName: my-app
          resourceGroup: my-rg
          imageToDeploy: ${{ steps.meta.outputs.tags }}

About the id-token: write permission

The id-token: write permission lets the workflow request an OIDC token from GitHub. That token is short-lived (usually 10 minutes) and valid only for the exact repository and workflow that's running. Azure Entra ID verifies the token via federated credentials — no client secret or certificate needed. This is the only way you should deploy to the cloud in 2026.

OIDC — Zero-credential deploys to Azure

Traditionally, CI/CD pipelines had to store a service principal password as a GitHub Secret. The problem: secrets can leak through logs, forks, or compromised actions. OIDC (OpenID Connect) removes long-lived credentials entirely.

sequenceDiagram
    participant GH as GitHub Actions
    participant IDP as GitHub OIDC Provider
    participant AZ as Azure Entra ID
    participant RES as Azure Resources
    GH->>IDP: Request OIDC token
    IDP-->>GH: JWT token (repo, branch, workflow)
    GH->>AZ: Present JWT token
    AZ->>AZ: Verify issuer + subject claims
    AZ-->>GH: Short-lived access token
    GH->>RES: Deploy using the access token
    Note over AZ,RES: No secrets stored anywhere

Figure 2: OIDC auth flow between GitHub Actions and Azure

Setting up OIDC on Azure

Azure-side config has three steps: create an App Registration, add a Federated Credential, and assign an RBAC role.

# 1. Create the App Registration
az ad app create --display-name "github-actions-deploy"
APP_ID=$(az ad app list --display-name "github-actions-deploy" --query "[0].appId" -o tsv)

# 2. Create the Service Principal
az ad sp create --id $APP_ID

# 3. Add a Federated Credential
az ad app federated-credential create --id $APP_ID --parameters '{
  "name": "github-main-branch",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:myorg/myrepo:ref:refs/heads/main",
  "audiences": ["api://AzureADTokenExchange"]
}'

# 4. Grant role on the resource group
az role assignment create \
  --assignee $APP_ID \
  --role "Contributor" \
  --scope "/subscriptions/{sub-id}/resourceGroups/my-rg"

The subject claim in the federated credential scopes exactly which repo and branch can deploy. That means even if an attacker forks the repo, the fork's OIDC token has a different subject and Azure rejects it — far more secure than static secrets.

New custom claims in 2026

GitHub just added repository custom properties to OIDC token claims. You can now write trust policies based on custom repo metadata — e.g. only allow deploys when a repo has the property environment: production. This attribute-based access control (ABAC) is more flexible than per-repo subject matching.

Supply-chain security — The 2026 Security Roadmap

In 2026, GitHub is investing heavily in CI/CD security with three pillars: Immutable Actions, Egress Firewall, and Actions Data Stream. This is a direct response to serious supply-chain attacks (like the Trivy compromise in March 2026).

Immutable Actions — Locking down dependencies

The core problem: action dependencies are resolved at runtime via mutable references (tags, branches). Today's v4 tag can point to a different commit tomorrow if the maintainer is compromised. That means CI/CD pipelines aren't deterministic.

GitHub's solution: a new dependencies: section in workflow YAML that locks every direct and transitive dependency by commit SHA. Every workflow run executes code you've actually reviewed — a hash mismatch halts execution before the job starts.

# Current pattern (UNSAFE)
- uses: actions/checkout@v4          # Tag can be moved

# Safe pattern (pin SHA)
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.7

# Future pattern (dependencies lock)
dependencies:
  actions/checkout:
    version: v4.1.7
    sha: b4ffde65f46336ab88eb53be808477a3936bae11
    # Transitive deps are locked too

Lesson from the Trivy Supply-Chain Attack (03/2026)

In March 2026, the aquasecurity/trivy-action was compromised — attackers injected malicious code into a mutable tag, hitting thousands of workflows. Pipelines that pinned SHAs were completely safe. Pipelines using @master or @latest tags were exploited. This is why pinning SHAs on every third-party action is non-negotiable.

Egress Firewall — Controlling outbound connections

GitHub is building a native egress firewall operating at Layer 7, living outside the runner VM. Even if an attacker gets root inside a runner, the firewall is immutable and can't be bypassed.

graph TB
    subgraph Runner["GitHub-hosted Runner VM"]
        W["Workflow Step"] --> NET["Outbound Request"]
    end
    subgraph FW["Egress Firewall (Layer 7)"]
        NET --> CHECK["Allowlist check"]
        CHECK -->|Allow| EXT["External Service"]
        CHECK -->|Deny| BLOCK["Blocked + Logged"]
    end
    subgraph AUDIT["Audit Trail"]
        CHECK --> LOG["Request → Workflow → Job → Step"]
    end
    style FW fill:#f8f9fa,stroke:#e94560,color:#2c3e50
    style BLOCK fill:#ff9800,stroke:#fff,color:#fff
    style EXT fill:#4CAF50,stroke:#fff,color:#fff

Figure 3: Egress Firewall controlling outbound traffic from runners

Organizations can:

  • Monitor: see every outbound request, auto-audited with the matching workflow/job/step
  • Allowlist: only permit connections to required domains (nuget.org, ghcr.io, Azure endpoints)
  • Block: fully block unwanted connections — preventing data exfiltration from a compromised step

Parallel Steps — The most-requested feature

Since 2020, GitHub Community Discussion #14484 about parallel steps has collected thousands of upvotes. In 2026, GitHub is officially building it, targeting H1 2026.

Currently, steps within a job run sequentially. To parallelize, you must split work into multiple jobs — complex and wasteful since each job needs its own runner. Parallel steps let independent steps run concurrently within the same job.

graph LR
    subgraph Before["Today: sequential"]
        direction TB
        S1["Restore (30s)"] --> S2["Build (45s)"]
        S2 --> S3["Unit Test (60s)"]
        S3 --> S4["Integration Test (90s)"]
        S4 --> S5["Lint (20s)"]
    end
    subgraph After["Parallel Steps"]
        direction TB
        P1["Restore + Build (50s)"] --> P2["Unit Test (60s)"]
        P1 --> P3["Integration Test (90s)"]
        P1 --> P4["Lint (20s)"]
    end
    style Before fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
    style After fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50

Figure 4: Parallel steps cut total pipeline time from 245 s down to ~140 s

With parallel steps, a .NET pipeline can run unit tests, integration tests, and linting concurrently after build — total time drops from the sum of steps (sequential) to the slowest step (parallel).

Reusable Workflows — DRY for multi-repo

When an organization manages many .NET microservices, duplicating workflow YAML is a nightmare. Reusable Workflows let you define the pipeline once in a central repo and reuse it from every service repo.

# .github/workflows/dotnet-ci.yml (central repo: myorg/ci-templates)
name: .NET CI Template
on:
  workflow_call:
    inputs:
      dotnet-version:
        type: string
        default: '10.0.x'
      project-path:
        type: string
        required: true
    secrets:
      AZURE_CLIENT_ID:
        required: true
      AZURE_TENANT_ID:
        required: true

jobs:
  build-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ inputs.dotnet-version }}
      - run: dotnet restore ${{ inputs.project-path }}
      - run: dotnet build ${{ inputs.project-path }} --no-restore -c Release
      - run: dotnet test ${{ inputs.project-path }} --no-build -c Release
# .github/workflows/ci.yml (service repo)
name: CI
on: [push, pull_request]

jobs:
  ci:
    uses: myorg/ci-templates/.github/workflows/dotnet-ci.yml@main
    with:
      project-path: src/MyService.sln
    secrets:
      AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
      AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}

Reusable Workflows + OIDC: Important caveat

When a reusable workflow needs OIDC, the caller workflow must declare permissions: id-token: write. The OIDC subject claim contains the caller repo's information (where it's invoked from), not the template repo's. That way the federated credential on Azure still matches the actual deploying repo.

Matrix builds — Parallel testing across versions

The matrix strategy lets you run the same test suite across multiple .NET versions, OSes, or databases simultaneously. Especially useful for NuGet libraries that must support multi-target.

jobs:
  test:
    strategy:
      matrix:
        dotnet: ['8.0.x', '9.0.x', '10.0.x']
        os: [ubuntu-latest, windows-latest]
      fail-fast: false
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ matrix.dotnet }}
      - run: dotnet test --configuration Release

With fail-fast: false, all 6 combinations (3 .NET × 2 OS) run to completion even if one job fails — surfacing every compatibility issue in a single run instead of fixing them one at a time.

Runner pricing 2026 — Stronger and cheaper

In January 2026, GitHub significantly cut runner prices: the new 4-vCPU "Standard" runner costs the same as the old 2-vCPU runner from 2024. Double the compute at the same price.

Runner type vCPU RAM Price ($/min) vs 2024
ubuntu-latest (2024) 2 7 GB $0.008
Standard Runner 2026 4 16 GB $0.008 -39% per vCPU
Large Runner 8 32 GB $0.016 Unchanged
GPU Runner (Linux) 4 28 GB + T4 $0.07 New 2025

The free tier is still extremely generous: 2,000 minutes/month for public repos (unlimited) and private repos on GitHub Free. With good caching, this is enough for a small team of 3-5 developers to run CI/CD every day.

Production CI/CD security checklist

Based on GitHub's 2026 Security Roadmap and lessons from supply-chain incidents, here's the essential security checklist for every .NET pipeline.

Item Practice Level
Pin Actions Pin every third-party action to a full SHA, never use tags Mandatory
OIDC Use OIDC instead of static credentials for cloud deploys Mandatory
Permissions Declare minimum permissions, never use default write-all Mandatory
Branch Protection Require status checks + review before merging to main Mandatory
Egress Firewall Enable monitor mode, build an allowlist, then enforce Recommended
Dependabot Auto-update action versions + NuGet packages Recommended
CODEOWNERS Protect .github/workflows/ with CODEOWNERS Recommended
Secret Scanning Enable push protection to block secret-laden commits Mandatory

GitHub Actions 2026 roadmap

Q1 2026
Timezone support for scheduled jobs — no more manual UTC offset math. workflow_dispatch returns a run ID for programmatic tracking.
Q1–Q2 2026
Actions Data Stream — a new visibility layer with detailed audit logs for every workflow execution. Egress Firewall native on GitHub-hosted runners.
H1 2026
Parallel Steps — independent steps running concurrently within a job. Immutable Actions with a dependencies lock file.
Q2 2026
Case function in expressions. UX improvements with faster page loads. Azure private networking failover for hosted runners.

Conclusion

GitHub Actions in 2026 is no longer just a "CI/CD tool" — it's a security-first DevOps platform. With egress firewall, immutable actions, and OIDC, CI/CD pipelines can for the first time reach the same security bar as production infrastructure. Combine with NuGet + Docker BuildKit caching that cuts build time by 80%, reusable workflows that standardize pipelines across the organization, and upcoming parallel steps to shave another 40% off runtimes.

For .NET 10 teams, GitHub Actions is the natural pick: deep GitHub integration, a generous free tier, stronger and cheaper runners, and a massive action ecosystem. The most important rule: pin SHAs on every action, use OIDC for every cloud deploy.

Get started now

If you're on Azure DevOps Pipelines or Jenkins, migrating to GitHub Actions isn't as painful as you might think. GitHub provides an official migration guide for each CI system. First step: create a simple workflow that just runs dotnet test on each PR — then grow from there.

References: