Your terraform plan looks clean. Your GitHub Actions workflow is green. Your Docker image builds without errors. But what about the Terraform provider you downloaded last week? The GitHub Action pinned to v4? The npm package three levels deep in your dependency tree?
Modern infrastructure doesn’t exist in isolation. Every Terraform module, every pip package, every GitHub Action you reference is a door you’ve opened into your environment. Most of those doors lead to well-maintained, trustworthy code. Some don’t. And you won’t know the difference until something goes wrong — unless you set up the right guardrails.
That’s where Dependabot and a broader supply chain security strategy come in.
Why Supply Chain Attacks Matter
If you write infrastructure code, you’re a consumer of other people’s code whether you realize it or not. A Terraform provider, an Ansible Galaxy role, an npm package — these are all links in your supply chain. And attackers have figured out that compromising one popular dependency is far more efficient than targeting individual organizations.
Here are real-world examples that should keep you up at night:
| Incident | Year | What Happened | Impact |
|---|---|---|---|
| SolarWinds | 2020 | Attackers compromised the build system and injected malicious code into the Orion software update | 18,000+ customers, including US government agencies |
| event-stream | 2018 | A maintainer handed off a popular npm package to an attacker who injected a cryptocurrency-stealing dependency | Millions of downloads before detection |
| Codecov | 2021 | Attackers modified the Bash Uploader script via a Docker image creation flaw, exfiltrating CI environment variables | 29,000+ enterprise customers affected |
| tj-actions/changed-files | 2025 | Attackers pushed malicious commits and updated 350+ Git tags to point to code that dumped runner secrets | 23,000+ repositories using the action |
The tj-actions incident in March 2025 is especially relevant for DevOps engineers. An attacker compromised a widely-used GitHub Action, retroactively modified version tags to point at malicious code, and every workflow referencing those tags silently started leaking secrets. If you were pinning to a tag like v45 instead of a commit SHA, you were exposed.
The Attack Vectors You Should Know
Before we talk about defenses, let’s understand the common attack patterns:
Dependency confusion exploits how package managers resolve names. If your organization uses an internal package called infra-utils and an attacker publishes infra-utils on the public npm or PyPI registry, your package manager might pull the public (malicious) version instead. Security researcher Alex Birsan demonstrated this in 2021 against companies like Apple, Microsoft, and Tesla.
Typosquatting is exactly what it sounds like. An attacker publishes reqeusts instead of requests on PyPI, or lodahs instead of lodash on npm, and waits for someone to make a typo. In 2023, researchers found 900 typosquats targeting just 40 popular PyPI packages.
Compromised maintainers happen when a trusted maintainer’s account is taken over, or when a popular but abandoned project is handed off to someone with bad intentions. The event-stream attack followed exactly this pattern.
Tag mutability is the GitHub Actions-specific variant. Git tags are mutable — they can be deleted and recreated pointing to completely different code. When you pin an action to v4, you’re trusting that tag will always point to safe code. The tj-actions attack proved that trust is misplaced.
Enter Dependabot
Dependabot is GitHub’s built-in tool for keeping dependencies current. It does two things:
- Version updates — Opens PRs when newer versions of your dependencies are available
- Security alerts — Notifies you when a known vulnerability affects a dependency in your project
You configure it by adding a .github/dependabot.yml file to your repository. Here’s a comprehensive example covering the ecosystems most DevOps engineers care about:
# .github/dependabot.yml
version: 2
updates:
# Keep GitHub Actions up to date
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
labels:
- "dependencies"
- "github-actions"
commit-message:
prefix: "ci"
# Terraform providers and modules
- package-ecosystem: "terraform"
directory: "/infrastructure"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "terraform"
reviewers:
- "infra-team"
commit-message:
prefix: "chore"
# Python dependencies (pip, pipenv, poetry all use "pip")
- package-ecosystem: "pip"
directory: "/scripts"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "python"
commit-message:
prefix: "chore"
# npm packages
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
labels:
- "dependencies"
- "javascript"
commit-message:
prefix: "chore"
# Docker base images
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "docker"
commit-message:
prefix: "chore"
A few things to note: each package-ecosystem entry targets a specific directory where the manifest lives. For Terraform, that’s wherever your .tf files are. For Docker, it’s where your Dockerfile is. You can have multiple entries for the same ecosystem pointing at different directories — useful for monorepos with Terraform configs in /infra/dev, /infra/staging, and /infra/prod.
By default, Dependabot opens a maximum of five pull requests for version updates per ecosystem. If you have a lot of outdated dependencies, bump open-pull-requests-limit so you’re not drip-fed updates over months.
Terraform-Specific Considerations
Dependabot monitors your .tf files for provider and module version constraints. When a new version of hashicorp/azurerm or a module you reference is released, Dependabot opens a PR updating the version constraint.
# Dependabot will detect this and propose updates
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.85.0"
}
}
}
module "networking" {
source = "Azure/network/azurerm"
version = "5.3.0"
# ...
}
When Dependabot opens a PR, it includes the changelog and release notes from the provider so you can assess what changed before merging. This is especially valuable for Terraform providers where a minor version bump can change resource behavior.
For repositories with multiple Terraform directories, add a separate entry per directory:
updates:
- package-ecosystem: "terraform"
directory: "/infra/networking"
schedule:
interval: "weekly"
- package-ecosystem: "terraform"
directory: "/infra/compute"
schedule:
interval: "weekly"
Pinning GitHub Actions to SHA
This is the single most impactful thing you can do for CI/CD supply chain security. Instead of referencing actions by tag:
# Risky -- tags are mutable
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
Pin them to a full commit SHA:
# Secure -- commit SHAs are immutable
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
The commit SHA is immutable. Even if an attacker compromises the repository and moves the v4 tag, workflows pinned to the SHA won’t pull the malicious code.
The comment at the end (e.g., # v4.2.2) is just for human readability — Git ignores it. And here’s the best part: Dependabot understands SHA-pinned actions. When a new version is released, Dependabot opens a PR updating both the SHA and the comment. You get immutability and maintainability.
To find the SHA for an action, check the releases page on GitHub or run:
# Get the commit SHA for a specific tag
git ls-remote --tags https://github.com/actions/checkout.git v4.2.2
As of August 2025, GitHub also supports organizational policies that enforce SHA pinning — workflows using tag-based references will fail, which is a great way to enforce this across a team.
Lockfiles and Reproducible Builds
If you manage any application code alongside your infrastructure, lockfiles are non-negotiable. A package-lock.json (npm), poetry.lock (Python), or .terraform.lock.hcl (Terraform) records the exact versions of every dependency and sub-dependency.
Why this matters for security:
- Without a lockfile,
npm installmight resolve to a different (potentially compromised) version than what you tested - Lockfiles include integrity hashes that verify the package content hasn’t been tampered with
- CI should always use
npm ci(notnpm install) to enforce strict lockfile adherence
# In CI -- installs exactly what's in the lockfile, fails on mismatch
npm ci
# For Python with pip
pip install --require-hashes -r requirements.txt
# Terraform initializes and writes .terraform.lock.hcl
terraform init
Commit your lockfiles. Every single one. I’ve seen teams .gitignore their lockfiles because they cause merge conflicts. That’s trading a minor inconvenience for a real security gap.
GitHub’s Security Toolkit Beyond Dependabot
Dependabot is one piece of a larger security surface that GitHub provides. Here’s the full picture:
| Feature | What It Does | Cost |
|---|---|---|
| Dependency Graph | Maps all dependencies in your repo, including transitive ones | Free |
| Dependabot Alerts | Notifies you when a known CVE affects your dependencies | Free |
| Dependabot Security Updates | Automatically opens PRs to fix vulnerable dependencies | Free |
| Dependabot Version Updates | Opens PRs to keep dependencies current (not just security fixes) | Free |
| Secret Scanning | Detects API keys, tokens, and credentials committed to the repo | Free for public repos |
| Push Protection | Blocks pushes that contain detected secrets before they reach the repo | Free for public repos |
| Code Scanning (CodeQL) | Static analysis to find vulnerabilities in your code | Free for public repos |
| SBOM Export | Generates a Software Bill of Materials from your dependency graph | Free |
Enable the dependency graph and Dependabot alerts at minimum. They’re free and require zero configuration beyond flipping them on in your repository settings under Settings > Code security and analysis.
Software Bill of Materials (SBOM)
An SBOM is a structured inventory of every component in your software — every library, every transitive dependency, every version number. Think of it as the ingredient list on the side of a food package.
Why you should care:
- Vulnerability response: When the next Log4j drops, an SBOM lets you instantly answer “are we affected?” instead of spending days auditing repos
- Compliance: Government agencies and enterprise customers increasingly require SBOMs (per CISA and Executive Order 14028)
- Transparency: You can evaluate your own exposure before an incident, not after
GitHub can export SBOMs directly from the dependency graph in SPDX format. Go to your repository, click Insights > Dependency graph > Export SBOM.
For automated generation in CI:
- name: Generate SBOM
uses: anchore/sbom-action@d94f46e13c6c62f59525ac9a1e147a99dc0b9bf5 # v0.17.2
with:
format: spdx-json
output-file: sbom.spdx.json
Evaluating Dependencies with OpenSSF Scorecard
Before you add a new dependency, how do you know if it’s well-maintained? The OpenSSF Scorecard project gives you a data-driven answer. It runs automated checks against open source projects and assigns scores from 0-10 across categories like:
- Whether the project uses branch protection
- Whether dependencies are pinned
- Whether the project has active maintainers
- Whether CI/CD pipelines run security checks
# Install and run Scorecard against a dependency
brew install scorecard
scorecard --repo=github.com/hashicorp/terraform-provider-azurerm
You can also check scores at scorecard.dev without installing anything. Make it a habit to check the Scorecard before adding a new Terraform module, GitHub Action, or npm package to your project.
Hands-On Lab: Set Up Dependabot on a Repository
Step 1: Create the configuration file
mkdir -p .github
cat > .github/dependabot.yml <<'YAML'
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
commit-message:
prefix: "ci"
- package-ecosystem: "terraform"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "chore"
YAML
Step 2: Pin your existing GitHub Actions to SHAs
Find each uses: line in your workflows and replace the tag with the full commit SHA. For example:
# Look up the SHA for actions/checkout v4.2.2
git ls-remote --tags https://github.com/actions/checkout.git | grep 'v4.2.2'
# Update your workflow
# Before: uses: actions/checkout@v4
# After: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Step 3: Enable security features in the repository
Navigate to Settings > Code security and analysis and enable:
- Dependency graph (usually enabled by default)
- Dependabot alerts
- Dependabot security updates
- Secret scanning (and push protection if available)
Step 4: Commit and push
git add .github/dependabot.yml
git commit -m "ci: add Dependabot configuration for Actions and Terraform"
git push
Within minutes, Dependabot will scan your repository and start opening PRs for outdated dependencies. Review them, check the changelogs, and merge with confidence.
Step 5: Verify it’s working
Go to Insights > Dependency graph > Dependabot in your repository. You should see each ecosystem listed with its last check time and any open PRs.
Troubleshooting Guide
| Problem | Cause | Fix |
|---|---|---|
| Dependabot not opening PRs | dependabot.yml not in .github/ directory on default branch | Ensure the file is at .github/dependabot.yml and merged to main |
| PRs not appearing for Terraform | Wrong directory path | The directory must point to where your .tf files live, not the repo root (unless that’s where they are) |
| Too many PRs at once | Default limit is 5 per ecosystem | Adjust open-pull-requests-limit or use ignore rules for low-priority packages |
| Dependabot can’t access private modules | No access to private registries or repos | Configure registries in dependabot.yml and grant Dependabot access in org settings |
| SHA-pinned action shows no updates | Dependabot doesn’t recognize the action | Ensure the SHA comment includes the version tag (e.g., # v4.2.2) for Dependabot to track |
| Security alerts but no auto-fix PRs | Dependabot security updates not enabled | Enable under Settings > Code security and analysis > Dependabot security updates |
FAQ:
Q: Does Dependabot work with OpenTofu?
A: Yes. Dependabot’s Terraform ecosystem support works with both Terraform and OpenTofu .tf files since they share the same syntax.
Q: Can I ignore certain dependencies?
A: Yes. Add an ignore block to skip specific packages or version ranges:
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
ignore:
- dependency-name: "aws-sdk"
versions: ["3.x"]
Q: How is Dependabot different from Renovate? A: Both keep dependencies current. Dependabot is built into GitHub and requires no installation. Renovate (by Mend) offers more configuration options, grouping strategies, and works across multiple Git platforms. For most GitHub-hosted projects, Dependabot is the simpler starting point.
What’s Next
Next post: Monorepos vs Polyrepos. We’ll dig into the tradeoffs of keeping everything in one repository versus splitting across many — and how your choice affects CI/CD, code ownership, and dependency management.
Happy automating!