Build a DevSecOps Security Pipeline
Automate security scanning for secrets, IaC, and containers in CI/CD.
Introduction
30 Second Summary
Imagine shipping a product with the doors left unlocked. That is what happens when code reaches production without a security check. In the real world, security vulnerabilities in code cost companies millions of dollars and erode customer trust overnight.
In this project, you will build an automated security pipeline that catches leaked secrets, insecure configurations, and container vulnerabilities before code can ever reach production. You will use four industry-standard open-source scanning tools, all running inside GitHub Actions.
What You'll Build
A fully automated DevSecOps security pipeline in GitHub Actions that scans a deliberately vulnerable sample project using four parallel security scanning jobs.
By the end of this project, you'll have:
- A GitHub repository with intentionally vulnerable infrastructure files and a multi-stage CI/CD workflow.
- Hands-on experience with four shift-left security tools: Gitleaks for secret scanning, Checkov for infrastructure security, ansible-lint for playbook best practices, and Trivy for container vulnerability scanning.
- A working security gate that blocks insecure code from merging into your main branch.
- Secret Mission: fix every vulnerability the pipeline finds and turn all four jobs green.
Are there any prerequisites?
You need a free GitHub account and basic comfort editing text files. No prior CI/CD, Docker, or Ansible experience is required. All scanning tools run on GitHub's cloud runners, so you do not need to install anything beyond Git on your local machine.
Set Up Your Repository
Every security pipeline needs a home. Your GitHub repository will hold both the files being scanned and the pipeline that scans them.
That pipeline runs through GitHub Actions, which triggers your security scans automatically every time you push code. In this step, you'll create the repository and prepare its folder structure.
In this step, get ready to:
- Create a public GitHub repository for the project.
- Clone the repository to your Desktop.
- Set up the ansible/, docker/, and .github/workflows/ directories.
Create a new GitHub repository
- Open your browser and navigate to github.com.
- Sign in to your account if you haven't already.
- Click the + icon in the top-right corner of the page.
- Select New repository from the dropdown.
- Enter devsecops-pipeline in the Repository name field.
- Select Public as the visibility.
- Check the Add a README file checkbox.
- Click Create repository.
GitHub redirects you to your new repository page. You should see a single README.md file listed.
Why a public repository?
GitHub Actions is completely free for public repositories. Private repositories get 2,000 free minutes per month on the free tier.
Since this is a learning project, keeping it public means you can run as many pipeline builds as you need at no cost.
Clone the repository to your machine
Git lets you download a full copy of your repository to work on locally. This process is called cloning.
- On your repository page in GitHub, click the green <> Code button.
- Make sure HTTPS is selected.
- Click the copy icon next to the URL to copy it to your clipboard.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Clone your repository by running this command:
git clone [[YOUR_REPO_URL="https://github.com/your-username/devsecops-pipeline.git"]]
- Confirm the repository was cloned by running:
ls
You should see a devsecops-pipeline folder in the output.
Not seeing the folder?
- Double-check that you pasted the correct URL from GitHub. The URL should end in .git.
- Make sure you are connected to the internet. The git clone command needs network access to download the repository.
- Confirm you ran cd ~/Desktop first. Without it, the folder may have been cloned to a different location.
Help me troubleshoot my git clone command.
Create the project folder structure
Your pipeline needs a specific folder structure. The ansible/ and docker/ directories will hold the intentionally vulnerable files you create in the next step.
- Move into your new repository folder by running this command:
cd devsecops-pipeline
- Create all three project directories by running these commands:
macOS
mkdir -p ansible docker .github/workflows
Windows
mkdir ansible
mkdir docker
mkdir .github\workflows
What are these directories for?
- ansible/ will hold an Ansible playbook and a secrets file.
- docker/ will hold a Dockerfile.
- .github/workflows/ is where GitHub Actions looks for workflow definitions. Your security pipeline will live here.
- Confirm the directories were created by running:
ls -a
You should see ansible, docker, and .github listed in the output.
Missing a directory?
- Make sure you ran the mkdir command from inside the devsecops-pipeline folder. Run pwd to check your current location.
- If you don't see .github, make sure you used the -a flag with ls. Directories starting with a dot are hidden by default on macOS.
- Windows PowerShell users can run ls without the -a flag. PowerShell shows dot-prefixed directories by default.
Help me fix my project directory structure.
Your repository is set up and your folder structure is in place. Next up, you'll create the intentionally vulnerable files that the security pipeline will scan.
Create the Vulnerable Files
Your repository is cloned and the folder structure is ready. Now it's time to create some intentionally vulnerable files.
These files mimic the kind of security mistakes that commonly slip into production codebases. Each vulnerability maps to a specific scanning tool in the pipeline you'll build next.
In this step, get ready to:
- Create an insecure Ansible playbook with disabled certificate validation.
- Plant a fake AWS credential to trigger secret scanning.
- Create a misconfigured Dockerfile with common security issues.
Create the insecure Ansible playbook and fake secret
Ansible is a tool that automates server configuration. Teams write YAML files called playbooks to define tasks like installing software or downloading files.
Your playbook will contain three tasks that each have a deliberate security flaw. They disable SSL certificate validation and use HTTP instead of HTTPS.
- Inside the ansible/ folder of your devsecops-pipeline repository, create a new file called site.yaml.
- Paste the following code into site.yaml:
---
- name: Configure web server
hosts: all
gather_facts: false
tasks:
- name: Download application package
ansible.builtin.get_url:
url: http://example.com/app/package.tar.gz
dest: /tmp/package.tar.gz
validate_certs: false
- name: Install httpd
ansible.builtin.yum:
name: httpd
state: present
validate_certs: false
- name: Check application health
ansible.builtin.uri:
url: http://example.com/health
validate_certs: false
What does this playbook do?
This playbook defines three tasks that would run on a server. Each task has a security problem that a scanning tool will catch later.
- validate_certs: false tells Ansible to skip SSL certificate checks, which makes the connection vulnerable to man-in-the-middle attacks.
- The http:// URLs use unencrypted HTTP instead of HTTPS, so data travels in plain text.
- Each task uses a different Ansible module (get_url, yum, uri), so your scanners get multiple findings to report.
- Save ansible/site.yaml.
You won't see any output yet. The next file completes the Ansible folder.
Seeing a YAML syntax error later?
- YAML is sensitive to indentation. Make sure you are using spaces, not tabs. Each nested level in the playbook uses exactly two spaces.
- Check that your file is saved as site.yaml (not site.yml or site.yaml.txt) and is inside the ansible/ folder.
- Help me fix a YAML indentation error in my Ansible playbook.
✔️ Awesome, I've got everything!
Double check you've saved ansible/site.yaml before moving on.
ⓧ I'd like to double check the full code
Here is the complete ansible/site.yaml file. Make sure yours matches exactly, including indentation.
---
- name: Configure web server
hosts: all
gather_facts: false
tasks:
- name: Download application package
ansible.builtin.get_url:
url: http://example.com/app/package.tar.gz
dest: /tmp/package.tar.gz
validate_certs: false
- name: Install httpd
ansible.builtin.yum:
name: httpd
state: present
validate_certs: false
- name: Check application health
ansible.builtin.uri:
url: http://example.com/health
validate_certs: false
Now you'll plant a fake AWS credential in the same folder. This gives your secret scanner something to find.
- Create a new file called secrets.txt inside the ansible/ folder.
- Paste the following content into secrets.txt:
# DO NOT DO THIS - This is an intentionally leaked secret for testing
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
What is this file for?
This file contains a fake AWS access key that follows the real AWS key format. The key starts with AKIA and is 20 characters long, which is exactly what real AWS keys look like.
Secret scanning tools like Gitleaks match these patterns to detect credentials that were accidentally committed. This is one of the most common security mistakes in real codebases.
- Save ansible/secrets.txt.
- Confirm both Ansible files exist by running:
ls ansible/
You should see secrets.txt and site.yaml in the output.
Create the insecure Dockerfile
Docker packages applications into containers that run identically on any machine. A Dockerfile is the recipe that defines what goes into a container.
Your Dockerfile will have three common security problems. It runs as root, uses an unpinned base image, and has no health check.
- Inside the docker/ folder, create a new file called Dockerfile (no file extension).
- Paste the following code into the Dockerfile:
FROM python:latest
COPY . /app
WORKDIR /app
RUN pip install flask
EXPOSE 5000
CMD ["python", "app.py"]
What's wrong with this Dockerfile?
- python:latest is an unpinned base image. The latest tag can change at any time, pulling in new vulnerabilities or breaking your build without warning.
- There is no USER instruction, so the container runs as root. If an attacker breaks into the container, they have full system-level access.
- There is no HEALTHCHECK instruction. Without one, container orchestrators cannot detect when the app is unhealthy and restart it.
- Save docker/Dockerfile.
- Confirm the file exists by running:
ls docker/
You should see Dockerfile in the output.
Don't see the Dockerfile?
- Make sure the file is named exactly Dockerfile with a capital D and no file extension. Names like dockerfile or Dockerfile.txt will not work.
- Check that the file is inside the docker/ folder, not in the repository root.
- Help me find my Dockerfile in the right location.
✔️ Awesome, I've got everything!
Double check you've saved docker/Dockerfile before moving on.
ⓧ I'd like to double check the full code
Here is the complete Dockerfile. Make sure yours matches exactly.
FROM python:latest
COPY . /app
WORKDIR /app
RUN pip install flask
EXPOSE 5000
CMD ["python", "app.py"]
Commit and push your files
All three vulnerable files are saved locally. Now you need to push them to GitHub so the pipeline you build later can scan them.
- Make sure your terminal is inside the devsecops-pipeline folder.
- Stage, commit, and push all your new files by running these commands:
git add .
git commit -m "Add vulnerable files for security scanning"
git push origin main
What do these commands do?
- git add . stages all new and changed files for the next commit.
- git commit -m saves a snapshot of those staged files with a descriptive message.
- git push origin main uploads your local commits to your GitHub repository on the main branch.
- Open your devsecops-pipeline repository on GitHub in your browser.
- Confirm you see the ansible/ and docker/ folders listed in the repository's file tree.
- Click into ansible/ and verify you see both site.yaml and secrets.txt.
Don't see your files on GitHub?
- Make sure you ran all three git commands (add, commit, and push). If you missed one, run them again from the top.
- Check that your terminal was inside the devsecops-pipeline folder when you ran the commands. Running git from the wrong directory will either fail or commit the wrong files.
- If you see an authentication error, GitHub may need you to set up a personal access token or SSH key. Help me fix a git push authentication error.
Your vulnerable files are live on GitHub. Next up, you'll build the first stage of your security pipeline to automatically catch the fake secret you just planted.
Add Secret Scanning with Gitleaks
Your repository now has a full set of deliberately vulnerable files. An insecure Ansible playbook, a leaked AWS key, and a misconfigured Dockerfile are all waiting to be caught.
Leaked credentials are one of the most common causes of security breaches. In this step, you'll create a GitHub Actions workflow and add Gitleaks as your first line of defense.
In this step, get ready to:
- Create a GitHub Actions workflow file for your security pipeline.
- Add a Gitleaks secret scanning job to the workflow.
- Push the workflow and review the scan results.
Create the Gitleaks workflow file
GitHub Actions workflows are YAML files that live in the .github/workflows/ directory. Each workflow defines when it runs and what jobs it executes.
- Create a new file called security-scan.yml inside your .github/workflows/ folder.
- Add the workflow name and trigger configuration by pasting this code:
---
# Display name shown in the GitHub Actions UI
name: DevSecOps Security Pipeline
# Run on pushes to main, PRs targeting main, or manual trigger
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
What does this code do?
- name: sets the display name you'll see in the GitHub Actions UI.
- The on: section defines three triggers: pushes to main, pull requests targeting main, and manual runs from the Actions tab.
- workflow_dispatch: lets you trigger the pipeline manually at any time, which is useful for testing.
You won't see a change yet. The next chunk adds the actual scanning job.
- Add the Gitleaks scanning job below the trigger configuration by pasting this code at the bottom of security-scan.yml:
jobs:
secret-scan:
name: Secret Scanning - Gitleaks
runs-on: ubuntu-latest
steps:
# Clone the full Git history for comprehensive scanning
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
# Run Gitleaks to detect leaked secrets
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
What does this code do?
- runs-on: ubuntu-latest runs the job on a GitHub-hosted Linux runner. You don't need anything installed on your own computer.
- fetch-depth: 0 clones the full Git history so Gitleaks can scan every commit for secrets, not just the latest one.
- gitleaks/gitleaks-action@v2 runs the Gitleaks scanner. For personal GitHub repositories, no license key is needed.
- GITHUB_TOKEN is automatically provided by GitHub. You don't need to create or configure any API keys.
- Save security-scan.yml.
YAML looking broken in your editor?
- YAML is sensitive to indentation. Use spaces, not tabs. Each nested level uses exactly 2 spaces.
- Make sure the jobs: line has no leading spaces. It should be at the very start of the line.
- Help me fix YAML indentation issues in my GitHub Actions workflow file.
✔️ Awesome, I've got everything!
Great. Double check you've saved security-scan.yml before moving on.
ⓧ I'd like to double check the full code
Here's the complete security-scan.yml file. Make sure yours matches exactly.
---
name: DevSecOps Security Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
secret-scan:
name: Secret Scanning - Gitleaks
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Push the workflow and check Gitleaks results
Your workflow file is ready locally. Pushing it to GitHub triggers the pipeline automatically because of the on: push trigger you just configured.
- Push your new workflow file to GitHub by running these commands:
git add .github/workflows/security-scan.yml
git commit -m "Add Gitleaks secret scanning workflow"
git push
- Open your devsecops-pipeline repository on GitHub in your browser.
- Click the Actions tab at the top of the repository page.
You should see a workflow run called DevSecOps Security Pipeline appear. It takes about 30 seconds to complete.
Don't see the workflow run?
- Make sure you pushed to the main branch. The workflow only triggers on pushes to main.
- Check that security-scan.yml is inside .github/workflows/, not somewhere else in the repo.
- Try refreshing the Actions tab. New runs can take a few seconds to appear.
- Help me troubleshoot why my GitHub Actions workflow isn't running.
Once the run finishes, it's time to read the Gitleaks output.
- Click the workflow run name (Add Gitleaks secret scanning workflow) to open it.
- Click the Secret Scanning - Gitleaks job on the left side.
- Expand the Run Gitleaks step to see the scan output.
Gitleaks found the fake AWS credentials you planted in ansible/secrets.txt. The output shows the file path, line number, and the type of secret detected.
What did Gitleaks find?
Gitleaks detected the AWS access key pattern (starting with AKIA) in ansible/secrets.txt. The output includes a fingerprint, the file path, the line number, and a rule description.
This is exactly the kind of leak that causes real-world security incidents. In a production pipeline, this finding would block the code from merging until the secret is removed.
Your pipeline just caught its first vulnerability. Next up, you'll add Checkov and ansible-lint to scan for infrastructure misconfigurations and best-practice violations.
Add Ansible and Dockerfile Scanning with Checkov and ansible-lint
Your pipeline already catches leaked secrets with Gitleaks. Secrets are just one category of security problem, though.
Your Ansible playbook and Dockerfile both contain misconfigurations that could let attackers in. In this step, you'll add two more scanning tools to your pipeline: Checkov for Infrastructure as Code security checks, and ansible-lint for Ansible best-practice enforcement.
Why two separate scanning tools?
Checkov catches security misconfigurations like disabled certificate validation and HTTP URLs. ansible-lint catches best-practice violations like syntax issues, deprecated modules, and formatting problems.
They complement each other. Real DevSecOps pipelines use layered scanning so that different tools cover different categories of risk.
In this step, get ready to:
- Add a Checkov job that scans your Ansible playbook and Dockerfile for security misconfigurations.
- Add an ansible-lint job that checks your Ansible playbook against best practices.
- Push the updated workflow and review the scan findings.
Add a Checkov job to scan infrastructure files
Checkov is an open-source static analysis tool that scans infrastructure files for security misconfigurations. It has dedicated policies for Ansible playbooks (like CKV_ANSIBLE_1 through CKV_ANSIBLE_6) and for Dockerfiles.
- Open .github/workflows/security-scan.yml in your text editor.
- Find the end of the secret-scan job. It ends with this line:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- Add the Checkov job below that line by copying this code:
checkov-scan:
name: IaC Security - Checkov
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: .
framework: ansible,dockerfile
output_format: cli
soft_fail: true
What does this code do?
- checkov-scan defines a new job in your workflow, running on its own GitHub Actions runner.
- framework: ansible,dockerfile tells Checkov to scan for both Ansible and Dockerfile misconfigurations in a single pass.
- soft_fail: true means the job reports findings but still shows as "passed." This lets you see all the results without blocking the other jobs while you're still learning.
- Save security-scan.yml.
You won't see any results yet. The next chunk adds the ansible-lint job, and then you'll push both together.
YAML not looking right?
YAML is whitespace-sensitive. Make sure checkov-scan: starts at the same indentation level as secret-scan: (two spaces in from the left). Each nested level adds two more spaces.
Help me fix YAML indentation in my GitHub Actions workflow
Add an ansible-lint job for best-practice checks
Checkov focuses on security misconfigurations. The ansible-lint tool covers a different angle. It checks for Ansible best-practice violations like deprecated syntax, formatting issues, and unsafe patterns that Checkov doesn't flag.
- In security-scan.yml, find the end of the checkov-scan job you just added. It ends with this line:
soft_fail: true
- Add the ansible-lint job below that line by copying this code:
ansible-lint:
name: Ansible Best Practices - ansible-lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install ansible-lint
run: pip install ansible-lint
- name: Run ansible-lint
run: ansible-lint ansible/
What does this code do?
- This job installs Python 3.12 on the runner because ansible-lint is a Python-based tool.
- pip install ansible-lint installs the linter, then ansible-lint ansible/ runs it against your ansible/ directory.
- Unlike the Checkov job that uses a pre-built action, this job installs and runs the tool manually. This gives you more visibility into what the scanning tool actually does under the hood.
- Save security-scan.yml.
You won't see results until you push the file in the next substep.
Worried about indentation?
Make sure ansible-lint: starts at the same indent level as secret-scan: and checkov-scan: (two spaces). Also check that python-version: "3.12" has the version number in quotes. YAML can misinterpret unquoted numbers.
Help me verify my workflow YAML is structured correctly
✔️ Awesome, I've got everything!
Great. Double check you've saved security-scan.yml before moving on.
ⓧ I'd like to double check the full code
Here is the complete security-scan.yml file with all three jobs. Compare it line by line with yours.
---
name: DevSecOps Security Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
secret-scan:
name: Secret Scanning - Gitleaks
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
checkov-scan:
name: IaC Security - Checkov
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: .
framework: ansible,dockerfile
output_format: cli
soft_fail: true
ansible-lint:
name: Ansible Best Practices - ansible-lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install ansible-lint
run: pip install ansible-lint
- name: Run ansible-lint
run: ansible-lint ansible/
Push the workflow and review scan results
Both new jobs are ready. Time to push them and watch the pipeline run.
- Commit and push the updated workflow by running these commands in your terminal:
git add .github/workflows/security-scan.yml
git commit -m "Add Checkov and ansible-lint jobs"
git push
- Open your devsecops-pipeline repository on GitHub.
- Click the Actions tab at the top of the page.
- Click on the latest workflow run (it should be titled Add Checkov and ansible-lint jobs).
You should see three jobs listed: Secret Scanning - Gitleaks, IaC Security - Checkov, and Ansible Best Practices - ansible-lint. Wait for all three to finish running.
- Click on the IaC Security - Checkov job to see its output.
- Expand the Run Checkov step in the log to see the scan results.
What should I see in the Checkov results?
Checkov's output lists each check it ran, whether it passed or failed, and which file triggered the finding. Look for failed checks like:
- CKV_ANSIBLE_1: URI module has disabled certificate validation (validate_certs: false).
- CKV_ANSIBLE_2: get_url module has disabled certificate validation.
- CKV_ANSIBLE_3: yum module has disabled certificate validation.
- CKV2_ANSIBLE_1 and CKV2_ANSIBLE_2: URI and get_url modules using HTTP instead of HTTPS.
The Checkov job still shows a green checkmark because you set soft_fail: true. It found the problems but didn't block the pipeline. You'll change this later.
- Click the back arrow to return to the workflow run summary.
- Click on the Ansible Best Practices - ansible-lint job.
- Expand the Run ansible-lint step to see its findings.
What should I see in the ansible-lint results?
ansible-lint reports best-practice warnings with rule names and descriptions. You'll see warnings about your playbook's formatting, task naming conventions, and potentially deprecated patterns.
These findings are different from Checkov's security checks. They focus on code quality and maintainability, which are important for real-world Ansible projects at scale.
Don't see all three jobs?
If only the Gitleaks job appears, your YAML likely has a syntax error that prevented GitHub from parsing the new jobs. Click the workflow run to check for a YAML parsing error message at the top.
The most common cause is incorrect indentation. All three job names (secret-scan, checkov-scan, ansible-lint) must be at exactly two spaces of indentation under jobs:.
Help me fix my GitHub Actions workflow that isn't showing all three jobs
Your pipeline now scans for leaked secrets, infrastructure misconfigurations, and Ansible best-practice violations. Next up, you'll add the final scanning layer: container image vulnerability scanning with Trivy.
Add Container Image Scanning with Trivy
Your pipeline now scans for leaked secrets, infrastructure misconfigurations, and Ansible best-practice violations. Three layers of security scanning are already catching problems in your configuration files.
There's one more attack surface to cover: the container image itself. Your Dockerfile builds an image from python:latest, which could include OS-level packages with known vulnerabilities. In this step, you'll add Trivy to your pipeline to scan the built Docker image before it ever reaches production.
In this step, get ready to:
- Add a Trivy container image scanning job to your GitHub Actions workflow.
- Push the updated workflow and review the vulnerability scan results.
Add the Trivy scanning job to your workflow
Trivy is an open-source vulnerability scanner that checks container images for known security issues called CVEs (Common Vulnerabilities and Exposures). Unlike Checkov, which analyzes your Dockerfile's configuration, Trivy scans the actual built image to find vulnerable packages hiding inside it.
- Open .github/workflows/security-scan.yml in your code editor.
- Scroll to the bottom of the file, after the last line of the ansible-lint job (run: ansible-lint ansible/).
- Add the following Trivy job below it:
trivy-scan:
name: Container Security - Trivy
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t devsecops-app:test docker/
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.33.1
with:
image-ref: "devsecops-app:test"
format: "table"
exit-code: "1"
severity: "CRITICAL,HIGH"
What does this code do?
- docker build -t devsecops-app:test docker/ builds the Docker image from your Dockerfile so Trivy has a real image to scan, not just the Dockerfile text.
- aquasecurity/trivy-action@0.33.1 runs the Trivy vulnerability scanner at a pinned stable version.
- image-ref tells Trivy which built image to scan.
- format: "table" produces a human-readable table of results in the Actions log.
- exit-code: "1" makes the job fail when vulnerabilities are found. This is your security gate.
- severity: "CRITICAL,HIGH" filters the scan to only report the most serious vulnerabilities, ignoring MEDIUM and LOW.
- Save security-scan.yml.
You won't see a change yet. The next substep pushes your updated workflow to GitHub where the pipeline will run.
YAML looking broken?
- Make sure trivy-scan: is indented with exactly 2 spaces, at the same level as secret-scan:, checkov-scan:, and ansible-lint:.
- Check that there's a blank line between the last line of the ansible-lint job and the new trivy-scan: line.
- YAML uses spaces, not tabs. If your editor inserts tabs, the workflow will fail to parse.
Why is my YAML indentation breaking the workflow?
✔️ Awesome, I've got everything!
Double check you've saved security-scan.yml before moving on.
ⓧ I'd like to double check the full code
Here's the complete security-scan.yml file with all four jobs. Compare it against yours:
---
name: DevSecOps Security Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
secret-scan:
name: Secret Scanning - Gitleaks
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
checkov-scan:
name: IaC Security - Checkov
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: .
framework: ansible,dockerfile
output_format: cli
soft_fail: true
ansible-lint:
name: Ansible Best Practices - ansible-lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install ansible-lint
run: pip install ansible-lint
- name: Run ansible-lint
run: ansible-lint ansible/
trivy-scan:
name: Container Security - Trivy
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t devsecops-app:test docker/
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.33.1
with:
image-ref: "devsecops-app:test"
format: "table"
exit-code: "1"
severity: "CRITICAL,HIGH"
Push the workflow and review the Trivy results
- Commit and push your updated workflow by running these commands:
git add .github/workflows/security-scan.yml
git commit -m "Add Trivy container image scanning"
git push
Push failing with authentication error?
If you see a 403 or authentication error, make sure you're using a personal access token or SSH key for GitHub authentication. GitHub no longer accepts account passwords for Git operations.
Help me fix my GitHub push authentication error.
- Open your devsecops-pipeline repository on GitHub.
- Click the Actions tab at the top of the repository.
- Click on the latest workflow run (it should be titled Add Trivy container image scanning).
You should see four jobs listed in the workflow run. The Container Security - Trivy job will take longer than the other jobs because it needs to build the Docker image and download the Trivy vulnerability database before scanning.
- Click on the Container Security - Trivy job to see its logs.
- Expand the Run Trivy vulnerability scanner step in the log output.
You should see a table listing CRITICAL and HIGH vulnerabilities found in the python:latest base image. The job should show a red X because exit-code: "1" tells the pipeline to fail when it finds vulnerabilities at these severity levels.
What am I looking at in the Trivy results?
Trivy's results table groups vulnerabilities by their source. Each row includes a CVE ID, the severity level, the package name, the installed version, and the fixed version (if one exists).
Most of the findings come from OS-level packages baked into the python:latest base image. This image is built on Debian, which includes hundreds of system packages. Some of those packages have known security vulnerabilities.
You may also see application dependency vulnerabilities from pip-installed packages like Flask. This is the key difference between Checkov (which checks your Dockerfile's configuration) and Trivy (which checks what's actually inside the built image).
Why so many vulnerabilities?
Using python:latest pulls the full Debian-based Python image, which includes many packages your app doesn't need. A slimmer base image like python:3.12-slim would dramatically reduce the number of CVEs. That's exactly the kind of fix you'll explore in the Secret Mission.
Don't see the Trivy job?
- Make sure you pushed the file after saving. Run git status to check for uncommitted changes.
- If the workflow run shows a YAML parsing error, check the indentation of the trivy-scan job. It must align with the other jobs under jobs:.
- Use the "double check the full code" tab above to compare your file against the expected version.
Help me debug why my Trivy job isn't showing up in my GitHub Actions pipeline.
Your pipeline now has four security scanning stages running in parallel. Next up, you'll review the complete pipeline and tighten the security gates so misconfigurations actually block the build.
Review Your Complete Pipeline
Your pipeline now has four security scanners running in parallel. Each one targets a different category of vulnerability.
Checkov currently runs in soft-fail mode, so it reports findings without actually blocking anything. In this step, you'll switch it to hard-fail and review every finding across all four tools.
In this step, get ready to:
- View all four parallel jobs running in GitHub Actions.
- Enable hard-fail mode on the Checkov security gate.
- Categorize every finding across the four scanning tools.
View the full pipeline in GitHub Actions
The GitHub Actions tab shows all four jobs running at the same time. Each job runs on its own isolated runner, so they execute simultaneously instead of one after another.
- Open your devsecops-pipeline repository on GitHub.
- Click the Actions tab at the top of the page.
- Click on the most recent workflow run labeled DevSecOps Security Pipeline.
You should see four jobs listed: Secret Scanning - Gitleaks, IaC Security - Checkov, Ansible Best Practices - ansible-lint, and Container Security - Trivy.
Why do the jobs run in parallel?
Each job in your workflow runs on its own separate GitHub Actions runner. Since none of the jobs use the needs: keyword to depend on another job, they all start at the same time.
This parallel execution speeds up your pipeline and mirrors how real DevSecOps pipelines run in production.
Enable hard-fail mode on the Checkov job
In a real security gate, you want the pipeline to fail when it detects misconfigurations. Right now, Checkov's soft_fail: true setting lets findings pass through without blocking anything.
- Open .github/workflows/security-scan.yml in your code editor.
- Scroll to the checkov-scan job and find this line:
soft_fail: true
- Change true to false, so the line now reads:
soft_fail: false
- Save security-scan.yml.
- Commit and push the change by running these commands:
git add .github/workflows/security-scan.yml
git commit -m "Enable hard-fail on Checkov scan"
git push
- Go back to the Actions tab in GitHub.
- Click on the latest workflow run.
The IaC Security - Checkov job should now show a red failure icon. This means the security gate is working.
What changed?
With soft_fail: false, Checkov now returns a non-zero exit code when it finds misconfigurations. GitHub Actions treats any non-zero exit code as a job failure, which marks the job in red.
This is how security gates work in production. They block insecure code from merging.
Checkov still showing green?
- Make sure you changed soft_fail: true to soft_fail: false. Deleting the line entirely is not the same as setting it to false.
- Confirm you saved the file, committed, and pushed to main. The Actions tab should show a new workflow run triggered by your push.
- Why is my Checkov job still passing after I changed soft_fail to false?
✔️ Awesome, I've got everything!
Double check you've saved your file and pushed to main.
ⓧ I'd like to double check the full code
Here is the complete security-scan.yml file after the change.
---
name: DevSecOps Security Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
secret-scan:
name: Secret Scanning - Gitleaks
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
checkov-scan:
name: IaC Security - Checkov
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: .
framework: ansible,dockerfile
output_format: cli
soft_fail: false
ansible-lint:
name: Ansible Best Practices - ansible-lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install ansible-lint
run: pip install ansible-lint
- name: Run ansible-lint
run: ansible-lint ansible/
trivy-scan:
name: Container Security - Trivy
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t devsecops-app:test docker/
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.33.1
with:
image-ref: "devsecops-app:test"
format: "table"
exit-code: "1"
severity: "CRITICAL,HIGH"
Categorize your security findings
Each tool caught a different type of security issue. Organizing these findings into categories helps you understand the layers of a DevSecOps pipeline.
- Click into each job in the Actions tab.
- Expand the step logs to see the detailed output from each scanner.
Your findings fall into four categories.
What did each tool find?
Secrets (Gitleaks): Detected the fake AWS access key in ansible/secrets.txt. Gitleaks flagged the AKIAIOSFODNN7EXAMPLE pattern as a potential AWS credential leak.
IaC Misconfigurations (Checkov): Failed checks include CKV_ANSIBLE_1 through CKV_ANSIBLE_3 for disabled certificate validation. CKV2_ANSIBLE_1 and CKV2_ANSIBLE_2 flagged HTTP URLs instead of HTTPS. Dockerfile checks flagged the missing USER instruction and missing HEALTHCHECK.
Best-Practice Violations (Ansible-lint): Flagged formatting issues and playbook structure warnings in ansible/site.yaml. These are not security vulnerabilities, but they indicate code that could lead to reliability issues.
Container CVEs (Trivy): Found CRITICAL and HIGH severity vulnerabilities in the python:latest base image. These are known CVEs in the operating system packages bundled with the image.
Why four separate tools instead of one?
No single tool catches everything. Gitleaks specializes in secret patterns. Checkov understands infrastructure-as-code policy rules. ansible-lint knows Ansible best practices. Trivy scans container images for known operating system vulnerabilities.
Layered scanning is a core principle of DevSecOps. Each tool covers a blind spot the others miss.
Secret mission
Fix the Vulnerabilities and Turn the Pipeline Green
Your pipeline caught real security issues across all four scanning tools. Now it's time to fix every finding and make the entire pipeline pass green. Create a remediation branch, patch the vulnerabilities, and prove your fixes work by opening a pull request.
Before you go
Clean Up Your Resources
This project runs entirely on GitHub's free tier and your local machine, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The devsecops-pipeline public GitHub repository.
- GitHub Actions workflow runs (free for public repositories).
- Local clone of the repository on your Desktop.
- Project files: ansible/site.yaml, ansible/secrets.txt, docker/Dockerfile, and .github/workflows/security-scan.yml.
Keep everything running
No action needed. Choose this if you're still actively building or plan to extend the project with the Secret Mission.
- Your GitHub repository stays public and accessible.
- GitHub Actions will continue to run on every push or pull request to main.
- Your local clone stays on your Desktop for future edits.
Pause - I'll come back to this later
Disable the GitHub Actions workflow so it stops running automatically. Your repository and files stay intact.
- Go to your devsecops-pipeline repository on GitHub.
- Click the Actions tab.
- Select the DevSecOps Security Pipeline workflow from the left sidebar.
- Click the three-dot menu (...) and select Disable workflow.
- When you're ready to come back, click Enable workflow from the same menu.
Delete - I don't want to use this again
Remove all project resources from GitHub and your local machine.
- Go to your devsecops-pipeline repository on GitHub.
- Click Settings in the top navigation bar.
- Scroll to the bottom of the page to the Danger Zone section.
- Click Delete this repository and follow the confirmation prompts.
- Delete the local clone from your Desktop by dragging the devsecops-pipeline folder to the Trash (macOS) or Recycle Bin (Windows).
Mission accomplished
That's a Wrap!
Nice work! You just built a real DevSecOps security pipeline from scratch using GitHub Actions. Your pipeline automatically catches leaked secrets, infrastructure misconfigurations, best-practice violations, and container vulnerabilities before code ever reaches production.
You've learned how to:
- Build a multi-stage GitHub Actions pipeline with four parallel security scanning jobs using Gitleaks, Checkov, ansible-lint, and Trivy.
- Detect leaked secrets, IaC misconfigurations, and container image vulnerabilities automatically using industry-standard open-source security tools.
- Configure security gates that block insecure code from merging, applying the shift-left security model that catches problems early in the development lifecycle.
- Remediate real security findings across all four tools and turn a failing pipeline green.
Ready to quiz yourself?