Build a Secure CI/CD Pipeline
Integrate Snyk security scanning into an Azure DevOps CI/CD pipeline.
Introduction
30 Second Summary
Every time a software team ships an update, someone needs to make sure that new code doesn't introduce security vulnerabilities. In professional teams, this checking happens automatically, long before any code reaches real users.
In this project, you will build a secure CI/CD pipeline in Azure DevOps that automatically builds, tests, and security-scans a Node.js application. The pipeline blocks any code with known vulnerabilities from being merged.
What You'll Build
A fully functional CI/CD pipeline that uses Azure DevOps for automation and Snyk for security scanning, running on a self-hosted build agent on your Mac.
By the end of this project, you'll have:
- A YAML-based CI/CD pipeline in Azure DevOps that builds, tests, and scans a Node.js application.
- A self-hosted build agent running on your Mac, connecting local infrastructure to a cloud DevOps platform.
- Snyk security scanning integrated as an automated gate that catches vulnerable dependencies before they reach production.
- A branch policy that enforces security checks on every pull request, plus a secret mission to add Infrastructure-as-Code scanning for Terraform configuration files.
Are there any prerequisites?
You need a macOS computer, a web browser, and a Microsoft account or GitHub account to sign up for Azure DevOps and Snyk. Both tools have free tiers that cover everything in this project at no cost.
Set Up Your Tools and Accounts
A secure CI/CD pipeline connects several moving parts. You need a runtime for your application, a platform to orchestrate your builds, and a scanner to catch vulnerabilities before they reach production.
This step gets those foundations in place. You will install Node.js for your app, create an Azure DevOps organization for your pipeline, and set up a Snyk account for security scanning.
In this step, get ready to:
- Install Node.js and verify Git on your machine.
- Create an Azure DevOps organization and project.
- Create a Snyk account and copy your API token.
Install Node.js and verify Git
Node.js is the JavaScript runtime your application runs on. It comes bundled with npm, the package manager you will use to install dependencies. Git tracks your code changes and pushes them to your remote repository.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Confirm Node.js is installed by running this command:
node --version
I see a version number
Node.js is ready to go. Continue to the next check below.
Command not found
- Open your browser and navigate to https://nodejs.org.
- Download the LTS version (the button on the left).
- Open the downloaded .pkg file and follow the installer prompts.
- After installation finishes, close and reopen Terminal.
- Run node --version again. You should now see a version number like v20.x.x.
- Also confirm npm is installed by running this command:
npm --version
You should see a version number printed. npm is bundled with Node.js, so if Node.js is installed, npm is too.
- Confirm Git is installed by running this command:
git --version
I see a version number
Git is ready. You have all three tools confirmed.
It's not installed yet
macOS may prompt you to install Xcode Command Line Tools when you first run a git command.
- If a dialog pops up, click Install and wait for the download to finish.
- If no dialog appears, install the tools manually by running this command:
xcode-select --install
- After installation completes, run git --version again. You should see a version number like 2.39.x.
Create your Azure DevOps organization and project
Azure DevOps is the platform that hosts your pipeline, your Git repository, and your build agents. You need an organization (the top-level container) and a project inside it.
- Open your browser and navigate to https://dev.azure.com.
- Click Start free.
- Sign in with your Microsoft account or GitHub account. If you do not have one, create a free Microsoft account when prompted.
What is an Azure DevOps organization?
An organization is the top-level container that groups related projects together. It handles billing, security, and administrative boundaries. Every project you create lives inside an organization.
- If this is your first time, click Create new organization. If you already have an organization, click New organization.
- Review the Terms of Service and click Continue.
- Enter a name for your organization. Pick any name you like. If the name is already taken, add a few numbers to make it unique.
- Choose a location near you for hosting your projects.
- Complete the captcha and click Continue.
Azure DevOps now prompts you to create your first project.
- Enter secure-pipeline-demo as the project name.
- Set the visibility to Private.
- Expand Advanced and set Version control to Git.
- Click Create.
You should see the project welcome page for secure-pipeline-demo.
Don't see the New project button?
You need to be an organization owner or have the "Create new projects" permission. If you just created the organization, you are automatically the owner and should see this button. Try refreshing the page.
Help me troubleshoot why I can't create a project in Azure DevOps.
Create your Snyk account and copy your API token
Snyk is a security scanning tool that detects known vulnerabilities in your dependencies and source code. You need an account and an API token so your pipeline can authenticate with Snyk later.
- Open your browser and navigate to https://app.snyk.io/login.
- Click Sign up with GitHub or Sign up with Google. Follow the prompts to create your free account.
Once you are logged in, you need to grab your API token.
- Click your profile avatar in the bottom left corner of the Snyk dashboard.
- Select Account settings.
- Scroll down to the Auth Token section.
- Click click to show to reveal your token, then select and copy it.
- Paste the token somewhere safe, like a temporary text file or a password manager. You will need it in a later step.
Keep your Snyk token private
Your API token grants access to your Snyk account. Do not share it publicly or commit it to a Git repository. If you suspect it has been leaked, navigate to app.snyk.io/account and click Revoke & Regenerate to create a new one.
Can't find your API token?
Navigate directly to https://app.snyk.io/account in your browser. The Auth Token section is near the bottom of the General Account Settings page.
Help me find my Snyk API token in the account settings.
Your tools and accounts are ready. Next up, you will set up a self-hosted build agent on your Mac so Azure DevOps has a machine to run your pipeline on.
Set Up a Self-Hosted Build Agent on Your Mac
Your tools and accounts are ready to go. Now you need something to actually run your pipeline.
Azure DevOps doesn't execute pipeline jobs on its own. It needs a build agent, which is a machine that picks up jobs, runs the commands, and reports results back. You'll set up a self-hosted agent right on your Mac, turning your computer into a pipeline worker.
Why a self-hosted agent instead of Microsoft-hosted?
Azure DevOps offers Microsoft-hosted agents in the cloud, but since April 2023, new organizations need to submit a request and wait several business days before those agents are enabled. Self-hosted agents bypass this entirely.
Every Azure DevOps organization gets 1 free self-hosted parallel job with unlimited minutes, no approval needed. This also mirrors real-world hybrid infrastructure, where companies connect on-premises machines to cloud DevOps platforms.
In this step, get ready to:
- Create a Personal Access Token for agent authentication.
- Download and configure the Azure DevOps build agent.
- Start the agent and verify it connects to Azure DevOps.
Create a Personal Access Token (PAT)
Your self-hosted agent needs permission to communicate with Azure DevOps. A Personal Access Token (PAT) acts as a password that grants the agent access to your organization's agent pool.
- In Azure DevOps, click the User settings icon in the top right corner (it looks like a person silhouette with a gear).
- Select Personal access tokens from the dropdown.
- Click New Token.
- Enter agent-token in the Name field.
- Set the Expiration to 30 days.
- Under Scopes, select Custom defined.
- Scroll down to find Agent Pools and check the Read & manage box.
- Click Create.
- Copy the generated token immediately and paste it somewhere safe (like a text file).
Copy your token now
Azure DevOps only shows the PAT value once. After you close this dialog, you will never be able to view it again. If you lose it, you'll have to create a new token.
Can't find the User settings icon?
Make sure you are signed in to Azure DevOps at https://dev.azure.com. The User settings icon is at the very top right of the page, next to your profile picture.
Help me find the User settings icon in Azure DevOps.
Download and configure the agent
Azure DevOps provides an agent package you download and install on your machine. Once extracted, you'll register the agent with your organization using the PAT you just created.
- In Azure DevOps, click Organization settings (gear icon in the bottom left corner of the sidebar).
- Under the Pipelines section, select Agent pools.
- Click the Default pool.
- Click New agent.
- Select the macOS tab.
- Click Download to save the agent package to your Downloads folder.
The agent package is now in your Downloads folder. You need to create a home for it and extract its files.
- Open Terminal by pressing Cmd+Space to open Spotlight, then typing Terminal and pressing Enter.
- Create a directory for the agent and extract the downloaded package by running these commands:
mkdir ~/myagent && cd ~/myagent
xattr -c ~/Downloads/vsts-agent-osx-*.tar.gz
tar xzf ~/Downloads/vsts-agent-osx-*.tar.gz
What do these commands do?
- mkdir ~/myagent && cd ~/myagent creates a folder called myagent in your home directory and moves into it.
- xattr -c clears the macOS quarantine flag on the downloaded file. Without this, macOS may block the agent scripts from running.
- tar xzf extracts the compressed agent package into the current folder.
- Confirm the agent files were extracted by running:
ls
You should see files including config.sh and run.sh in the output.
Not seeing any files after extraction?
- Make sure the agent download finished completely before you ran the extraction command. Check your Downloads folder for a file named vsts-agent-osx-*.tar.gz.
- If you see a "not found" error, confirm you ran the xattr -c command before the tar command. macOS quarantine flags can prevent extraction.
- Make sure you are inside the ~/myagent directory. Run pwd to confirm your current location.
Help me troubleshoot agent extraction issues on macOS.
Now you need to register this agent with your Azure DevOps organization. The configuration script is interactive and asks a series of questions.
- Start the agent configuration wizard by running:
./config.sh
What prompts will you see?
The script asks several questions. Here is what to enter at each prompt:
- For Server URL, enter your Azure DevOps organization URL:
https://dev.azure.com/[[AZURE_ORG="your-organization-name"]]
- For Authentication type, press Enter to accept the default (PAT).
- For Personal access token, paste the token you copied earlier.
- For Agent pool, press Enter to accept the default (Default).
- For Agent name, press Enter to accept the default (your computer's name).
- For Work folder, press Enter to accept the default (_work).
After the last prompt, you should see a message confirming that the agent settings were saved successfully.
Seeing an authentication error?
- Double check that your server URL matches the format https://dev.azure.com/your-org-name with no trailing slash. You can find your organization name in the Azure DevOps URL bar.
- Make sure you selected the Agent Pools (Read & manage) scope when creating your PAT. If you missed it, create a new token with the correct scope.
- Confirm you pasted the full PAT with no extra spaces. Tokens are long strings that start with letters and numbers.
Help me fix my Azure DevOps agent configuration error.
Start the agent
The agent is configured but not running yet. You need to start it so it begins listening for pipeline jobs from Azure DevOps.
- Start the agent interactively by running:
./run.sh
You should see output that ends with Listening for Jobs. This means your agent is connected to Azure DevOps and ready to pick up pipeline work.
Keep this Terminal window open
The agent runs in this Terminal window for the rest of the project. If you close it or press Ctrl+C, the agent stops and your pipeline jobs will have no machine to run on. When you need to run other commands later, open a new Terminal window.
Agent not connecting?
- Make sure you are running ./run.sh from inside the ~/myagent directory. Run cd ~/myagent first if you're unsure.
- If you see a permissions error, run chmod +x run.sh to make the script executable, then try again.
- Confirm you completed the ./config.sh step successfully first. The agent cannot start without a valid configuration.
Help me get my self-hosted agent running.
- Verify the agent appears in Azure DevOps by going back to Organization settings, then Agent pools, then the Default pool.
- Click the Agents tab.
You should see your agent listed with a green Online status. This confirms your Mac is connected to Azure DevOps and ready to run pipeline jobs.
Your Mac is now a fully registered build agent. Next up, you'll create a Node.js application and push it to Azure Repos so the pipeline has something to build and scan.
Create a Node.js App and Push It to Azure Repos
Your self-hosted agent is up and running, listening for pipeline jobs. Now it needs actual code to build, test, and scan.
In this step, you'll create a simple Node.js application with an intentionally vulnerable dependency. Then you'll push it to Azure Repos so your pipeline has something to work with.
In this step, get ready to:
- Initialize a Node.js project with an intentionally vulnerable dependency.
- Create application and test files for the pipeline to build and test.
- Push the code to your Azure Repos repository.
Initialize the Node.js project
Your self-hosted agent is running in your current Terminal window. You'll need a separate window to work in.
- Open a new Terminal window by pressing Cmd+N in Terminal.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and initialize a Node.js project inside it by running these commands:
mkdir secure-app && cd secure-app
npm init -y
What do these commands do?
- mkdir secure-app creates a new folder called secure-app and cd secure-app moves you into it.
- npm init -y creates a package.json file with default values. This file tracks your project's dependencies and scripts.
- You should see a package.json preview printed in the terminal output.
Now you need a web framework for the app. You'll install Express, but specifically an older version that has known security vulnerabilities. This is intentional.
- Install express@4.17.1 by running:
npm install express@4.17.1
Why install an old version of Express?
Express 4.17.1 has known vulnerabilities that Snyk will detect. When you run the pipeline later, Snyk's security scan will flag these vulnerabilities and fail the build. That's exactly what you want to prove your security gate works.
- Confirm the install succeeded by running:
ls
- You should see node_modules, package.json, and package-lock.json in the output.
Seeing 'npm: command not found'?
This means Node.js is not installed or not on your system PATH. Go back to Step 1 and make sure you installed Node.js and verified it with node --version.
Help me troubleshoot my npm installation
Create the application files
Your project needs three files: a package.json with the right scripts, an application file, and a test file. These give the pipeline something to build and test.
The npm init -y command created a package.json with placeholder values. You need to update it with the correct scripts for starting and testing your app.
- Open package.json in a text editor and replace its entire contents with the following:
{
"name": "secure-app",
"version": "1.0.0",
"description": "A simple Node.js app for testing secure CI/CD pipelines",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "node index.test.js"
},
"dependencies": {
"express": "4.17.1"
}
}
What does this file do?
- The "start" script runs your app with node index.js.
- The "test" script runs your test file with node index.test.js. The pipeline will use this script to verify your app works.
- The "dependencies" section pins Express to 4.17.1, which is the vulnerable version Snyk will flag.
- Save package.json.
Next, you need the main application file. This is a basic Express web server with two routes.
- Create a new file called index.js inside your secure-app folder and add the following content:
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
// Root route returns a welcome message
app.get('/', (req, res) => {
res.json({ message: 'Hello from the secure pipeline demo!' });
});
// Health check route for monitoring
app.get('/health', (req, res) => {
res.json({ status: 'healthy' });
});
// Only start the server if this file is run directly
if (require.main === module) {
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
}
module.exports = app;
What does this code do?
- The / route returns a JSON welcome message.
- The /health route returns a health status. This is a common pattern for production apps.
- The if (require.main === module) check ensures the server only starts when you run the file directly. This prevents the server from starting during tests.
- module.exports = app exports the app so the test file can import it.
- Save index.js.
- Verify the server starts by running:
node index.js
- You should see Server running on port 3000 in the terminal output.
- Press Ctrl+C to stop the server.
Finally, you need a test file. The pipeline will run this test to verify the app is working before it moves on to security scanning.
- Create a new file called index.test.js inside your secure-app folder and add the following content:
const app = require('./index');
// Simple assertion helper
function assert(condition, message) {
if (!condition) {
console.error('FAIL:', message);
process.exit(1);
}
console.log('PASS:', message);
}
// Test that the app exports an Express application
assert(typeof app === 'function', 'App exports an Express application');
// Test that Express loads correctly
const express = require('express');
assert(typeof express === 'function', 'Express module loads successfully');
console.log('All tests passed!');
What does this test do?
- The assert function checks a condition and exits with an error code if it fails. The pipeline uses this exit code to detect test failures.
- The first test checks that index.js exports a valid Express app.
- The second test checks that the Express module loads without errors.
- Save index.test.js.
- Run the tests by running:
npm test
- You should see PASS: App exports an Express application, PASS: Express module loads successfully, and All tests passed! in the output.
Seeing 'Cannot find module' errors?
- Make sure both index.js and index.test.js are in the same secure-app folder.
- Check that your node_modules folder exists. If it does not, run npm install again.
- Verify the "test" script in package.json says node index.test.js, not the default echo "Error: no test specified".
Help me debug my test file
Awesome, I've got everything!
Great. Double check you've saved all three files before moving on.
I'd like to double check the full code
Here is each file as it should look right now.
{
"name": "secure-app",
"version": "1.0.0",
"description": "A simple Node.js app for testing secure CI/CD pipelines",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "node index.test.js"
},
"dependencies": {
"express": "4.17.1"
}
}
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.json({ message: 'Hello from the secure pipeline demo!' });
});
app.get('/health', (req, res) => {
res.json({ status: 'healthy' });
});
if (require.main === module) {
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
}
module.exports = app;
const app = require('./index');
function assert(condition, message) {
if (!condition) {
console.error('FAIL:', message);
process.exit(1);
}
console.log('PASS:', message);
}
assert(typeof app === 'function', 'App exports an Express application');
const express = require('express');
assert(typeof express === 'function', 'Express module loads successfully');
console.log('All tests passed!');
Push to Azure Repos
Your app files are ready locally. Now you need to push them to Azure Repos so the pipeline can access them.
- Initialize a Git repository and make your first commit by running these commands:
git init
git branch -M main
git add .
git commit -m "Initial commit with Express app"
What do these commands do?
- git init creates a new Git repository in your secure-app folder.
- git branch -M main renames your default branch to main. Azure Repos expects this branch name.
- git add . stages all your files for the commit.
- git commit saves a snapshot of your staged files.
- You should see a summary listing the files that were committed, including index.js, index.test.js, and package.json.
Now you need to connect your local repository to Azure Repos. You can find your repository URL in the Azure DevOps web portal.
- In your browser, go to your Azure DevOps project at dev.azure.com.
- Click Repos in the left sidebar.
- On the Repos page, look for the Push an existing repository from command line section. You will see a git remote add origin command with your repository URL.
- Add the Azure Repos remote and push your code by running these commands:
git remote add origin https://dev.azure.com/[[AZURE_ORG="your-org-name"]]/secure-pipeline-demo/_git/secure-pipeline-demo
git push -u origin main
What does git push do?
git remote add origin connects your local repository to the Azure Repos remote. git push -u origin main uploads your committed files to the main branch on Azure Repos.
You may be prompted to sign in
When you run git push, a browser window or credential prompt may appear asking you to sign in with your Microsoft account. This is normal. Sign in with the same account you used for Azure DevOps.
- After the push completes, refresh the Repos page in Azure DevOps.
- You should see your files listed: index.js, index.test.js, package.json, and package-lock.json.
Push failed or seeing an error?
- If you see remote: TF401019, double check that the organization name and project name in your remote URL match exactly what you created in Azure DevOps.
- If authentication fails, make sure you are signing in with the same Microsoft or GitHub account you used to create the Azure DevOps organization.
- If you see error: src refspec main does not match any, make sure you ran git branch -M main and git commit before pushing.
Help me troubleshoot my git push to Azure Repos
Your code is now in Azure Repos and ready for automation. Next up, you'll build a YAML pipeline that automatically builds, tests, and security-scans this app every time you push a change.
Build the YAML Pipeline with Snyk Security Scanning
Your Node.js app is live in Azure Repos, complete with an intentionally vulnerable dependency.
Now it's time to build the pipeline that catches those vulnerabilities automatically. You'll define a YAML-based CI/CD pipeline that installs dependencies, runs tests, and scans your code with Snyk. If Snyk finds vulnerabilities, it fails the build.
In this step, get ready to:
- Install the Snyk extension and create a service connection in Azure DevOps.
- Write a YAML pipeline file that builds, tests, and security-scans your app.
- Run the pipeline and watch Snyk catch vulnerable dependencies.
Install the Snyk extension and create a service connection
Before Snyk can scan your code inside a pipeline, Azure DevOps needs two things: the Snyk extension installed in your organization, and a service connection that securely stores your Snyk API token.
- In Azure DevOps, click Organization settings in the bottom-left corner of the sidebar.
- Select Extensions from the left menu.
- Click Browse Marketplace in the top-right area.
- Search for Snyk Security Scan in the Marketplace search bar.
- Click the Snyk Security Scan result, then click Get it free.
- Select your organization from the dropdown and click Install.
The Snyk extension is now installed. Next, create the service connection that links your Snyk account to this Azure DevOps project.
- Navigate back to your secure-pipeline-demo project in Azure DevOps.
- Click Project settings in the bottom-left corner of the sidebar.
- Select Service connections under the Pipelines section in the left menu.
- Click Create service connection.
- Scroll down and select Snyk Auth as the connection type, then click Next.
- Paste the Snyk API token you copied in Step 1 into the Server URL or Snyk API Token field.
- Enter SnykAuth as the Service connection name.
- Click Save.
What is a service connection?
A service connection is how Azure DevOps securely connects to external services like Snyk, AWS, or Docker Hub. It stores credentials (in this case, your Snyk API token) so that pipeline tasks can authenticate without exposing secrets in your YAML file.
Why does the name matter?
The name SnykAuth must match the serviceConnectionEndpoint value you'll use in the pipeline YAML file. If these don't match exactly, the Snyk task won't be able to authenticate.
Don't see Snyk Auth as an option?
Make sure you installed the Snyk Security Scan extension to your organization in the previous steps. The Snyk Auth connection type only appears after the extension is installed.
Help me set up my Snyk service connection in Azure DevOps
Create the pipeline YAML file
The pipeline file tells Azure DevOps exactly what to do every time code is pushed. Your pipeline will install dependencies, run tests, and then scan for security vulnerabilities using Snyk.
- Create a new file called azure-pipelines.yml in the root of your secure-app folder (the same folder that contains index.js and package.json) and paste the following code:
# Run this pipeline whenever code is pushed to main
trigger:
- main
# Use the self-hosted agent in the Default pool
pool:
name: Default
steps:
# Ensure Node.js 18 is available on the agent
- task: NodeTool@0
inputs:
versionSpec: '18.x'
displayName: 'Install Node.js'
# Install the app's npm packages
- script: |
npm install
displayName: 'Install dependencies'
# Run the project's test suite
- script: |
npm test
displayName: 'Run tests'
# Scan dependencies for known vulnerabilities
- task: SnykSecurityScan@1
inputs:
serviceConnectionEndpoint: 'SnykAuth'
testType: 'app'
monitorWhen: 'always'
failOnIssues: true
displayName: 'Snyk Security Scan'
What does this pipeline do?
- trigger: - main tells Azure DevOps to run this pipeline automatically whenever code is pushed to the main branch.
- pool: name: Default targets the self-hosted agent you set up in Step 2 instead of a Microsoft-hosted agent.
- NodeTool@0 installs Node.js 18.x on the agent so npm commands and the Snyk task can run.
- The two script steps install your project's dependencies and run the test suite.
- SnykSecurityScan@1 performs a Software Composition Analysis (SCA) scan. It checks your package.json dependencies against Snyk's vulnerability database.
- failOnIssues: true is the security gate. If Snyk finds any known vulnerabilities, the entire pipeline fails.
- Save azure-pipelines.yml.
- Confirm the file exists in your secure-app folder by running:
ls azure-pipelines.yml
You should see azure-pipelines.yml in the output.
File not found?
Make sure you saved the file in the root of your secure-app folder, not inside a subfolder like node_modules. Run pwd to check your current directory. It should end with /secure-app.
Also check the filename is exactly azure-pipelines.yml (not .yaml or with extra spaces). Azure DevOps looks for this exact filename.
Help me find my azure-pipelines.yml file
Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
I'd like to double check the full code
trigger:
- main
pool:
name: Default
steps:
- task: NodeTool@0
inputs:
versionSpec: '18.x'
displayName: 'Install Node.js'
- script: |
npm install
displayName: 'Install dependencies'
- script: |
npm test
displayName: 'Run tests'
- task: SnykSecurityScan@1
inputs:
serviceConnectionEndpoint: 'SnykAuth'
testType: 'app'
monitorWhen: 'always'
failOnIssues: true
displayName: 'Snyk Security Scan'
Create and run the pipeline
The pipeline YAML file needs to be in your Azure Repos repository before Azure DevOps can use it. Push the file, then point Azure DevOps at it.
- Commit and push the pipeline file by running these commands:
git add azure-pipelines.yml
git commit -m "Add CI/CD pipeline with Snyk security scanning"
git push
Push rejected or authentication error?
If git push fails, Azure DevOps may prompt you to authenticate. Follow the prompts in your browser or terminal to sign in with the same account you used for Azure DevOps.
Help me push my code to Azure Repos
Now create the pipeline in Azure DevOps so it knows to run your YAML file.
- In the left sidebar of your Azure DevOps project, click Pipelines.
- Click Create Pipeline (or New pipeline if you've visited this page before).
- Select Azure Repos Git as your code source.
- Select your secure-pipeline-demo repository.
- Choose Existing Azure Pipelines YAML file.
- In the popup, leave the branch as main and select /azure-pipelines.yml as the path.
- Click Continue.
Azure DevOps displays a review of your YAML file. This is your chance to verify it looks correct before running.
- Click Run in the top-right corner to start the pipeline.
What about pipeline permissions?
Azure DevOps may ask you to permit the pipeline to use your SnykAuth service connection. If you see a permissions banner at the top of the pipeline run page, click Permit to allow it. This is a one-time authorization.
The pipeline picks up the job on your self-hosted agent. You can watch each step execute in real time by clicking on the job.
- Click on the running job to view the live logs.
- Watch the Install Node.js, Install dependencies, and Run tests steps complete successfully.
- Watch the Snyk Security Scan step. It should fail with a red error indicator.
The pipeline fails because Snyk detected known vulnerabilities in the outdated Express 4.17.1 dependency you installed in Step 3. This is exactly what you want. The security gate is working.
Why is a failed pipeline a good thing?
You intentionally installed a vulnerable version of Express to test the security gate. The pipeline's job is to catch exactly this kind of risk. failOnIssues: true in your YAML file is what makes the pipeline fail instead of just logging a warning.
In a real production environment, this prevents vulnerable code from reaching your users.
Pipeline stuck on queued or not starting?
- Check that your agent is still running. Switch to the Terminal window where you ran ./run.sh in Step 2. You should see Listening for Jobs. If the agent stopped, run ./run.sh again.
- Verify the pipeline uses pool: name: Default in your YAML file. If you used vmImage: 'ubuntu-latest', the pipeline tries to use a Microsoft-hosted agent, which requires a separate parallelism grant.
- Check if Azure DevOps is waiting for permission. Look for a yellow banner at the top of the pipeline run page asking you to permit the service connection.
Help me get my Azure DevOps pipeline to start running
Snyk scan passed instead of failing?
Double check that your package.json lists express at version 4.17.1 specifically. If you installed a newer version, Snyk may not find vulnerabilities. Also confirm failOnIssues: true is set in your YAML file.
Help me understand why my Snyk scan did not detect vulnerabilities
Your security-scanning pipeline is live and catching vulnerabilities. Next up, you'll lock down the main branch with a policy that forces every pull request to pass this pipeline before merging.
Enforce Security with Branch Policies
Your pipeline detected vulnerabilities in the last step. That proves Snyk is scanning your code and catching insecure dependencies. But there is a gap.
Nothing stops a developer from pushing code directly to main and skipping the pipeline entirely. Branch policies fix this by requiring every change to pass through a pull request that automatically triggers the pipeline.
In this step, get ready to:
- Create a feature branch for making changes.
- Configure a build validation policy on the main branch.
- Test the policy by submitting a pull request that gets blocked by the security gate.
Create a feature branch
In a real team, developers never commit directly to main. They create a separate branch, make their changes, and open a pull request. The branch policy you set up next will enforce this workflow automatically.
- In the Terminal window where you have been working in your secure-app project, create a new branch by running this command:
git checkout -b feature/update-deps
You should see Switched to a new branch 'feature/update-deps' in your Terminal output.
Seeing an error about your current directory?
Make sure you are inside the ~/secure-app folder. Run cd ~/secure-app first, then try the git checkout command again.
Help me fix this git checkout error
Configure a build validation policy on main
A build validation policy tells Azure DevOps to automatically run your pipeline whenever someone opens a pull request targeting a protected branch. If the pipeline fails, the pull request cannot be merged.
Why branch policies instead of YAML triggers?
You might expect to add a pr: trigger in your YAML file. However, YAML-based PR triggers are not supported for Azure Repos Git repositories. Branch policies with build validation are the correct approach for Azure Repos.
- In Azure DevOps, click Repos in the left sidebar.
- Click Branches.
- Find the main branch in the list.
- Click the ellipsis (...) next to main.
- Select Branch policies.
- Scroll down to the Build validation section.
- Click the + button to add a new build policy.
- In the Build pipeline dropdown, select your pipeline (it matches your repository name).
- Set Trigger to Automatic.
- Set Policy requirement to Required.
- Enter Security Pipeline in the Display name field.
- Click Save.
You should see the Security Pipeline listed under Build validation with the trigger set to Automatic and the policy set to Required.
What does this policy do?
- Automatic trigger means the pipeline runs every time a pull request targets main. No one needs to remember to start it manually.
- Required policy means the pull request is physically blocked from merging if the pipeline fails. The merge button stays greyed out until the pipeline passes.
Don't see the + button under Build validation?
Make sure you clicked the ellipsis on the main branch specifically, not on a different branch. Also confirm your pipeline ran successfully at least once in the previous step. Azure DevOps needs to know the pipeline exists before it can appear in the dropdown.
Help me find the build validation settings in Azure DevOps
Test the policy with a pull request
The branch policy is configured, but you have not seen it in action yet. Time to make a change on your feature branch, push it, and open a pull request to see the security gate block the merge.
- Create a simple file change and push it to your feature branch by running these commands:
echo "# Secure Pipeline Demo" > README.md
git add README.md
git commit -m "Add README"
git push -u origin feature/update-deps
What do these commands do?
- echo creates a new README.md file with a heading. This gives you a change to commit.
- git add stages the file, git commit saves it to your local branch history.
- git push uploads your feature/update-deps branch to Azure Repos so you can create a pull request from it.
- You should see output ending with * [new branch] feature/update-deps -> feature/update-deps confirming the push succeeded.
Push rejected or authentication error?
- If you see an authentication prompt, enter your Azure DevOps credentials. You may need to use a Personal Access Token (PAT) as the password instead of your account password.
- If the push is rejected, make sure you are on the feature/update-deps branch by running git branch. The active branch has an asterisk next to it.
Help me fix this git push error
Now create a pull request in Azure DevOps to trigger the branch policy.
- In Azure DevOps, click Repos in the left sidebar.
- Click Pull requests.
- Click New pull request in the top right.
- Set the source branch to feature/update-deps.
- Set the target branch to main.
- Click Create.
As soon as the pull request is created, you should see the Security Pipeline check appear in the pull request overview. The pipeline starts running automatically.
Wait for the pipeline to finish. When it completes, the Snyk Security Scan step fails because it detects vulnerabilities in the outdated express@4.17.1 dependency. The merge button stays greyed out. This is the security gate in action.
Why is this powerful?
Without this policy, a developer could push vulnerable code to main and nobody would know until production breaks. With the branch policy, every single change must pass through the security pipeline first. No exceptions, no shortcuts.
This pattern is a core part of the Secure Software Development Lifecycle (SSDLC). Security is not a manual review at the end. It is an automated gate built directly into the delivery pipeline.
Pipeline not starting automatically?
- Check that your self-hosted agent is still running. Switch to the Terminal window running ./run.sh and confirm it still says Listening for Jobs. If it stopped, run ./run.sh again.
- Verify the build validation policy is saved. Go back to Repos then Branches and check that main shows a policy icon next to it.
- Make sure the pull request targets the main branch. The policy only triggers for pull requests targeting the branch you configured it on.
Help me figure out why my build validation policy is not triggering
Secret mission
Add Snyk Infrastructure-as-Code (IaC) Scanning
Your pipeline catches vulnerable dependencies and insecure application code. But what about the infrastructure your application runs on? In this secret mission, you will extend your pipeline with a Snyk IaC scan that checks a Terraform configuration file for security misconfigurations like missing encryption, disabled versioning, and unrestricted public access.
Before you go
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses Azure DevOps and Snyk free tiers with a self-hosted agent running locally, so there are no ongoing cloud costs. The only local resource actively running is the build agent process in your Terminal.
Resources you used:
- Azure DevOps organization and project (secure-pipeline-demo).
- Self-hosted build agent running in Terminal (~/myagent).
- Node.js application directory (~/secure-app).
- CI/CD pipeline and branch policy in Azure DevOps.
- Snyk account and service connection (SnykAuth).
- Personal Access Token (PAT) in Azure DevOps.
Keep everything running
No action needed. Choose this if you are still actively building on the pipeline or plan to extend it soon.
- Your Azure DevOps organization, project, and pipeline stay active on the free tier at no cost.
- Your Snyk account remains on the free tier with 200 SCA tests and 100 SAST tests per month.
- Keep the Terminal window with ./run.sh open so the build agent can pick up pipeline jobs.
Pause - I'll come back to this later
Shut down the running agent process to free up local resources. Your Azure DevOps project and Snyk account will stay intact for when you return.
- Go to the Terminal window running the build agent and press Ctrl + C to stop the agent process.
- Your Azure DevOps organization, project, pipeline, and branch policies remain saved. No data is lost.
- Your local project files in ~/secure-app and agent files in ~/myagent remain on disk.
- To resume later, run ./run.sh from the ~/myagent directory to restart the agent.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow these steps to clean up everything.
- Stop the build agent by pressing Ctrl + C in the Terminal window running ./run.sh.
- Remove the agent registration by running ./config.sh remove from the ~/myagent directory. Enter your PAT when prompted.
- Delete the local directories by running rm -rf ~/myagent ~/secure-app in Terminal.
- Revoke your Personal Access Token in Azure DevOps under User settings, then Personal access tokens. Select your token and click Revoke.
- Delete the Azure DevOps project by going to Project Settings, scrolling to the bottom, and clicking Delete. Type the project name to confirm.
- Optionally delete your Azure DevOps organization under Organization Settings, then Overview, then click Delete organization.
- Optionally delete your Snyk account by going to Settings in Snyk and selecting the option to delete your account.
Mission accomplished
That's a Wrap!
You just built a secure CI/CD pipeline from scratch using Azure DevOps, a self-hosted build agent, and Snyk security scanning. This is exactly the kind of hands-on DevOps and security integration that teams rely on to ship safe code to production.
Here's what you learned:
- Configured a self-hosted build agent on your Mac, connecting local infrastructure to a cloud-hosted Azure DevOps platform.
- Built a YAML-based CI/CD pipeline with Snyk security scanning that automatically catches vulnerable dependencies before they reach production.
- Enforced branch policies with build validation to block insecure code from merging into your main branch.
- Extended the pipeline with Infrastructure-as-Code scanning to catch Terraform misconfigurations before deployment.
Ready to quiz yourself?