Skip to main content

DevSecOps in 2026: Secure Your CI/CD Pipeline in .NET

Learn how to build a DevSecOps CI/CD pipeline for .NET in 2026 — secret scanning, SAST, SBOMs and runtime hardening. Start securing your builds today.

Building a DevSecOps CI/CD pipeline is no longer a "nice to have" for .NET teams — in 2026 it is the baseline expectation of every enterprise security review, every SOC 2 auditor, and increasingly every regulator. The EU Cyber Resilience Act obligations are landing, SLSA provenance is showing up in procurement checklists, and the majority of breaches that hit .NET shops still trace back to the same three things: a leaked credential, an unpatched transitive NuGet package, and a SQL string that someone concatenated at 4:45pm on a Friday.

The good news is that the .NET toolchain has quietly become one of the best-instrumented ecosystems for security automation. Roslyn analyzers run inside your compiler. dotnet list package --vulnerable ships in the SDK. SBOM generation is a one-line MSBuild target. This guide walks through how to build security into your pipeline from day one, with runnable C# examples and the reasoning behind each control.

What DevSecOps Actually Means (and What It Doesn't)

DevSecOps is often reduced to "run a scanner in CI." That framing is why so many programs fail. The real definition is simpler: security controls execute automatically at the earliest point where they can still be cheap to fix.

That last clause matters. A SQL injection caught by a Roslyn analyzer in your IDE costs about thirty seconds to fix. The same bug caught by a penetration test six months later costs a sprint, a customer notification, and a retrospective. This is the entire economic argument for shift left security — not that early scanning finds more bugs, but that early bugs are 10–100x cheaper to remediate.

What DevSecOps is not: a gate that blocks every build on every CVE. If your pipeline cries wolf, developers will learn to bypass it. A pipeline that blocks on 12 findings and gets respected is worth more than one that reports 400 findings and gets ignored.

The Five Stages of a Secure .NET Pipeline

Think of your pipeline as five checkpoints, each catching a different class of problem:

  • Pre-commit — secret detection, formatting, fast analyzers
  • Build — Roslyn security analyzers, banned API enforcement, warnings as errors
  • Test — dependency scanning, SAST, security-focused unit tests
  • Package — SBOM generation, container scanning, artifact signing
  • Deploy & runtime — secretless auth, least privilege, runtime monitoring

Stage 1: Stop Secrets Before They Reach Git

Once a credential is in git history, it is compromised — rewriting history does not help because forks, CI caches, and clones already have it. Rotation is the only remedy, so prevention pays enormous dividends.

Install a pre-commit hook using Gitleaks or the built-in dotnet-gitleaks tooling, and pair it with a build-time analyzer. But the structural fix is architectural: make it impossible for a secret to need to live in source. Here is the pattern to standardise on:

// DON'T: a connection string with credentials in appsettings.json
// "ConnectionStrings": { "Db": "Server=prod;User Id=sa;Password=Hunter2;" }

// DO: passwordless auth via managed identity — there is no secret to leak
using Azure.Identity;
using Microsoft.Data.SqlClient;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped(_ =>
{
    // Connection string contains NO credentials
    var cs = builder.Configuration.GetConnectionString("Db");
    // e.g. "Server=prod.database.windows.net;Database=orders;
    //       Authentication=Active Directory Default;"
    return new SqlConnection(cs);
});

// Pull any remaining secrets from a vault at startup, never from source control
builder.Configuration.AddAzureKeyVault(
    new Uri("https://my-vault.vault.azure.net/"),
    new DefaultAzureCredential());

Why this works: DefaultAzureCredential resolves to your developer identity locally and to a workload/managed identity in production. The credential is issued at runtime, short-lived, and never serialised into a file an engineer could commit. You have removed the class of bug rather than detecting instances of it.

Stage 2: Turn the Compiler Into a Security Scanner

Most .NET teams leave free security tooling switched off. The .NET SDK ships security analyzers (the CA2100–CA5405 range) that are disabled or informational by default. Enable them at the highest level your codebase can tolerate, then ratchet up.

<!-- Directory.Build.props — applies to every project in the repo -->
<Project>
  <PropertyGroup>
    <AnalysisMode>All</AnalysisMode>
    <AnalysisLevel>latest</AnalysisLevel>
    <EnableNETAnalyzers>true</EnableNETAnalyzers>
    <Nullable>enable</Nullable>

    <!-- The line that makes security real: findings break the build -->
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

    <!-- Supply-chain hardening -->
    <RestoreLockedMode>true</RestoreLockedMode>
    <NuGetAuditMode>all</NuGetAuditMode>
    <NuGetAuditLevel>moderate</NuGetAuditLevel>
  </PropertyGroup>
</Project>

NuGetAuditMode=all is the important one for 2026. It audits transitive dependencies, not just direct ones — and transitive packages are where supply-chain attacks live. RestoreLockedMode forces restore to fail if packages.lock.json doesn't match, which defeats dependency-confusion attacks where an attacker publishes a higher version number of your internal package name to nuget.org.

Next, ban the dangerous APIs outright. Create a BannedSymbols.txt file and reference it as an AdditionalFiles item:

// BannedSymbols.txt
// T:System.Runtime.Serialization.Formatters.Binary.BinaryFormatter;
//     Insecure deserialization — use System.Text.Json
// M:System.Security.Cryptography.MD5.Create();
//     Broken hash — use SHA256
// M:System.Text.Encoding.UTF8.GetString(System.Byte[]);
//     Validate encoding explicitly

// Now this fails the build with RS0030 instead of shipping:
var hash = MD5.Create().ComputeHash(passwordBytes);   // ERROR

// The compliant version — cost-factor password hashing
using System.Security.Cryptography;

public static string HashPassword(string password)
{
    byte[] salt = RandomNumberGenerator.GetBytes(16);

    using var pbkdf2 = new Rfc2898DeriveBytes(
        password, salt, iterations: 600_000, HashAlgorithmName.SHA256);

    byte[] key = pbkdf2.GetBytes(32);
    return $"{Convert.ToBase64String(salt)}.{Convert.ToBase64String(key)}";
}

public static bool Verify(string password, string stored)
{
    var parts = stored.Split('.');
    byte[] salt = Convert.FromBase64String(parts[0]);
    byte[] expected = Convert.FromBase64String(parts[1]);

    using var pbkdf2 = new Rfc2898DeriveBytes(
        password, salt, 600_000, HashAlgorithmName.SHA256);

    // Constant-time compare — prevents timing attacks
    return CryptographicOperations.FixedTimeEquals(
        pbkdf2.GetBytes(32), expected);
}

Why FixedTimeEquals matters: a naive SequenceEqual returns as soon as bytes differ. An attacker measuring response times can recover a hash byte-by-byte. This is the kind of subtlety no scanner will catch for you — which is why secure coding in C# training sits alongside tooling, not behind it.

Stage 3: Security Unit Tests — The Underused Control

SAST tools have false negatives. Tests do not. Write executable assertions for your security invariants, and they will run on every commit forever:

public class AuthorizationTests
{
    [Fact]
    public async Task Every_endpoint_requires_authorization_unless_opted_out()
    {
        var endpoints = _factory.Services
            .GetRequiredService<EndpointDataSource>()
            .Endpoints
            .OfType<RouteEndpoint>();

        var unprotected = endpoints.Where(e =>
            e.Metadata.GetMetadata<IAuthorizeData>() is null &&
            e.Metadata.GetMetadata<IAllowAnonymous>() is null)
            .Select(e => e.RoutePattern.RawText);

        Assert.Empty(unprotected);   // Fail loudly on a forgotten [Authorize]
    }

    [Theory]
    [InlineData("' OR 1=1--")]
    [InlineData("'; DROP TABLE Orders;--")]
    public async Task Search_is_not_injectable(string payload)
    {
        var response = await _client.GetAsync($"/orders?q={Uri.EscapeDataString(payload)}");

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.Empty(await ReadOrders(response));
    }
}

The first test is the single highest-value security test most ASP.NET Core teams are missing. Forgotten [Authorize] attributes on new controllers are a leading cause of broken access control — number one on the OWASP Top 10 — and this catches every one of them at build time.

Stage 4: SBOMs and Supply-Chain Provenance

A software bill of materials (SBOM) is now contractually required by many US federal and UK public-sector buyers. More practically, it answers the question you will be asked the morning after the next Log4Shell-class event: "are we affected?" Without an SBOM, that takes days. With one, it takes a grep.

# .github/workflows/ci.yml — a complete DevSecOps CI/CD pipeline
name: build-and-scan
on: [push, pull_request]

permissions:
  contents: read
  security-events: write
  id-token: write          # OIDC — no long-lived cloud secrets in CI

jobs:
  secure-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }

      - name: Secret scanning
        uses: gitleaks/gitleaks-action@v2

      - uses: actions/setup-dotnet@v4
        with: { dotnet-version: '10.0.x' }

      - name: Restore (locked)
        run: dotnet restore --locked-mode

      - name: Vulnerable dependency audit
        run: |
          dotnet list package --vulnerable --include-transitive \
            --format json > audit.json
          grep -q '"severity": "\(High\|Critical\)"' audit.json && exit 1 || true

      - name: Build with analyzers as errors
        run: dotnet build -c Release --no-restore /warnaserror

      - name: Test
        run: dotnet test -c Release --no-build

      - name: Generate SBOM (CycloneDX)
        run: |
          dotnet tool install --global CycloneDX
          dotnet CycloneDX ./src/Api/Api.csproj -o ./sbom

      - name: Container scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:${{ github.sha }}
          severity: CRITICAL,HIGH
          exit-code: '1'

      - uses: actions/upload-artifact@v4
        with: { name: sbom, path: ./sbom }

Note id-token: write. Federated OIDC lets your pipeline authenticate to Azure or AWS with a short-lived token instead of a stored secret. Removing static cloud credentials from CI eliminates one of the most valuable targets an attacker can hit — CI runners typically hold production-grade permissions.

Stage 5: Harden the Runtime

Ship containers that run as non-root, with a read-only filesystem and no shell. .NET 8+ chiselled images make this nearly free:

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish ./src/Api -c Release -o /app

# Chiselled runtime: no shell, no package manager, ~100MB smaller attack surface
FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled
WORKDIR /app
COPY --from=build /app .
USER $APP_UID
ENTRYPOINT ["./Api"]

Best Practices for Your DevSecOps CI/CD Pipeline

  • Fail the build only on High/Critical. Report everything else. Signal-to-noise determines whether your program survives contact with a deadline.
  • Baseline existing findings. Legacy code will light up like a Christmas tree. Suppress the existing set, then block all new findings. Ratchet, don't big-bang.
  • Commit packages.lock.json. Reproducible restores are a prerequisite for meaningful scanning.
  • Use Central Package Management (Directory.Packages.props) so one version bump fixes every project.
  • Scan the pipeline itself. Pin GitHub Actions to commit SHAs, not tags — tags are mutable.
  • Measure mean time to remediate, not vulnerability count. Count is vanity; MTTR is the number that predicts breach outcomes.

Common Pitfalls

Treating the scanner's output as the definition of "secure." SAST cannot see business logic flaws — the IDOR where user A can read user B's invoice by changing an ID. Threat modelling and code review still carry that load.

Blocking merges on scans that take 25 minutes. Developers will route around it. Run fast checks (secrets, analyzers, unit tests) on every PR; run deep scans (DAST, full SCA) nightly on main.

Ignoring transitive dependencies. Your direct dependency count might be 20. Your transitive count is probably 400. That is where the risk lives — hence --include-transitive.

Security as a separate team's job. If a security engineer is the only person who can read the pipeline output, you have built a bottleneck, not a DevSecOps practice. Findings must land in the developer's PR, in their language, with a suggested fix.

Key Takeaways

A working DevSecOps CI/CD pipeline in 2026 is not an expensive platform purchase — for .NET teams, most of it is already sitting in your SDK waiting to be switched on. Start here:

  • Enable AnalysisMode=All, NuGetAuditMode=all and TreatWarningsAsErrors in Directory.Build.props today. That is a 30-minute change with outsized return.
  • Eliminate secrets architecturally with managed identity and DefaultAzureCredential rather than detecting them after the fact.
  • Write the "every endpoint is authorized" test — it prevents the most common serious ASP.NET Core vulnerability class.
  • Generate an SBOM on every build so the next zero-day is a grep, not a fire drill.
  • Replace static CI credentials with OIDC federation and ship non-root chiselled containers.
  • Tune aggressively for signal. A pipeline developers trust beats a comprehensive one they bypass.

Security built in from day one costs a fraction of security bolted on after an incident. Pick one control from this list, ship it this sprint, and let the ratchet do the rest.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...