GitLab Security Hardening: Instance Configuration, CI/CD Permissions, and Dependency Scanning

Retool's new app builder is where AI-generated code ships safely
Building apps with AI is easy. Getting them to production safely is another story.
A default GitLab self-managed installation allows public project visibility at the instance level, CI/CD job tokens with broad scope, unprotected branches, and runner registration tokens accessible to project Maintainers. For GitLab.com organizations, visibility defaults are more restrictive but CI/CD permission issues and branch protection gaps are equally common. This guide covers the configuration changes that close the largest attack surface areas.
Instance-Level Security Configuration (Self-Managed)
Self-managed GitLab admins control instance-wide settings that affect every group and project. These should be the starting point for any GitLab hardening engagement.
Restrict default project visibility to Internal or Private
Admin Area > Settings > General > Visibility and Access Controls > Default project visibility: change from Public to Internal or Private. With Internal visibility, projects are visible to all authenticated users in the instance: appropriate for most internal tooling. Private requires explicit membership for every project. Set to Private for instances hosting sensitive code. Also change: Default group visibility and Default snippet visibility to match.
Disable public sign-ups or require admin approval
Admin Area > Settings > General > Sign-up restrictions: either disable sign-up entirely (appropriate for internal GitLab instances where users are provisioned via SSO) or enable 'Require admin approval for new sign-ups.' An open sign-up GitLab instance allows any internet user to create an account, access Internal-visibility projects, and register personal runners.
Enforce SSO for all groups using SAML
For GitLab.com: Group > Settings > SAML SSO > Enable SAML and Enable SSO enforcement. Enforcement requires all group members to authenticate via your IdP SAML rather than GitLab username/password. Members who do not have an IdP account are removed from the group automatically. Configure your IdP (Okta, Entra ID) to enforce MFA before issuing SAML assertions to GitLab. For self-managed: Admin Area > Settings > General > Sign-in restrictions > configure LDAP or SAML as the authentication method.
Migrate to next-generation runner token model
GitLab 16.0+ deprecated the shared runner registration token in favor of an authentication token per runner. Migrated runners use a token generated during registration rather than a reusable group/instance registration token. Verify migration status: Admin Area > Runners > check that no runners show 'registration token' authentication. Old-style runners that share a registration token allow any project Maintainer who can view the token to register additional (potentially malicious) runners under the same group or project.
CI/CD Security: Job Token Scope and Runner Configuration
The CI_JOB_TOKEN variable is automatically injected into every CI/CD job and provides API access to the GitLab instance. Its scope determines what the token can access.
Enable CI/CD job token scope allowlist
By default, CI_JOB_TOKEN can access any project in the same GitLab instance that the job's project can access. Enable the allowlist to restrict which projects a job token can interact with: Project > Settings > CI/CD > Job token permissions > enable 'Limit access to this project' > add only the specific projects this project's CI/CD legitimately needs to access (typically: package registry, container registry of dependency projects). This prevents a compromised job in one project from reading code or artifacts from unrelated projects.
Run CI/CD jobs with least-privilege runner configuration
Self-managed runners should be configured with: docker executor (not shell executor: shell executor gives CI/CD jobs access to the runner host's file system and environment variables), no privileged mode unless required for Docker-in-Docker builds, and network restrictions preventing jobs from accessing internal services not required by the build. Privileged Docker runners can mount the host file system and escape the container: use rootless containerd or Kaniko for container image builds instead.
Protect runner registration to specific groups and projects
Group runners and project runners should be registered only to the groups and projects that need them. A high-privilege runner (with production AWS credentials or Kubernetes deployment access) should be registered as a project runner to the specific deployment project, not as a group or instance runner accessible to all projects. Tag runners by environment (prod-runner, staging-runner) and require tags in pipeline jobs that need specific runners: only runs on: [tags: [prod-runner]].
Briefings like this, every morning before 9am.
Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.
Protected Branches and Protected Environments
GitLab's protected branches prevent direct pushes to main and require merge requests. Protected environments add approval gates for deployments to specific environments.
Protect main and release branches
Project > Settings > Repository > Protected Branches: add main, master, release/*, and any production deployment branches. Configure: Allowed to merge = Maintainers, Allowed to push = No one (forces all changes through MRs), Allowed to force push = disabled, Code owner approval required = enabled (if CODEOWNERS file exists). This prevents developers from bypassing code review with direct pushes and prevents force-push history rewrites on production branches.
Configure protected environments for production deployments
Project > Settings > CI/CD > Protected Environments: add production (or your production environment name). Configure: Allowed to deploy = specific users or Maintainer+ role, Required approvals = 2 (requires two reviewers to approve the deployment pipeline). This means even if a CI/CD pipeline builds successfully, a deployment to production requires explicit human approval from named approvers: preventing an accidental or malicious deployment from a compromised pipeline.
Enable merge request approval rules
Project > Settings > Merge Requests > Merge Request Approvals: set minimum approvals required = 2 for projects deploying to production. Enable 'Prevent approval by author' and 'Prevent approval by committers' to prevent self-approval. For high-security projects, add a CODEOWNERS file mapping sensitive directories (infrastructure/, secrets/, .github/workflows/) to security team reviewers who must approve MRs that touch those files.
GitLab Security Scanning: SAST, DAST, and Dependency Scanning
GitLab Ultimate and Premium include built-in security scanners that run in CI/CD pipelines and report findings to the Security Dashboard. Enable these as part of your CI/CD template.
Enable SAST in your CI/CD pipeline
Add to your .gitlab-ci.yml: include: - template: Security/SAST.gitlab-ci.yml. GitLab SAST automatically detects the project's language and runs the appropriate analyzer (Semgrep for Python/JS/Go/Java, SpotBugs for Java, Brakeman for Rails, etc.). SAST findings appear in the Merge Request security widget and are tracked in the Security Dashboard. Configure the SAST_EXCLUDED_PATHS variable to skip test directories: SAST_EXCLUDED_PATHS: spec,test,tests,tmp.
Enable Dependency Scanning and configure critical severity blocking
Add: include: - template: Security/Dependency-Scanning.gitlab-ci.yml. Dependency scanning checks all dependencies against the GitLab Advisory Database (based on OSV). To fail the pipeline on critical vulnerabilities: add a security policy in Project > Security > Policies > Scan Result Policies: fail the MR when critical vulnerabilities are detected in new code. This prevents the merge of code with known-critical dependency vulnerabilities.
Enable Secret Detection for pre-push scanning
Add: include: - template: Security/Secret-Detection.gitlab-ci.yml. GitLab's Secret Detection scanner checks for API keys, passwords, and tokens in commit diffs. Configure push rules to block secret-containing commits at the project level: Project > Settings > Repository > Push Rules > Prevent pushing secret files = enabled, Deny deleting tag = enabled. Push rules are a pre-receive hook that blocks the push before it enters the repository.
The bottom line
Self-managed GitLab's three most critical hardening actions are: restrict default project visibility to Internal or Private (prevents public exposure of internal code), enable CI/CD job token scope allowlisting (prevents lateral movement via compromised job tokens), and configure protected branches on main with no-direct-push enforced (prevents bypassing code review). For GitLab.com organizations, enforce SAML SSO with MFA at the group level and configure protected environments with required approval gates for production: these two controls prevent credential compromise from directly enabling a production deployment.
Frequently asked questions
What is the GitLab CI_JOB_TOKEN and what can it access?
CI_JOB_TOKEN is a short-lived authentication token automatically created by GitLab for every CI/CD job. By default (before scope allowlisting), the job token can be used to authenticate against the GitLab API with the same project-level permissions as the pipeline's triggering user. This means it can: clone other repositories the pipeline user has access to, download packages from group package registries, interact with the container registry, and call the GitLab API for project-level operations. If a CI/CD job is compromised (malicious code in a dependency, RCE via a build tool), an attacker with the CI_JOB_TOKEN can move laterally to access other projects' code and artifacts. Enabling the job token scope allowlist restricts this to explicitly permitted projects.
What is the difference between GitLab runners: shared, group, and project?
Shared runners are available to every project on the GitLab instance (or GitLab.com: Saas shared runners). Group runners are registered to a specific group and available to all projects in that group. Project runners are registered to a single project. Security implication: a high-privilege runner (with production cloud credentials) should always be a project runner, never a shared or group runner. Shared runners are appropriate for ephemeral build tasks that need only the repository code; production deployment runners need credentials restricted to the deployment project. Audit runner configuration by checking: Admin Area > Runners > filter by type to ensure high-privilege runners are tagged and scoped correctly.
How do protected branches work with CI/CD in GitLab?
Protected branches in GitLab prevent direct pushes to specified branches (typically main, release/*) and can require merge requests for all changes. When a branch is fully protected (Allowed to push = No one), developers must create a feature branch, push their changes there, and open a Merge Request targeting the protected branch. The CI/CD pipeline runs on the MR source branch. When the MR is approved and merged, the pipeline can run again on the protected branch if configured. This means no code reaches main without passing CI/CD and human review. Protected environment gates add an additional approval step before deployment pipelines deploy to production from the protected branch.
What GitLab configuration reduces supply chain attack risk?
Key supply chain controls: (1) Pin all CI/CD template inclusions to specific commit SHAs rather than branch refs: include: project: gitlab-org/pipeline-templates ref: sha256:abc123 prevents a compromised template from auto-propagating; (2) Use dependency pinning with hash verification in package managers (poetry.lock with hash checking, package-lock.json, Cargo.lock); (3) Enable Dependency Scanning to detect compromised package versions; (4) Use GitLab's package registry as a proxy to cache approved package versions rather than pulling directly from npmjs.com or PyPI at build time; (5) Enable required CI/CD pipeline configuration from a parent group's .gitlab-ci.yml to ensure all projects use organization-approved templates.
How do I audit GitLab access for a departing employee?
GitLab access audit for offboarding: (1) Deactivate the user account (Admin Area > Users > [user] > Deactivate): this revokes all tokens, invalidates all sessions, and removes the user from all projects and groups while preserving their authored commits and MRs; (2) Revoke personal access tokens: Admin Area > Users > [user] > Personal Access Tokens > revoke all; (3) Review and revoke deployed SSH keys: Admin Area > Keys > filter by user; (4) Check if the user was a CI/CD runner registration token holder and rotate affected runner registration tokens; (5) Review group and project ownership and transfer ownership of groups the user owned to another maintainer before deactivation. Do not delete GitLab users: deletion removes attributed commit history.
Is GitLab SAST as effective as commercial SAST tools like Checkmarx or Veracode?
GitLab SAST is effective for high-confidence, low-false-positive findings (SQL injection, XSS, command injection, hardcoded credentials) but has lower coverage depth than commercial SAST tools. GitLab SAST uses Semgrep as the primary analyzer, which excels at pattern-based detection but does not perform taint analysis (data flow tracking from source to sink) as thoroughly as Checkmarx or Veracode. For teams that need compliance evidence (PCI DSS Requirement 6.3, OWASP SAMM), commercial SAST provides deeper analysis and better audit documentation. For teams that want automated security gates in CI/CD at zero additional cost (GitLab Ultimate is already licensed), GitLab SAST provides meaningful coverage and integrates natively with the Security Dashboard and Merge Request workflow.
What GitLab audit logs should I forward to my SIEM?
Priority GitLab audit log events for SIEM: (1) User authentication events (login, failed login, password change, 2FA disable); (2) Group and project membership changes (user added, role changed, user removed: especially Maintainer+ grants); (3) Protected branch and environment configuration changes (protection removed or loosened); (4) CI/CD pipeline trigger events for production environments; (5) Runner registration and deletion events; (6) Personal access token creation and revocation; (7) Admin area configuration changes. GitLab ships audit events via the GitLab audit events API (/api/v4/audit_events) and via streaming audit events (Admin Area > Monitoring > Audit Event Streaming) to HTTP destinations. Splunk's GitLab Add-on and Sentinel's GitLab connector consume these streams.
Sources & references
Free resources
Critical CVE Reference Card 2025–2026
25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.
Ransomware Incident Response Playbook
Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.
Get threat intel before your inbox does.
50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, daily, no spam.
Unsubscribe anytime. We never sell your data.

Founder & Cybersecurity Evangelist, Decryption Digest
Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals every morning.
Win a $2,495 Black Hat pass.
Full-access to Black Hat USA 2026 in Las Vegas. Subscribe free to enter.
