DEV Community

Cover image for Hardening GitLab CI/CD with an open-source pipeline linter
Thib
Thib

Posted on

Hardening GitLab CI/CD with an open-source pipeline linter

Most GitLab linters tell you if your YAML is valid. Very few tell you if your YAML is dangerous.

It's easy to feel safe when you see a green build, but a "successful" pipeline can still have major governance gaps. A green checkmark won't tell you if:

  • Your pipeline uses mutable :latest tags or untrusted registries.
  • A developer accidentally disabled a security job with allow_failure: true.
  • Your "protected" branch settings are actually misconfigured.
  • Sensitive variables are being leaked via CI_DEBUG_TRACE.

We built Plumber to bridge the gap between "valid syntax" and "secure configuration." It's an open-source CLI that checks both your .gitlab-ci.yml and your GitLab project settings to see if they meet your organization's compliance standards.


Beyond Linting: The PBOM

One of the most powerful features is the Pipeline Bill of Materials (PBOM). Plumber can export a CycloneDX SBOM specifically for your CI/CD. This gives you a machine-readable inventory of every container image, component, and remote template your pipeline touches, essential for auditing your software supply chain.

What it enforces

Plumber ships with a default policy you can customize in .plumber.yaml. It covers:

  • Image Governance: Allowed registries, forbidden tags, and mandatory digest pinning.
  • Pipeline Integrity: Ensures includes are up-to-date and prevents the use of forbidden versions (like main or HEAD).
  • Security Hardening: Detects Docker-in-Docker (DinD) usage on shared runners and prevents unsafe variable expansion (mapped to OWASP CICD-SEC-1).
  • Setting Verification: Confirms branch protection is active and sensitive variable overrides are blocked.

More controls are available and documented at getplumber.io/docs/cli#available-controls.


What the output looks like

Running plumber analyze gives you a detailed, actionable report right in your terminal:

Analyzing project: mygroup/my-api
Branch: main
Config: .plumber.yaml

CONTROLS

✓  containerImageMustNotUseForbiddenTags
✗  containerImageMustComeFromAuthorizedSources
   • job "build": image "node:18" is not from an authorized registry
   • job "test": image "python:3.11" is not from an authorized registry
✓  branchMustBeProtected
✗  pipelineMustNotIncludeHardcodedJobs
   • job "lint" is hardcoded and not sourced from an include or component
✓  includesMustBeUpToDate
✗  includesMustNotUseForbiddenVersions
   • include "gitlab.com/myorg/templates/security@main" uses forbidden version "main"
✓  pipelineMustIncludeComponent
✓  pipelineMustIncludeTemplate

SUMMARY

Controls passed:    5 / 8
Compliance score:   62%
Threshold:          100%

Status: FAILED

Report written to: plumber-report.json
Enter fullscreen mode Exit fullscreen mode

The team gets a clear score plus exact failing controls. Findings are actionable and mapped to docs. plumber-report.json can be archived in CI for audits and trend tracking.


Example 1: Install and first run

Install (via Homebrew):

brew tap getplumber/plumber
brew install plumber
Enter fullscreen mode Exit fullscreen mode

Token Setup: Create a GitLab token with read_api and read_repository.

Note: The token needs Maintainer permissions. GitLab restricts access to branch protection and repository settings to Maintainer roles or higher, and Plumber needs this visibility to verify your project posture.

export GITLAB_TOKEN=glpat_xxxxxxxxxxxxxxxxxxxx
Enter fullscreen mode Exit fullscreen mode

Generate and Analyze:

# Create a local .plumber.yaml to customize your rules
plumber config generate

# Run the analysis
plumber analyze
Enter fullscreen mode Exit fullscreen mode

Tip: Plumber auto-detects your GitLab URL and project from your git remote.


Example 2: Local vs. Remote Analysis

A key technical advantage is that Plumber uses your local .gitlab-ci.yml but compares it against remote project settings. This allows you to catch security regressions before you even git push.

If you want to focus on specific issues (e.g., during a migration), you can target specific controls:

plumber analyze --controls containerImageMustNotUseForbiddenTags,branchMustBeProtected
Enter fullscreen mode Exit fullscreen mode

Example 3: Continuous Feedback in GitLab CI

You can automate these checks by adding the Plumber component to your pipeline. This provides feedback directly in your Merge Requests.

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
      when: never
    - if: $CI_COMMIT_BRANCH
    - if: $CI_COMMIT_TAG

include:
  - component: gitlab.com/getplumber/plumber/plumber@v0.1.30
    inputs:
      mr_comment: true
      badge: true
Enter fullscreen mode Exit fullscreen mode

With mr_comment: true, Plumber will post results as a comment on the MR, ensuring no one merges a pipeline that weakens your security posture.


Why use a Linter for Governance?

Plumber is about policy enforcement. While scanners like Trivy or Grype look for vulnerabilities inside your code and images, Plumber ensures the framework holding your CI/CD together is solid. It ensures your security scanners are actually running and that your repository settings aren't leaving the back door open.

Feedback

If you maintain GitLab at scale, what would you want a pipeline linter to enforce that generic CI UI or scanners do not cover well? I am especially curious about policy ideas that fit real-world developer workflows.

Links

Top comments (0)