Back to posts
Post

CI/CD Pipeline Vulnerabilities: How Security Gaps Creep Into DevOps

How vulnerabilities emerge in CI/CD pipelines, from leaked secrets to overprivileged runners. Learn the common gaps and practical fixes to harden your DevOps delivery chain.

DevopsDevOpsCI/CD SecurityPipeline VulnerabilitiesSecret ManagementSupply Chain SecurityDevSecOpsArtifact Signing

Security vulnerabilities in DevOps pipelines usually don't come from one big mistake. They accumulate from small misconfigurations across your CI/CD chain — hardcoded secrets in YAML files, runners executing untrusted code with too many privileges, artifact registries with no signing verification, and deployment credentials living in plaintext where anyone with repo access can read them. I've seen these patterns repeat across many environments, and the root cause is almost always speed prioritized over safety.

As I touched on in my earlier post about DevOps culture (https://furkanikkan.com/urun/devops-kulturu-nedir-yazilim-ekiplerini-nasil-degistirir-42), the push for faster delivery is great until it isn't. Your pipeline is now the most privileged system in your infrastructure — it can deploy to production, access databases, and push images. That makes it a prime target.

Secrets Leaking Through CI/CD Configuration Files

The most common vulnerability I see is secrets stored directly in pipeline definitions. Developers paste API keys, database passwords, and cloud credentials straight into .gitlab-ci.yml, Jenkinsfile, or GitHub Actions workflows because it's fast and it works on their machine.

The problem hits when that file lands in version control. Now your secrets are in the git history forever — even after you remove them from the current version.

# Check if you've leaked secrets in git history
git log -p | grep -iE '(password|secret|token|api_key|AWS_SECRET)'

Use your CI platform's secret management instead — GitLab CI/CD variables, GitHub encrypted secrets, Jenkins credentials store. Mark them as masked so they don't show up in logs. And rotate any secret that's ever been committed, because you have to assume it's already compromised.

Overprivileged CI Runners and Build Agents

This one keeps me up at night. Your CI runner executes arbitrary code from the repository — build scripts, test commands, post-install hooks from npm packages. If that runner has production deployment access or runs as root, you've just handed attackers a direct path from a compromised dependency to your production environment.

Here's what I do in my environment:

  • Run build agents in isolated containers or VMs that get destroyed after each job
  • Never give runners long-lived cloud credentials — use OIDC federation or short-lived tokens
  • Separate build runners from deploy runners — building code and deploying code should not share the same identity
  • Restrict egress traffic from runners so even if they're compromised, they can't phone home easily

As I mentioned before about least privilege with sudo (https://furkanikkan.com/urun/root-yetkisi-vermeden-linux-sistem-yonetimi-sudo-ile-en-az-yetki-46), the same principle applies here: give your pipeline components only the access they absolutely need and nothing more.

Untrusted Dependencies and Supply Chain Attacks

Your pipeline pulls hundreds of dependencies automatically. npm packages, PyPI modules, Docker base images, Terraform providers — each one is a potential entry point. The Solarwinds attack showed how devastating a compromised build toolchain can be, and similar attacks hit npm and PyPI packages regularly.

The scary part is that you don't even need to use the malicious package directly. A transitive dependency — something pulled in by a library you actually use — is enough.

A few things that help:

  • Pin dependency versions with lockfiles (package-lock.json, poetry.lock, requirements.txt with hashes)
  • Enable dependency scanning in your pipeline — Dependabot, Snyk, GitLab Dependency Scanning
  • Verify Docker base image digests instead of pulling latest
  • Use pip install --require-hashes for critical Python deployments
# Example: pinning by digest in a pipeline
image:
  name: myregistry.com/app-base:latest
  # Bad - tag can be overwritten

image:
  name: myregistry.com/app-base@sha256:abc123...
  # Good - immutable digest

Artifact Tampering and Unsigned Deployments

Most teams I've worked with build Docker images and push them to a registry without any signing. If someone gains write access to that registry — through leaked credentials, a compromised CI account, or a misconfigured IAM policy — they can replace your production image with a malicious one and your pipeline will happily deploy it.

This is where artifact signing comes in. Tools like Cosign (part of Sigstore) or Notary let you sign images at build time and verify them at deploy time. If the signature doesn't match, the deployment stops.

# Sign an image with Cosign
cosign sign --key cosign.key myregistry.com/app:v1.2.3

# Verify before deploying
cosign verify --key cosign.pub myregistry.com/app:v1.2.3

It adds a step to your pipeline, yes. But the alternative is deploying unverified code to production every single time.

Pipeline as Code: When Your CI Config Becomes the Attack Surface

Here's something I see teams miss: your pipeline configuration files are themselves code, and they need the same scrutiny as your application code. A malicious pull request can modify the CI config to exfiltrate secrets, skip tests, or inject commands into the build process.

I require approval for any change to CI/CD config files. Branch protection rules, required reviews, and restricting who can modify pipeline definitions — these are not bureaucratic overhead, they're security controls.

# GitHub branch protection for workflow files
paths:
  - '.github/workflows/*'
  - 'Jenkinsfile'
  - '.gitlab-ci.yml'
required_reviews: 2
restrict_pushes: true

Monitoring Pipeline Activity and Detectating Anomalies

You can't secure what you don't monitor. I send all CI/CD logs to a centralized logging system and set up alerts for unusual patterns — a pipeline running outside business hours, a job that suddenly starts accessing new secrets, deployments from branches that shouldn't deploy.

This ties back to what I wrote about how attackers breach companies (https://furkanikkan.com/urun/saldirganlar-sirketleri-nasil-ihlal-eder-gercek-saldiri-senaryolari-41) — the initial access is often quiet, and the damage happens when they find a privileged system to pivot through. Your CI/CD pipeline is exactly that kind of system.

Practical Steps to Harden Your Pipeline Today

If you're reading this and realizing your pipeline has gaps, don't panic. Here's where I'd start:

  1. Audit your repositories for committed secrets using tools like trufflehog or gitleaks
  2. Rotate every secret that's been in version control — assume exposure
  3. Review what permissions your CI runners actually have versus what they need
  4. Enable SAST and dependency scanning in your pipeline if you haven't already
  5. Implement image signing before your next production deploy
  6. Restrict who can modify pipeline configuration files

The goal isn't a perfectly secure pipeline — that doesn't exist. The goal is reducing the attack surface so that when something goes wrong, and it will, the blast radius is small enough to contain.

Your CI/CD pipeline is now a critical part of your infrastructure. Treat it like one.


Cover image: HD Wallpapers · CC0 (Openverse / kamu malı) · https://stocksnap.io/photo/light-abstract-V9L6XXK3LB