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
| Capability | What It Does | Why It Matters |
|---|---|---|
| SAST / Advanced SAST | Scans source code for vulnerabilities | Catches issues at commit time |
| Dependency Scanning | Scans open-source libraries for CVEs | Addresses supply chain risk |
| Container Scanning | Scans Docker images for vulnerabilities | Critical for cloud-native workloads and supply chain integrity |
| Secret Detection | Finds hardcoded credentials and tokens | Prevents credential leaks |
| IaC Scanning | Scans Terraform, Helm, Ansible for misconfigs | Prevents cloud misconfigurations |
| DAST / API Security | Tests running applications at runtime | Finds issues static analysis misses |
| Vulnerability Management | Centralized triage and tracking | Turns findings into actionable work |
| Security Policies | Enforces scanning and approval gates | Scalable governance at group level |
| Compliance Frameworks | Maps projects to regulatory standards | Audit-readiness and risk segmentation |
1.3 Use Cases & Trigger Points
| Customer Signal | Recommend | Escalate to PS? |
|---|---|---|
| "We find bugs after deployment" | Advanced SAST + MR Approval Policies | If tuning needed |
| "We don't know what's in our software" | Dependency Scanning + Container Scanning + SBOM | No |
| "We had a credential leak" | Secret Detection + Secret Push Protection | No |
| "We need to prove compliance" | Compliance Frameworks + Policies | Yes |
| "We have 50+ projects, security is inconsistent" | Group-level Security Policies | Yes |
| "Our pipelines got slow after enabling scans" | Runner Architecture review | Yes |
| "We want DAST or API scanning" | DAST / API Security | Yes — 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-coreshould 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 onGIT_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. IncreaseGIT_DEPTHif 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.tomlto 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"
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"
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.
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.
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:
- DAST Browser-based analyzer — enabling
- DAST Browser-based analyzer — settings
- API Security Testing — enabling
- API Security Testing — settings
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 Type | What It Does | When to Use |
|---|---|---|
| Scan Execution Policy | Forces scans to run regardless of project CI config | Guarantee coverage across all projects; scheduled scans |
| Pipeline Execution Policy | Injects jobs into pipelines group-wide | When Scan Execution Policies are not enough — e.g. custom logic, conditional jobs, or non-security workloads |
| MR Approval Policy | Blocks merge if new vulnerabilities with certain severities are introduced | Security gate before code reaches main |
| Vulnerability Management Policy | Auto-triage findings based on rules | Scale 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.
Workflow:
- Create a Compliance Framework (e.g.,
PCI-DSS,SOC2,ISO27001) based on the compliance standards reference - Assign the relevant projects to the framework
- 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 Tag | vCPUs | RAM | Notes |
|---|---|---|---|
saas-linux-small-amd64 | 2 | 8 GB | Default for untagged jobs |
saas-linux-medium-amd64 | 4 | 16 GB | Premium/Ultimate |
saas-linux-large-amd64 | 8 | 32 GB | Premium/Ultimate; recommended for Advanced SAST |
saas-linux-xlarge-amd64 | 16 | 64 GB | Premium/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.
- Fleet scaling: https://docs.gitlab.com/runner/fleet_scaling/#monitoring-runners
- Prometheus metrics: https://docs.gitlab.com/runner/monitoring/
-
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 SecurityDAST 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:
| Pitfall | Mitigation |
|---|---|
| Enabling all scanners on all projects at once | Roll out incrementally and monitor |
| No dedicated security runners | Separate pools with runner tags |
| DAST on every MR without autoscaling | Schedule nightly; passive scan on MRs only |
| Historic secret scans on all repos simultaneously | Stagger off-peak, one group at a time |
| No monitoring baseline before rollout | Set 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:
| Exploitability | Exposure | Action |
|---|---|---|
| KEV or EPSS > 0.7 | Internet-facing | Immediate — P1 |
| KEV or EPSS > 0.7 | Internal | Urgent — P2 |
| Reachable, Critical/High | Internet-facing | High — P2 |
| Reachable, Critical/High | Internal | Medium — P3 |
| Not reachable | Any | Dismiss with documentation |
| Low/Info, not reachable | Any | Dismiss 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:
- OWASP SAMM: https://owaspsamm.org/ — maps well to GitLab feature adoption stages
- ISO/IEC 27034: Formal application security controls for regulated industries
- NIST SSDF (SP 800-218): Secure Software Development Framework — maps directly to GitLab shift-left capabilities; see also GitLab's NIST SP 800-218 compliance reference
Appendix — Recommended Rollout Sequence
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.