DevSecOps

GitLab CI Security Scanning Setup

Step-by-step guide to enabling SAST, DAST, dependency scanning, and container scanning in GitLab CI pipelines.

Shadab Khan
Application Security Engineer
6 min read

GitLab bundles security scanning directly into its CI/CD platform. Unlike GitHub, where you assemble scanning from third-party actions, GitLab ships SAST, DAST, dependency scanning, container scanning, and secret detection as built-in templates. The problem is not availability — it is configuration. Most teams either enable everything with defaults and drown in noise, or enable nothing because the documentation is dense.

This guide walks through setting up each scanner properly, tuning results, and making the output actionable.

What GitLab Offers Out of the Box

GitLab's security scanning features (available on Ultimate tier, or partially on other tiers) include:

  • SAST: Static analysis across 15+ languages using analyzers like Semgrep, SpotBugs, and Bandit.
  • DAST: Dynamic testing against running applications.
  • Dependency Scanning: CVE detection in package manifests.
  • Container Scanning: Vulnerability detection in Docker images using Trivy or Grype.
  • Secret Detection: Pattern-matching for leaked credentials in commits.
  • License Compliance: Identifying problematic open-source licenses.

Each scanner runs as a CI job using pre-built Docker images maintained by GitLab.

Enabling SAST

The fastest way to enable SAST is to include the GitLab-managed template:

include:
  - template: Security/SAST.gitlab-ci.yml

variables:
  SAST_EXCLUDED_PATHS: "spec, test, tests, tmp, vendor"

This auto-detects your project's languages and runs the appropriate analyzers. But the defaults cast a wide net. Narrow the scope:

  • Use SAST_EXCLUDED_PATHS to skip test directories, vendor code, and generated files.
  • Set SAST_EXCLUDED_ANALYZERS to disable analyzers for languages you do not use.
  • Configure SAST_BANDIT_EXCLUDED_PATHS or equivalent per-analyzer variables for fine-tuning.

Review the first few runs. Expect false positives. Triage them once, add suppressions, and the signal-to-noise ratio improves dramatically.

Setting Up Dependency Scanning

include:
  - template: Security/Dependency-Scanning.gitlab-ci.yml

variables:
  DS_EXCLUDED_PATHS: "doc, spec"

Dependency scanning parses package-lock.json, Gemfile.lock, requirements.txt, go.sum, and similar lockfiles. It cross-references versions against vulnerability databases.

Key configuration points:

  • Ensure your lockfiles are committed. If the scanner cannot find a lockfile, it skips the project.
  • Use DS_EXCLUDED_ANALYZERS to disable analyzers you do not need (e.g., disable the Bundler analyzer if you have no Ruby code).
  • Set DS_REMEDIATE to false in CI if you do not want auto-remediation merge requests.

Container Scanning

If your pipeline builds Docker images, scan them:

include:
  - template: Security/Container-Scanning.gitlab-ci.yml

variables:
  CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  CS_SEVERITY_THRESHOLD: HIGH

CS_SEVERITY_THRESHOLD controls which severity levels fail the pipeline. Setting it to HIGH means only high and critical vulnerabilities cause failures. Start here and tighten over time.

The scanner runs after your docker build stage. Make sure the image is pushed to a registry the scanner can access, or use CS_IMAGE to point to the local image.

DAST Configuration

DAST requires a running application. You need a review environment or a staging deployment:

include:
  - template: Security/DAST.gitlab-ci.yml

variables:
  DAST_WEBSITE: https://staging.example.com
  DAST_FULL_SCAN_ENABLED: "false"

dast:
  stage: dast
  needs: ["deploy-staging"]

Start with passive scanning (DAST_FULL_SCAN_ENABLED: "false"). Passive scanning crawls the application and identifies issues without sending attack payloads. It is safe for production-like environments and finds a surprising number of issues: missing security headers, exposed debug endpoints, information disclosure.

Move to active scanning once your staging environment is stable and you have coordinated with your team. Active scanning sends injection payloads and can cause data corruption in stateful applications.

Secret Detection

include:
  - template: Security/Secret-Detection.gitlab-ci.yml

variables:
  SECRET_DETECTION_HISTORIC_SCAN: "false"

Secret detection runs on every commit. Setting SECRET_DETECTION_HISTORIC_SCAN to true scans the entire git history, which is useful for initial setup but slow on large repositories.

The default rules catch AWS keys, GCP service account keys, private keys, and common API token patterns. Add custom rules for your organization's internal credential formats:

variables:
  SECRET_DETECTION_RULESET_FILE: .gitlab/secret-detection-ruleset.toml

Making Results Actionable

Enabling all these scanners is the easy part. The hard part is handling the output.

Use the Security Dashboard

GitLab's Security Dashboard aggregates findings across all scanners and all projects in a group. Use it to:

  • Identify the most common vulnerability types across your organization.
  • Track remediation timelines.
  • Spot projects that have not enabled scanning at all.

Set Merge Request Approval Rules

Configure merge request approval rules that require security team sign-off when new vulnerabilities are introduced:

# In project settings, not .gitlab-ci.yml
# Settings > Merge Requests > Approval Rules
# Add "Security" approval rule for vulnerability reports

This does not block all merges — only merges that introduce new findings. Developers can still merge freely when their code is clean.

Triage with Vulnerability Management

GitLab's vulnerability management workflow lets you dismiss false positives with a reason, create issues for confirmed findings, and track remediation. Use it. Do not let the vulnerability list grow unbounded — that is how teams learn to ignore security results entirely.

Pipeline Structure

A well-structured secure pipeline looks like this:

stages:
  - build
  - test
  - security
  - deploy-staging
  - dast
  - deploy-production

include:
  - template: Security/SAST.gitlab-ci.yml
  - template: Security/Dependency-Scanning.gitlab-ci.yml
  - template: Security/Container-Scanning.gitlab-ci.yml
  - template: Security/Secret-Detection.gitlab-ci.yml
  - template: Security/DAST.gitlab-ci.yml

Security scans run in parallel during the security stage. DAST runs after staging deployment. Production deployment is gated on all previous stages passing.

Common Pitfalls

  • Running scanners on every branch: Limit full security scans to merge requests and the default branch. Feature branches can run a subset.
  • Not excluding generated code: Auto-generated files produce massive false positive counts. Always configure exclusion paths.
  • Ignoring scanner updates: GitLab updates scanner images regularly. Pin to a specific major version if you need stability, but update quarterly at minimum.
  • Not providing authentication for DAST: Without login credentials, DAST only tests unauthenticated surfaces. Configure DAST_AUTH_URL and related variables to cover authenticated pages.

How Safeguard.sh Helps

Safeguard.sh complements GitLab's built-in scanners by providing a unified security view that spans multiple GitLab groups, projects, and even non-GitLab repositories. While GitLab's Security Dashboard works within its own ecosystem, Safeguard.sh correlates findings across your entire toolchain — connecting CI/CD scan results with SBOM data, runtime monitoring, and policy enforcement. It identifies gaps where scanning is not enabled, tracks vulnerability remediation across teams, and provides the executive-level reporting that GitLab's native tools lack.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.