Your pipeline ran. Your artifact was built, signed, and deployed. And somewhere in that chain, a dependency you didn’t write—one you probably didn’t audit—quietly did something it wasn’t supposed to. That’s the software supply chain threat in one sentence: the attack surface you don’t control is almost always larger than the one you do.
SolarWinds proved this in 2020. The build system itself was compromised, injecting malicious code into signed, trusted software before the signature was even applied. The digital signature verified the signer—it said nothing about the integrity of the code that went in. A signed artifact isn’t a trustworthy artifact. It’s just a signed one.
“Trust but verify” in an Azure DevOps context means building a pipeline that doesn’t just build and sign—it proves. Every artifact comes with a traceable history: what went into it, who built it, on what infrastructure, from which commit. This post walks you through implementing that capability across five areas: SBOM generation, artifact signing with Microsoft Artifact Signing, dependency supply chain lockdown with Azure Artifacts, dependency scanning, and deployment gate enforcement.
Prerequisites
Before you start adding tasks to pipelines, make sure your environment matches these requirements. Some of these involve Azure resource creation that needs to happen ahead of time.
-
An Azure DevOps organization with at least one pipeline you control
-
An Azure subscription with permissions to create resource groups and Azure resources
-
The Microsoft SBOM Tool task available in your organization (available via the marketplace extension or as a direct binary call)
-
An Artifact Signing account provisioned in Azure (covered in the signing section below)
-
Workload Identity Federation configured on your Azure DevOps service connection—if you’re still using service principal secrets here, fix that first
Pro Tip: The Workload Identity Federation requirement isn’t optional nicety. Running a pipeline with a service principal secret means that secret could be exfiltrated from pipeline logs, environment variables, or compromised agents. Workload Identity Federation eliminates the secret entirely—your pipeline authenticates via a token exchange, not a password. Microsoft’s documentation covers the setup.
What Digital Provenance Actually Means
Provenance is the verifiable history of an artifact. Not just where it ended up—where it came from, step by step. In software terms, provenance answers: which source commit triggered this build, what dependencies were resolved, which build agent ran the pipeline, and who (or what automated system) authorized the deployment.
This matters because Supply-chain Levels for Software Artifacts (SLSA)—the framework that formalizes supply chain security requirements—grades your builds on exactly this. SLSA isn’t a checkbox. It’s a maturity ladder:
| SLSA Level | What It Requires | What It Prevents |
|---|---|---|
| Level 1 | Build process is scripted and version-controlled; provenance generated | Accidental tampering |
| Level 2 | Build runs on a hosted platform (like Azure DevOps); provenance generated and cryptographically signed by the platform | Tampering after the build |
| Level 3 | Hardened build platform with isolated environments; signing key material inaccessible to user-defined build steps | Tampering during the build by insiders or compromised credentials |
Azure DevOps pipelines running on Microsoft-hosted agents satisfy the infrastructure requirement for SLSA Level 2 but do not achieve it automatically—Level 2 also requires cryptographically signed provenance generated by the build platform. Native SLSA attestation is not built into Azure DevOps; third-party tools are currently required. Getting to Level 3 requires hardened build isolation and key inaccessibility, which is where the artifact signing and attestation story gets serious.
Executive Order 14028 on improving cybersecurity mandated SBOM requirements for software sold to the US federal government. Even if you don’t sell to the government, the expectation that vendors can produce SBOMs on demand is becoming standard procurement language.
Step 1: Generate an SBOM in Your Pipeline
A Software Bill of Materials (SBOM) is a machine-readable inventory of every component in your software—first-party code, open-source dependencies, transitive dependencies your dependencies pulled in. It’s the document your auditors request when they want to know exactly what’s running in production—and they will request it.
The Microsoft SBOM Tool (sbom-tool) generates SBOMs in SPDX 2.2 format, the standard required by most regulatory frameworks. It auto-detects dependency manifests for NuGet, npm, PyPI, Maven, and Rust Crates, among others.
Add this to your pipeline YAML after your build step but before artifact publication:
- task: Bash@3
displayName: 'Generate SBOM'
inputs:
targetType: 'inline'
script: |
curl -Lo $AGENT_TOOLSDIRECTORY/sbom-tool https://github.com/microsoft/sbom-tool/releases/latest/download/sbom-tool-linux-x64
chmod +x $AGENT_TOOLSDIRECTORY/sbom-tool
$AGENT_TOOLSDIRECTORY/sbom-tool generate \
-b $(Build.ArtifactStagingDirectory) \
-bc $(Build.SourcesDirectory) \
-pn $(Build.Repository.Name) \
-pv $(Build.BuildNumber) \
-ps "YourOrganizationName" \
-nsb https://sbom.yourorg.com \
-V Verbose
The parameters worth understanding:
-
-b(Build Drop Path): Where your compiled artifacts live. The tool places the_manifestfolder here, so the SBOM travels with the binary. -
-bc(Build Components Path): Your source root. The tool scans this to findpackage.json,.csproj,requirements.txt, and similar manifests. -
-nsb(Namespace Base URI): A unique URI your organization controls. It doesn’t need to resolve to anything—it’s just a globally unique identifier for your SBOM namespace.
After generating, publish the SBOM as a pipeline artifact alongside your build output:
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifacts with SBOM'
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
publishLocation: 'Container'
Because the _manifest directory lands inside your artifact staging directory, PublishBuildArtifacts picks it up automatically. The SBOM now lives alongside your binary with the same retention policy and the same build association.
Key Insight: The SBOM is only useful if it stays with the artifact. Storing it separately—in a wiki, a SharePoint folder, a ticket—breaks the chain. If the SBOM can’t be cryptographically linked back to the specific binary it describes, it’s a document, not provenance.
Step 2: Sign Your Artifacts with Microsoft Artifact Signing
Signing proves two things: the artifact came from you (authenticity), and it hasn’t been modified since you signed it (integrity). What it doesn’t prove—as SolarWinds demonstrated—is that the source code was clean before you built it. That’s why signing is necessary but not sufficient.
Microsoft Artifact Signing (formerly Trusted Signing) is Microsoft’s fully managed signing service. Certificates and private keys live in FIPS 140-2 Level 3 certified hardware security modules (HSMs). You never handle the private key—the service does digest signing, where your pipeline sends a hash of the artifact, the HSM signs the hash, and the signature gets embedded back in your file. The key never leaves the HSM.
Provision the Artifact Signing Account
Create an Artifact Signing account resource using the Azure CLI:
az trustedsigning create \ --name <your-signing-account-name> \ --resource-group <your-resource-group> \ --location <your-region> \ --sku Premium
Then create a certificate profile for your account:
az trustedsigning certificate-profile create \ --account-name <your-signing-account-name> \ --resource-group <your-resource-group> \ --name <your-cert-profile-name> \ --profile-type PublicTrust \ --identity-validation-id <your-identity-validation-id>
You’ll need to complete two setup steps before the CLI commands fully take effect:
-
An identity validation step, where Microsoft verifies the organization name that will appear in the certificate’s Common Name (CN) field. This takes time—don’t plan to do it the day before a release.
-
A certificate profile type decision: choose between Public Trust and Private Trust based on how your software is distributed.
| Profile Type | Use Case | Trust Scope |
|---|---|---|
| Public Trust | Externally distributed software; signed by a Microsoft-trusted CA | Trusted by all Windows systems by default |
| Private Trust | Internal tooling, build artifacts, CI artifacts not distributed externally | Trusted only by systems you configure |
Configure the Pipeline Service Connection
Assign the Artifact Signing Certificate Profile Signer role to the managed identity or service principal backing your Azure DevOps service connection. Do this at the certificate profile scope, not the subscription scope. Least privilege applies here—if that service connection is compromised, the blast radius should be limited to signing operations, not your entire subscription.
az role assignment create \ --role "Artifact Signing Certificate Profile Signer" \ --assignee <your-managed-identity-object-id> \ --scope /subscriptions/<subscription-id>/resourceGroups/<rg-name>/providers/Microsoft.CodeSigning/codeSigningAccounts/<account-name>/certificateProfiles/<profile-name>
Add the Signing Task
The ArtifactSigning@0 task uses DefaultAzureCredential under the hood. The recommended authentication approach is OIDC via a federated service connection. First, inject the credentials using AzureCLI@2, then invoke the signing task with Azure CLI credential enabled:
- task: AzureCLI@2
displayName: 'Set OIDC Credentials for Artifact Signing'
inputs:
azureSubscription: 'ArtifactSigningServiceConnection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
addSpnToEnvironment: true
inlineScript: |
echo "##vso[task.setvariable variable=ARM_CLIENT_ID;issecret=true]$servicePrincipalId"
echo "##vso[task.setvariable variable=ARM_ID_TOKEN;issecret=true]$idToken"
echo "##vso[task.setvariable variable=ARM_TENANT_ID;issecret=true]$tenantId"
- bash: |
az login --service-principal \
-u $(ARM_CLIENT_ID) \
--tenant $(ARM_TENANT_ID) \
--allow-no-subscriptions \
--federated-token $(ARM_ID_TOKEN)
displayName: 'Azure Login'
- task: ArtifactSigning@0
displayName: 'Sign Artifacts'
inputs:
ArtifactSigningAccountName: 'your-signing-account'
CertificateProfileName: 'your-cert-profile'
FilesFolder: '$(Build.ArtifactStagingDirectory)'
FilesFolderFilter: 'exe,dll,msix'
FilesFolderRecurse: true
TimestampRfc3161: 'http://timestamp.acs.microsoft.com'
TimestampDigest: 'SHA256'
ExcludeAzureCliCredential: false
ExcludeEnvironmentCredential: true
ExcludeWorkloadIdentityCredential: true
ExcludeManagedIdentityCredential: true
For container images, Artifact Signing also integrates with the Notary Project’s notation CLI, allowing you to sign Open Container Initiative (OCI) artifacts in Azure Container Registry. The notation sign command uses Artifact Signing as a plugin, so the same HSM-managed keys sign both your binaries and your container images.
- task: Bash@3
displayName: 'Sign Container Image'
inputs:
targetType: 'inline'
script: |
notation sign \
--plugin azure-artifactsigning \
--signature-format cose \
--timestamp-url <azure-tsp-url> \
--plugin-config credential_type=workloadidentity \
--plugin-config subscription_id=<sub-id> \
--plugin-config resource_group=<rg> \
--plugin-config account_name=<account> \
--plugin-config certificate_profile=<profile> \
<registry>.azurecr.io/<image>@<digest>
Step 3: Lock Down Your Dependency Supply Chain with Azure Artifacts
Signing your artifacts addresses the downstream end. But supply chain attacks often enter from upstream: a compromised npm package, a deleted PyPI version, a typosquatted NuGet package that appeared in your lock file three months ago. Azure Artifacts addresses this.
Enable Upstream Sources and Feed Immutability
Configure your Azure Artifacts feed to proxy public registries rather than having your pipelines pull directly from npmjs.com, NuGet.org, or PyPI. When a package is pulled through an upstream source, Azure Artifacts caches a copy in your feed.
That cached copy is immutable. If the same version is later unpublished from the public registry—like the left-pad incident that broke thousands of builds—or if the published version is compromised and replaced, your build still gets the version you originally resolved. Your build is now reproducible because the dependencies it uses are frozen in a registry you control.
Reality Check: Upstream source pinning doesn’t protect you from a package that was malicious when you first pulled it. It only preserves your choice. This is why you also need the dependency scanning step covered in Step 4—scan before you pin, not after.
Configure your pipeline to use your feed exclusively:
- task: NuGetAuthenticate@1
displayName: 'Authenticate to Azure Artifacts Feed'
- task: NuGetCommand@2
displayName: 'Restore NuGet Packages'
inputs:
command: 'restore'
restoreSolution: '**/*.sln'
feedsToUse: 'select'
vstsFeed: '<your-organization>/<your-feed-name>'
Use Feed Views to Gate Promotion
Azure Artifacts supports feed views that define promotion stages for your packages. Use view promotion as a quality gate to create a traceable path from raw build output to production-approved artifact:
| Feed View | Promotion Criteria | Retention |
|---|---|---|
@Local |
Default; all packages land here first | Subject to retention policy |
@Prerelease |
Passes vulnerability scanning and testing | Subject to retention policy |
@Release |
Passes production deployment gates | Exempt from retention policy deletion—preserved indefinitely |
This creates a provenance trail at the artifact level: you can answer “which version of package X was in the production build from November?” and retrieve the exact binary.
Step 4: Scan Dependencies Before They Become Your Problem
You can’t sign and track what you don’t know you have. Dependency scanning—or Software Composition Analysis (SCA)—finds vulnerabilities in the open-source components your code depends on, including transitive dependencies (the packages your packages pull in).
GitHub Advanced Security for Azure DevOps (GHAS) includes dependency scanning that integrates directly into Azure Pipelines. It runs as a pipeline task and surfaces vulnerabilities in the pipeline interface and in Advanced Security alerts.
- task: AdvancedSecurity-Dependency-Scanning@1 displayName: 'Dependency Vulnerability Scan'
Microsoft Defender for DevOps, part of Microsoft Defender for Cloud, adds agentless scanning at the organization level. It can scan connected Azure DevOps repositories without requiring task-level integration on every pipeline, and it traces vulnerabilities found in deployed cloud resources back to the specific build and repository that introduced them—closing the provenance loop from source commit to runtime cloud resource.
Secret scanning is part of the same package. A pipeline secret committed to your repository can be used to sign malicious artifacts or authenticate to your artifact feed. GitHub Advanced Security (GHAS) for Azure DevOps detects credentials, tokens, and keys committed to your repos and blocks pushes that would expose them.
Step 5: Enforce Trust at Deployment Gates
You’ve generated an SBOM, signed your artifacts, frozen your dependencies, and scanned for vulnerabilities. None of that matters if a deployment pipeline will happily ship an unsigned binary to production. Enforcement is where the “verify” in “trust but verify” actually happens.
Azure Pipelines environment deployment gates and approvals let you block deployment to protected environments (staging, production) unless specific conditions are met.
Use custom policy checks to verify:
-
The deploying artifact was produced by a pipeline run (not manually uploaded)
-
The artifact has an associated SBOM in the build’s artifact store
-
The artifact’s signature can be verified against your Artifact Signing certificate profile
-
No high or critical vulnerability alerts are unacknowledged in Defender for DevOps
stages:
- stage: Build
jobs:
- job: BuildAndSign
steps:
# ... build, SBOM generation, signing steps ...
- stage: Deploy_Production
dependsOn: Build
jobs:
- deployment: DeployToProd
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: AzureCLI@2
displayName: 'Verify Artifact Signature'
inputs:
azureSubscription: 'ArtifactSigningServiceConnection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
# Verify the artifact before deployment
notation verify \
$(pipeline.artifactPath)
Branch policies in Azure Repos add another layer: require pull request review, require a successful build, and require all comments to be resolved before any code reaches the main branch. These ensure that the source commit feeding your pipeline has been reviewed by a human—which doesn’t prevent all supply chain attacks, but it does close the unreviewed-commit vector.
Putting It Together: A Hardened Pipeline Template
Here’s a condensed pipeline that applies all five practices in sequence. Adapt it to your build system and artifact types.
trigger:
- main
pool:
vmImage: 'windows-latest' # ArtifactSigning@0 requires a Windows runner
variables:
artifactStagingDir: '$(Build.ArtifactStagingDirectory)'
stages:
- stage: Build
displayName: 'Build, Scan, Sign, and Document'
jobs:
- job: SecureBuild
steps:
# Step 1: Authenticate to your private feed
- task: NuGetAuthenticate@1
displayName: 'Authenticate to Azure Artifacts'
# Step 2: Restore from your controlled feed only
- task: NuGetCommand@2
displayName: 'Restore Packages'
inputs:
command: 'restore'
vstsFeed: '<org>/<feed>'
# Step 3: Build
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'publish'
publishWebProjects: true
arguments: '--output $(artifactStagingDir)'
# Step 4: Scan dependencies
- task: AdvancedSecurity-Dependency-Scanning@1
displayName: 'Dependency Scan'
# Step 5: Generate SBOM
- task: Bash@3
displayName: 'Generate SBOM'
inputs:
targetType: 'inline'
script: |
curl -Lo $AGENT_TOOLSDIRECTORY/sbom-tool \
https://github.com/microsoft/sbom-tool/releases/latest/download/sbom-tool-linux-x64
chmod +x $AGENT_TOOLSDIRECTORY/sbom-tool
$AGENT_TOOLSDIRECTORY/sbom-tool generate \
-b $(artifactStagingDir) \
-bc $(Build.SourcesDirectory) \
-pn $(Build.Repository.Name) \
-pv $(Build.BuildNumber) \
-ps "YourOrg" \
-nsb https://sbom.yourorg.com
# Step 6: Sign artifacts (run AzureCLI@2 + az login steps before this)
- task: ArtifactSigning@0
displayName: 'Sign Artifacts'
inputs:
ArtifactSigningAccountName: '<signing-account>'
CertificateProfileName: '<cert-profile>'
FilesFolder: '$(artifactStagingDir)'
FilesFolderFilter: 'exe,dll'
TimestampRfc3161: 'http://timestamp.acs.microsoft.com'
TimestampDigest: 'SHA256'
ExcludeAzureCliCredential: false
ExcludeEnvironmentCredential: true
ExcludeWorkloadIdentityCredential: true
ExcludeManagedIdentityCredential: true
# Step 7: Publish with SBOM included
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifacts'
inputs:
PathtoPublish: '$(artifactStagingDir)'
ArtifactName: 'drop'
- stage: Deploy
dependsOn: Build
condition: succeeded()
jobs:
- deployment: Production
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- script: echo "Deploy verified artifact"
This pipeline runs on a hosted build platform and generates build provenance—satisfying the infrastructure requirements that SLSA Level 2 builds on. To actually achieve Level 2, you need cryptographically signed provenance from the build platform itself, which requires third-party tooling on top of Azure DevOps today. Layer in hardened build isolation with key material inaccessible to pipeline steps and you’re approaching Level 3.
Where to Go from Here
A few things you should add before calling this production-ready:
Retention policy for your @Release view. Promote artifacts that pass production gates to the Release view in Azure Artifacts. Packages and pipeline artifacts in that view are excluded from retention policies — they won’t be automatically deleted, which means you can audit production-era builds indefinitely.
Defender for DevOps at the organization level. The pipeline-level scanning in Step 4 is good. Adding Defender for DevOps at the organization level gives you a continuous, agentless view across all repositories—including the ones your team hasn’t touched in two years but are still running in production somewhere. (You have some of those. Everyone does.)
Separate environments for signing. Your signing service connection should be scoped to a production environment. Don’t let development or feature branch pipelines invoke signing with production certificates. Artifact Signing’s role assignment is scoped to the certificate profile, so you can have a dev-cert-profile and a prod-cert-profile with entirely separate role assignments.
The goal here isn’t a compliance checkbox. It’s a pipeline where every artifact tells a story: what went in, who built it, when, and on what. When something goes wrong—and eventually, something will—that story is what you hand to your incident responders instead of a two-week investigation.
Signing says “we built this.” Provenance says “here’s exactly how.” Both together say “verify it yourself.” That’s the point.