GitLab Ultimate Security — Opinionated Enablement Guide

[[TOC]]


⚠️ This guide represents best practices and opinionated defaults for GitLab Ultimate security enablement. It is intended as a practical starting point for customers and GitLab teams. For complex environments, large-scale rollouts, or deep customization, engaging GitLab Professional Services is strongly recommended to ensure a successful and supported implementation.


Part 1 — Intro

1.1 Why Security Matters

Security is both a business driver and a board-level concern. Without strong security posture:

  • Enterprises lose new customers who require security certifications or audits as a procurement condition
  • Breaches cause direct revenue loss, regulatory fines, and reputational damage
  • Development teams slow down as security debt accumulates and incidents require emergency response

Key external drivers (Examples):

  • Log4Shell / XZ Utils: Single open-source dependencies that exposed millions of applications — invisible without dependency scanning.
  • EU Cyber Resilience Act (CRA) / SBOM: Regulatory mandates requiring software supply chain transparency.
  • Digital Operational Resilience Act (DORA): EU regulation imposing strict ICT risk management and operational resilience requirements on financial entities — often the most important and significant driver for regulated EU customers.
  • NIS2, ISO 27001, SOC 2: Audit requirements that map directly to GitLab Ultimate security features.
  • AI-Assisted Development (CSA Mythos, 2026): AI coding assistants accelerate velocity but also scale vulnerability introduction — AI-generated code reproduces known vulnerability patterns from training data faster than teams can manually review. Automated security scanning is the only scalable countermeasure. The Zero Day Clock illustrates why: the window between vulnerability disclosure and active exploitation is shrinking — making shift-left detection and fast remediation not just best practice, but survival.

GitLab value in one sentence: GitLab Ultimate embeds security into the developer workflow — teams find and fix vulnerabilities before they ship, not after.


1.2 Capability Overview

CapabilityWhat It DoesWhy It Matters
SAST / Advanced SASTScans source code for vulnerabilitiesCatches issues at commit time
Dependency ScanningScans open-source libraries for CVEsAddresses supply chain risk
Container ScanningScans Docker images for vulnerabilitiesCritical for cloud-native workloads and supply chain integrity
Secret DetectionFinds hardcoded credentials and tokensPrevents credential leaks
IaC ScanningScans Terraform, Helm, Ansible for misconfigsPrevents cloud misconfigurations
DAST / API SecurityTests running applications at runtimeFinds issues static analysis misses
Vulnerability ManagementCentralized triage and trackingTurns findings into actionable work
Security PoliciesEnforces scanning and approval gatesScalable governance at group level
Compliance FrameworksMaps projects to regulatory standardsAudit-readiness and risk segmentation

1.3 Use Cases & Trigger Points

Customer SignalRecommendEscalate to PS?
"We find bugs after deployment"Advanced SAST + MR Approval PoliciesIf tuning needed
"We don't know what's in our software"Dependency Scanning + Container Scanning + SBOMNo
"We had a credential leak"Secret Detection + Secret Push ProtectionNo
"We need to prove compliance"Compliance Frameworks + PoliciesYes
"We have 50+ projects, security is inconsistent"Group-level Security PoliciesYes
"Our pipelines got slow after enabling scans"Runner Architecture reviewYes
"We want DAST or API scanning"DAST / API SecurityYes — always

1.4 Discovery Questions

For all engagements, use the PS Security Delivery Kit discovery questionnaire as the single source of truth for discovery.


Part 2 — Technical Implementation

We strongly recommend reading the official GitLab documentation alongside this guide. This guide provides opinionated defaults and practical starting points — the docs contain the full reference, all available variables, and up-to-date configuration options. Too many implementations go wrong because engineers skip the docs.

2.1 Scanner Tuning

2.1.1 Advanced SAST

Advanced SAST uses cross-file, cross-function dataflow analysis — higher signal, but requires tuning to reduce noise on large codebases.

Opinionated Defaults:

include:
  - template: Security/SAST.gitlab-ci.yml
variables:
  SAST_EXCLUDED_PATHS: "spec,test,tests,tmp,vendor,node_modules"
  GITLAB_ADVANCED_SAST_ENABLED: "true"

Performance & Optimization Tuning:

The Advanced SAST analyzer (GitLab-native, not Semgrep) accepts CLI options via SAST_SCANNER_ALLOWED_CLI_OPTS. These are the two highest-impact knobs for large codebases:

variables:
  # Parallelize analysis across CPU cores — set to (vCPUs - 1), never exceed physical cores
  # Default: 1 (single-threaded). On an 8-core runner this alone can cut scan time by 60-70%.
  SAST_SCANNER_ALLOWED_CLI_OPTS: "--multi-core 4 --max-memory 8192"

  # Abort analysis of a single file if it exceeds this threshold (seconds).
  # Prevents one pathological file from blocking the entire scan.
  # Default: 600s. Reduce to 300s on large repos to keep pipelines predictable.
  SAST_PROCESS_TIMEOUT: 300

Starting in 19.0 two new CI variables were introduced: ADVANCED_SAST_AVAILABLE_CPUS and ADVANCED_SAST_AVAILABLE_MEMORY.

Sizing guide: --multi-core should equal (runner vCPUs - 1). --max-memory (MB) should be ~75% of runner RAM. A runner with 8 vCPU / 16 GB RAM → --multi-core 7 --max-memory 12288. 📖 Advanced SAST CI/CD variables reference 📖 Tune runner resources for Advanced SAST — official guidance on runner sizing, auto-detection behavior, and available CI/CD variables (ADVANCED_SAST_AVAILABLE_CPUS, ADVANCED_SAST_AVAILABLE_MEMORY)

By default, Advanced SAST scans the entire repository on every pipeline run — including MR pipelines. Two opt-in mechanisms reduce this: 📖 Optimization docs

  • Diff-based scanning (ADVANCED_SAST_PARTIAL_SCAN: differential): scans only changed files + their dependents in the MR. Fastest feedback, but may miss cross-file vulnerabilities. A full scan always runs automatically on the default branch after merge. Note that diff computation depends on GIT_DEPTH — on shallow clones, if the merge base is not within the fetched history, the analyzer cannot determine the changed files and falls back to a full scan. Increase GIT_DEPTH if this happens.
  • Incremental scanning (GITLAB_ADV_SAST_INCR_SCAN: "true"): caches taint signatures between runs, reanalyzes only changed code. No coverage loss, but requires artifact storage.
  • C/C++: use GITLAB_ADVANCED_SAST_CPP_ENABLED: "true" alongside either option.

Key Practices:

  • Exclude test directories and vendored code — covered by Dependency Scanning
  • Use .sast-ruleset.toml to suppress specific rule IDs after confirming false positives — never suppress broadly
  • On legacy or large codebases: plan a triage sprint after first run; expect initial noise

Enterprise Scale: For monorepos or 50+ projects, enforce via Scan Execution Policy at group level rather than per-project CI — avoids configuration drift and ensures consistent coverage.

Common Pitfall: Disabling Advanced SAST entirely due to noise, or dismissing too much as false positive later. Tune exclusions first; use dismissal workflows in the Vulnerability Report.


2.1.2 Dependency Scanning (SCA)

Opinionated Defaults:

include:
  - template: Security/Dependency-Scanning.gitlab-ci.yml
variables:
  DS_EXCLUDED_PATHS: "spec,test,tests"

Version pinning: By default the template tracks the latest analyzer released for your GitLab version — this is intentional, so you automatically receive advisory database and analyzer updates. Pin the analyzer version only in regulated or air-gapped environments that require reproducible scans, and review pinned versions regularly to avoid falling behind.

Performance & Optimization Tuning:

variables:
  # Limit how deep into the directory tree the scanner searches for manifest files.
  # Default: -1 (unlimited). On monorepos with deeply nested packages this can
  # dramatically reduce scan time. Start with 4 and adjust.
  DS_MAX_DEPTH: 4

  # Pin the Java version used by the Gemnasium Maven/Gradle analyzer.
  # Mismatches between the scanner's JVM and your project's JVM cause false negatives.
  # Set this to match your project's actual Java version.
  DS_JAVA_VERSION: 17

  # Disable the automatic DB update check on every run in air-gapped / offline environments.
  # Without this, the scanner will fail or time out trying to reach the advisory DB.
  # Only set in offline environments — leave unset otherwise.
  # GEMNASIUM_DB_UPDATE_DISABLED: "true"

📖 Dependency Scanning CI/CD variables reference

Key Practices:

  • Enable on all projects using package managers (npm, pip, Maven, Go modules, etc.)
  • Use reachability analysis (JS/TS and Java) — only prioritize findings where vulnerable code is actually called
  • SBOM export (CycloneDX) is enabled by default — see Dependency Scanning SBOM docs for configuration and export options

Common Pitfall: Treating all findings equally. A dev-only dependency with no reachable path is not the same as a reachable critical CVE in a production library.


2.1.3 IaC Scanning

Opinionated Defaults:

include:
  - template: Security/SAST-IaC.gitlab-ci.yml
variables:
  SAST_EXCLUDED_PATHS: "test,tests,.terraform"

Version pinning: By default the template tracks the latest analyzer released for your GitLab version — this ensures you receive the newest misconfiguration rules. Pin the analyzer version only where reproducible scan results are required (e.g. regulated environments), and review pinned versions regularly.

Performance & Optimization Tuning:

IaC Scanning uses the KICS analyzer under the hood. There are no dedicated high-impact performance variables beyond path exclusions — the scanner is lightweight by design. The main optimization lever is excluding generated or cached Terraform provider directories:

variables:
  # Exclude Terraform provider cache and test fixtures — these are not your IaC
  SAST_EXCLUDED_PATHS: "test,tests,.terraform,.terraform.lock.hcl"

📖 IaC Scanning CI/CD variables reference

Key Practices:

  • GitLab automatically detects whether IaC files are present — if a repo has no Terraform, Helm, or similar files, the scanner exits cleanly without producing findings
  • Triage priority order: public exposure rules → IAM misconfigurations → unencrypted storage
  • Cross-reference with CIS Benchmarks for AWS/GCP/Azure

2.1.4 Secret Detection

Opinionated Defaults:

include:
  - template: Security/Secret-Detection.gitlab-ci.yml
variables:
  SECRET_DETECTION_EXCLUDED_PATHS: "spec,test,tests"
  SECRET_DETECTION_HISTORIC_SCAN: "false"

Version pinning: By default the template tracks the latest analyzer released for your GitLab version — this ensures new secret detection rules and token patterns are picked up automatically. Pin the analyzer version only where reproducible scan results are required, and review pinned versions regularly.

Performance & Optimization Tuning:

Secret Detection uses git log under the hood. On repos with long histories the default behaviour (scan all commits) is the primary performance risk. These variables give you surgical control:

variables:
  # Limit how many commits back the scanner reads on non-historic runs.
  # Default: unset (scans from the last pipeline's commit, which is fast on MRs).
  # On scheduled full-branch scans of old repos, cap depth to avoid timeouts.
  # Passed directly to `git log` as --max-count.
  SECRET_DETECTION_LOG_OPTIONS: "--max-count=500"

  # Historic scan: scans the ENTIRE git history. Only run once per repo, off-peak,
  # never on every MR. After the baseline run, set back to false.
  SECRET_DETECTION_HISTORIC_SCAN: "false"

Historic scan strategy: Schedule a one-time historic scan per repo group during off-peak hours using a scheduled pipeline. After it completes, triage findings and set SECRET_DETECTION_HISTORIC_SCAN: "false" permanently. Incremental scans on subsequent MRs are then sub-second.

📖 Secret Detection CI/CD variables reference

Key Practices:

  • Run on every branch and MR — secrets on feature branches are still a risk
  • Combine with Secret Push Protection for the strongest pre-commit control — blocks secrets before they ever enter the repository

Common Pitfall: Only scanning the default branch. Secrets on feature branches are exposed via MR diffs and forks.


2.1.5 Container Scanning

Opinionated Defaults:

include:
  - template: Security/Container-Scanning.gitlab-ci.yml
variables:
  CS_IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"

Performance & Optimization Tuning:

variables:
  # Only report findings at or above this severity — reduces noise in the Vulnerability Report
  # without suppressing the underlying scan. Trivy still scans everything; GitLab filters on ingest.
  # Recommended starting point: HIGH. Lower to MEDIUM once your Critical/High backlog is under control.
  CS_SEVERITY_THRESHOLD: "HIGH"

  # Point the scanner at your Dockerfile so it can correlate CVEs to specific build layers.
  # Enables layer-aware triage — you can see which RUN/COPY instruction introduced a CVE.
  # Default: Dockerfile in repo root. Override if yours is elsewhere.
  CS_DOCKERFILE_PATH: "docker/Dockerfile"

  # Trivy pulls its vulnerability DB on every run (~200 MB). In air-gapped environments
  # or to speed up scans, mirror the DB and point the scanner at your mirror.
  # Leave unset in standard environments.
  # TRIVY_DB_REPOSITORY: "registry.example.com/mirror/trivy-db:2"

Severity threshold strategy: Start with CS_SEVERITY_THRESHOLD: "HIGH" to establish a manageable baseline, then lower to "MEDIUM" once Critical/High debt is under control. Never set to "CRITICAL" only — you will miss too much.

📖 Container Scanning CI/CD variables reference

Key Practices:

  • Scan the final built image, not a base image — catches all layers
  • Prioritise findings by: EPSS score > severity label alone; KEV status = immediate action. Always weigh application/service context as well — a medium-EPSS finding in an internet-facing application handling PII may still be more important than a high-EPSS finding in a non-internet-facing application
  • Ignore the reachability field for container findings — it is always null and not meaningful for this scanner type
  • Keep base images updated; most container CVEs are in OS packages, fixed by a base image bump

Enterprise Scale: Enforce scanning via Scan Execution Policy and use the Vulnerability Report to track image-level debt across all projects centrally.

Common Pitfall: Dismissing container findings because "the app doesn't use that package directly" — OS-level CVEs in base images are still exploitable at the container level.


2.1.6 DAST + API Security

DAST requires a running application environment. This is the most complex scanner — always involve a security engineer for initial setup.

See the official docs for enabling and configuring the analyzer:

Opinionated Defaults (DAST):

include:
  - template: DAST.gitlab-ci.yml
variables:
  DAST_TARGET_URL: "https://your-app.example.com"
  DAST_FULL_SCAN: "true"

For APIs, use API Security Testing with an OpenAPI spec:

include:
  - template: API-Security.gitlab-ci.yml
variables:
  APISEC_TARGET_URL: "https://your-api.example.com"
  APISEC_OPENAPI: "openapi.json"
  APISEC_PROFILE: "Full"

Performance & Optimization Tuning:

DAST is the most resource-intensive scanner. These variables are the primary levers for controlling scan depth vs. pipeline duration:

variables:
  # Maximum number of browser interactions (clicks, form submissions, navigation) the
  # crawler will perform. Default: 10000. Reduce for MR scans to keep them under 15 min;
  # use the full default (or higher) for scheduled nightly scans.
  DAST_BROWSER_MAX_ACTIONS: 2000

  # Number of parallel browser instances. Default: 3.
  # Increase on runners with more CPU/RAM to speed up crawling.
  # Each browser uses ~500 MB RAM — size accordingly.
  DAST_BROWSER_NUMBER_OF_BROWSERS: 3

  # Per-request timeout in seconds. Default: 120.
  # Lower this on fast internal Review Apps to avoid waiting on hung requests.
  DAST_REQUEST_TIMEOUT: 30

  # Total crawl time limit in minutes. Hard ceiling regardless of actions count.
  # Useful as a safety net on MR pipelines to prevent runaway scans.
  DAST_CRAWL_TIMEOUT: "20m"

MR vs. nightly strategy:

  • On MRs: DAST_BROWSER_MAX_ACTIONS: 2000 + DAST_CRAWL_TIMEOUT: "15m" — fast passive coverage
  • Nightly scheduled: Remove overrides, use defaults — full depth crawl with active scanning

📖 DAST browser-based analyzer variables reference 📖 API Security Testing variables reference

Key Practices:

  • Always use Review Apps as the target — never point DAST at production
  • Start with passive scans; enable full active scanning only after validating the environment
  • Authenticate DAST — unauthenticated scans miss the majority of real vulnerabilities

Enterprise Scale: Do not run DAST on every MR without autoscaling runners — it will saturate your fleet. Schedule nightly full scans; run passive-only on MRs.

Common Pitfall: Running unauthenticated DAST against a non-representative environment — produces mostly false positives and misses real issues.


2.2 Security Policies

Security Policies in GitLab are rules that enforce security behavior across projects and groups — independent of individual project CI configurations.

📖 YAML examples and templates: PS Security Delivery Kit

Policy TypeWhat It DoesWhen to Use
Scan Execution PolicyForces scans to run regardless of project CI configGuarantee coverage across all projects; scheduled scans
Pipeline Execution PolicyInjects jobs into pipelines group-wideWhen Scan Execution Policies are not enough — e.g. custom logic, conditional jobs, or non-security workloads
MR Approval PolicyBlocks merge if new vulnerabilities with certain severities are introducedSecurity gate before code reaches main
Vulnerability Management PolicyAuto-triage findings based on rulesScale triage; enforce SLAs; automatic severity updates

Opinionated Defaults:

  • MR Approval Policy: require approval for new Critical or High only — Medium/Low creates approval fatigue. Note: this guidance applies primarily to SCA/Container Scanning, which use a Low-to-Critical severity scale. SAST only rates findings Low-to-High, so Medium/High SAST findings should still be of concern and handled via triage rather than blanket-excluded
  • Start with Scan Execution Policies before MR Approval Policies — enforce coverage before adding gates

📖 Security configuration profiles — use profiles to manage and reuse scanner configurations across projects and groups without duplicating CI variables.


2.3 Compliance Frameworks

Compliance Frameworks are labels applied to projects that signal which regulatory or risk standard they must adhere to. Their primary purpose is twofold: making it visible which projects comply with a given standard (e.g. PCI-DSS, SOC 2, ISO 27001), and enabling policy scoping — so that Policies automatically apply to the right projects without per-project configuration.

📖 Configure policy scope

Workflow:

  1. Create a Compliance Framework (e.g., PCI-DSS, SOC2, ISO27001) based on the compliance standards reference
  2. Assign the relevant projects to the framework
  3. Scope your Policies to that framework

New projects added to a framework inherit policies immediately — no per-project configuration needed.

Note: A well-designed group and subgroup structure is critical for effective policy scoping and compliance coverage. This is a non-trivial architectural decision — GitLab Professional Services can help design the right hierarchy for your organisation.


2.4 Runner Architecture & Performance

Enabling security scans without capacity planning is the most common cause of pipeline degradation after an Ultimate rollout.

Runner Sizing

GitLab does not publish official benchmarks by repository size or LOC. The right approach is: monitor first, size from data.

The one hard rule documented for Advanced SAST is:

Minimum 4 GB RAM per CPU core. If detection fails, the analyzer defaults to 1 core / 4 GB.

This sizing logic applies proportionally to other scanners as well — DAST browser workers, Dependency Scanning, and Container Scanning all benefit from additional cores and memory, though none have the same strict per-core minimum as Advanced SAST.

📖 Tune runner resources for Advanced SAST — official guidance including auto-detection behavior and CI/CD variables.

GitLab SaaS hosted runner specs (official, source):

Runner TagvCPUsRAMNotes
saas-linux-small-amd6428 GBDefault for untagged jobs
saas-linux-medium-amd64416 GBPremium/Ultimate
saas-linux-large-amd64832 GBPremium/Ultimate; recommended for Advanced SAST
saas-linux-xlarge-amd641664 GBPremium/Ultimate; large monorepos

For self-managed runners, set cgroup memory limits so the analyzer can auto-detect correctly, or override explicitly via ADVANCED_SAST_AVAILABLE_CPUS and ADVANCED_SAST_AVAILABLE_MEMORY.

Best Practices

  • Monitor first: Set up runner metrics before enabling scans.

  • Dedicated runner pools: Use tags to route security jobs away from build runners. Two pools are sufficient for most setups:

    Pool A: build-runner     -> compile, unit tests
    Pool B: security-runner  -> SAST, Dependency Scanning, Container Scanning, Secret Detection, DAST, API Security
    

    DAST uses a browser-based crawler (Chromium workers) and benefits from more CPU, but does not require a separate pool — the key lever is scheduling (nightly, not on every MR), not isolation.

  • Autoscale security runners using Kubernetes executor — scale on queue depth, not fixed capacity

  • Schedule heavy scans (DAST, historic secret detection) nightly rather than on every MR

Common Pitfalls:

PitfallMitigation
Enabling all scanners on all projects at onceRoll out incrementally and monitor
No dedicated security runnersSeparate pools with runner tags
DAST on every MR without autoscalingSchedule nightly; passive scan on MRs only
Historic secret scans on all repos simultaneouslyStagger off-peak, one group at a time
No monitoring baseline before rolloutSet up Prometheus & Grafana dashboards first

2.5 Vulnerability Management & Prioritization

Remediate by business risk, not just by score. A CVSS Critical in an internal tool with no internet exposure and no reachable code path is less urgent than a CVSS High in a customer-facing API that is actively exploited in the wild. Scanner severity scores are a starting point — business context (exposure, reachability, asset criticality, active exploitation) determines actual priority. Build triage workflows that encode this logic, not just severity thresholds.

Prioritization decision logic:

1. KEV = true?          -> Immediate action regardless of severity (P1)
2. EPSS > 0.7?          -> Treat as urgent (P1/P2)
3. Reachable = true?    -> Prioritize for remediation (Dependency Scanning only)
4. Reachable = false?   -> Dismiss with documentation (Dependency Scanning only)
5. Reachable = null?    -> Do NOT assume safe — investigate
6. Internet-facing?     -> Higher priority than internal-only
7. Critical?            -> Remediate within 7 days
8. High?                -> Remediate within 30 days
9. Medium?              -> Remediate within 90 days or accept risk
10. Low/Info?           -> Batch or accept with documentation

Priority Matrix:

ExploitabilityExposureAction
KEV or EPSS > 0.7Internet-facingImmediate — P1
KEV or EPSS > 0.7InternalUrgent — P2
Reachable, Critical/HighInternet-facingHigh — P2
Reachable, Critical/HighInternalMedium — P3
Not reachableAnyDismiss with documentation
Low/Info, not reachableAnyDismiss or batch

Severity Override Policies:

GitLab's Vulnerability Management Policies support severity overrides — allowing you to automatically adjust the reported severity of a vulnerability based on your own context, independent of the scanner's original score.

This is a powerful tool for operationalizing the "business risk over score" principle at scale:

  • Downgrade a Critical finding to Medium if it is in an internal-only service with no internet exposure and no reachable path — removes it from emergency queues without dismissing it
  • Upgrade a Medium finding to High if it affects a payment processing service or a compliance-scoped project — ensures it gets the attention it deserves
  • Scope overrides by project, group, or Compliance Framework — different risk profiles for different parts of the estate

Severity overrides do not change the underlying scanner finding — they adjust how GitLab surfaces and prioritizes it in the Vulnerability Report and policy evaluations. The original score is always preserved.

Triage Workflow:

  • Week 1: Enable scanners → run first scan → categorize Critical/High vs rest → dismiss confirmed false positives
  • Week 2–4: Fix Critical + reachable High → create issues for remaining High with owners and due dates
  • Ongoing: Triage new findings within 48h · weekly 15-min review · monthly trend check

Maturity Frameworks for Reference:


For a structured, phased rollout plan including timelines, milestones, and engagement guidance, use the Security Delivery Kit Roadmap Outline as the single source of truth.


Living document. Link policy sections to the PS Security Delivery Kit for concrete YAML examples. Update scanner defaults as new analyzer versions are released.