v0.1.0-draft AI Drafted

GitLab Hardening Guide

DevOps Last updated: 2026-02-19

DevOps platform security for CI/CD pipelines, repository access, and runners

Overview

GitLab is used by 50%+ of Fortune 100 with 30,000+ paying customers. Integrated CI/CD pipelines, container registry, and secrets management concentrate attack surface. Runner tokens, project API keys, and OAuth integrations with cloud providers enable code injection and infrastructure access. A compromised GitLab instance provides attackers with source code, CI/CD secrets, and deployment capabilities.

Intended Audience

  • Security engineers hardening GitLab instances
  • DevOps engineers configuring CI/CD security
  • GRC professionals assessing DevSecOps compliance
  • Platform teams managing GitLab infrastructure

How to Use This Guide

  • L1 (Baseline): Essential controls for all organizations
  • L2 (Hardened): Enhanced controls for security-sensitive environments
  • L3 (Maximum Security): Strictest controls for regulated industries

Scope

This guide covers GitLab security configurations including authentication, CI/CD pipeline security, runner hardening, and third-party integration controls.


Table of Contents

  1. Authentication & Access Controls
  2. CI/CD Pipeline Security
  3. Runner Security
  4. Repository Security
  5. Secrets Management
  6. Monitoring & Detection
  7. Compliance Quick Reference

1. Authentication & Access Controls

1.1 Enforce SSO with MFA

Profile Level: L1 (Baseline) CIS Controls: 6.3, 6.5 NIST 800-53: IA-2(1)

Description

Require SAML/OIDC SSO with MFA for all GitLab authentication, eliminating password-based access.

Rationale

Why This Matters:

  • GitLab credentials provide access to source code and CI/CD pipelines
  • Compromised accounts can inject malicious code
  • SSO enables centralized access control and MFA enforcement

Attack Scenario: Malicious .gitlab-ci.yml injects backdoor during build; stolen runner token enables unauthorized deployments.

ClickOps Implementation (GitLab.com Premium/Ultimate)

Step 1: Configure SAML SSO

  1. Navigate to: Group → Settings → SAML SSO
  2. Configure:
    • Identity provider SSO URL: Your IdP endpoint
    • Certificate fingerprint: From IdP
    • Enforce SSO: Enable
  3. Click Save changes

Step 2: Enforce Group-Managed Accounts

  1. Navigate to: Group → Settings → SAML SSO
  2. Enable: Enforce SSO-only authentication for web activity
  3. Enable: Enforce SSO-only authentication for Git and Dependency Proxy activity

Step 3: Disable Password Authentication

  1. Navigate to: Admin → Settings → General → Sign-in restrictions
  2. Disable: Password authentication enabled for web interface
  3. Disable: Password authentication enabled for Git over HTTP(S)

Code Implementation

Code Pack: CLI Script
hth-gitlab-1.01-configure-saml-sso.rb View source on GitHub ↗
# /etc/gitlab/gitlab.rb

# SAML Configuration
gitlab_rails['omniauth_enabled'] = true
gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
gitlab_rails['omniauth_block_auto_created_users'] = false
gitlab_rails['omniauth_providers'] = [
  {
    name: 'saml',
    args: {
      assertion_consumer_service_url: 'https://gitlab.company.com/users/auth/saml/callback',
      idp_cert_fingerprint: 'XX:XX:XX...',
      idp_sso_target_url: 'https://idp.company.com/saml/sso',
      issuer: 'https://gitlab.company.com',
      name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
    }
  }
]

# Disable password authentication
gitlab_rails['gitlab_signin_enabled'] = false

Compliance Mappings

Framework Control ID Control Description
SOC 2 CC6.1 Logical access controls
NIST 800-53 IA-2(1) MFA for network access

1.2 Implement Granular Project Permissions

Profile Level: L1 (Baseline) NIST 800-53: AC-3, AC-6

Description

Configure project-level access controls using GitLab’s role-based permissions.

ClickOps Implementation

Step 1: Define Role Strategy

Role Permissions Use Case
Guest View issues, wiki External stakeholders
Reporter Clone, view CI/CD QA, read-only developers
Developer Push to non-protected branches Development team
Maintainer Merge to protected, manage CI/CD Tech leads
Owner Full control Project owners only

Step 2: Configure Protected Branches

  1. Navigate to: Project → Settings → Repository → Protected branches
  2. Protect main and release/*:
    • Allowed to merge: Maintainers
    • Allowed to push: No one (force MR workflow)
    • Require approval from code owners: Enable

Step 3: Enable Required Approvals

  1. Navigate to: Project → Settings → Merge requests
  2. Configure:
    • Approvals required: 2 (minimum)
    • Prevent approval by author: Enable
    • Prevent editing approval rules: Enable

1.3 Configure Personal Access Token Policies

Profile Level: L1 (Baseline) NIST 800-53: IA-5

Description

Restrict personal access token (PAT) creation and enforce expiration policies.

ClickOps Implementation

Step 1: Set Token Expiration Limits

  1. Navigate to: Admin → Settings → General → Account and limit
  2. Configure:
    • Maximum allowable lifetime for access tokens: 90 days
    • Limit project access token creation: Enable

Step 2: Disable API Scope for Non-Essential Tokens

  • Audit tokens with api scope
  • Replace with minimal scopes (read_repository, write_repository)

Code Pack: API Script
hth-gitlab-1.03-configure-personal-access-token-policies.sh View source on GitHub ↗
# List all active personal access tokens and flag risky configurations
info "1.3 Retrieving active personal access tokens..."
PAGE=1
ALL_PATS="[]"
while true; do
  RESPONSE=$(gl_get "/personal_access_tokens?state=active&per_page=100&page=${PAGE}" 2>/dev/null) || break
  COUNT=$(echo "${RESPONSE}" | jq 'length' 2>/dev/null || echo "0")
  [ "${COUNT}" -eq 0 ] && break
  ALL_PATS=$(echo "${ALL_PATS} ${RESPONSE}" | jq -s 'add')
  PAGE=$((PAGE + 1))
done

TOTAL=$(echo "${ALL_PATS}" | jq 'length' 2>/dev/null || echo "0")
info "1.3 Found ${TOTAL} active personal access token(s)"

# Flag tokens with overly broad 'api' scope
API_SCOPE_PATS=$(echo "${ALL_PATS}" | jq '[.[] | select(.scopes | index("api"))]' 2>/dev/null || echo "[]")
API_SCOPE_COUNT=$(echo "${API_SCOPE_PATS}" | jq 'length' 2>/dev/null || echo "0")

if [ "${API_SCOPE_COUNT}" -gt 0 ]; then
  warn "1.3 Found ${API_SCOPE_COUNT} token(s) with full 'api' scope (overly permissive)"
  echo "${API_SCOPE_PATS}" | jq -r '.[] | "  - \(.name // "unnamed") (user: \(.user_id // "unknown"), created: \(.created_at // "unknown"))"' 2>/dev/null || true
fi

# Flag tokens with no expiration date
NO_EXPIRY_PATS=$(echo "${ALL_PATS}" | jq '[.[] | select(.expires_at == null)]' 2>/dev/null || echo "[]")
NO_EXPIRY_COUNT=$(echo "${NO_EXPIRY_PATS}" | jq 'length' 2>/dev/null || echo "0")

if [ "${NO_EXPIRY_COUNT}" -gt 0 ]; then
  warn "1.3 Found ${NO_EXPIRY_COUNT} token(s) with no expiration date"
  echo "${NO_EXPIRY_PATS}" | jq -r '.[] | "  - \(.name // "unnamed") (user: \(.user_id // "unknown"), scopes: \(.scopes | join(", ")))"' 2>/dev/null || true
fi

# Flag tokens with write_repository scope (supply chain risk)
WRITE_REPO_PATS=$(echo "${ALL_PATS}" | jq '[.[] | select(.scopes | index("write_repository"))]' 2>/dev/null || echo "[]")
WRITE_REPO_COUNT=$(echo "${WRITE_REPO_PATS}" | jq 'length' 2>/dev/null || echo "0")

if [ "${WRITE_REPO_COUNT}" -gt 0 ]; then
  warn "1.3 Found ${WRITE_REPO_COUNT} token(s) with 'write_repository' scope"
  echo "${WRITE_REPO_PATS}" | jq -r '.[] | "  - \(.name // "unnamed") (user: \(.user_id // "unknown"), expires: \(.expires_at // "never"))"' 2>/dev/null || true
fi
Code Pack: Sigma Detection Rule
hth-gitlab-1.03-configure-personal-access-token-policies.yml View source on GitHub ↗
detection:
    selection:
        entity_type: 'PersonalAccessToken'
        action: 'create'
    condition: selection
fields:
    - author_name
    - entity_path
    - target_details
    - ip_address
    - created_at

2. CI/CD Pipeline Security

2.1 Protect CI/CD Variables

Profile Level: L1 (Baseline) NIST 800-53: SC-28

Description

Configure CI/CD variables with appropriate protection levels and masking.

ClickOps Implementation

Step 1: Configure Variable Protection

  1. Navigate to: Project → Settings → CI/CD → Variables
  2. For each sensitive variable:
    • Protect variable: Enable (only available in protected branches)
    • Mask variable: Enable (hidden in job logs)
    • Expand variable reference: Disable

Step 2: Use Group-Level Variables

  1. Navigate to: Group → Settings → CI/CD → Variables
  2. Define shared secrets at group level
  3. Limit duplication across projects

Step 3: Environment-Scoped Variables

  1. Create separate variables for each environment:
    • PROD_API_KEY (protected)
    • STAGING_API_KEY
  2. Scope to specific environments

Code Implementation

Code Pack: API Script
hth-gitlab-2.01-protect-cicd-variables.sh View source on GitHub ↗
# Retrieve all project-level CI/CD variables and check protection settings
VARIABLES=$(gl_get "/projects/${PROJECT_ID}/variables" 2>/dev/null) || {
  fail "2.1 Failed to retrieve CI/CD variables -- check PROJECT_ID and token permissions"
  increment_failed
  summary
  exit 0
}

VAR_COUNT=$(echo "${VARIABLES}" | jq 'length' 2>/dev/null || echo "0")
info "2.1 Found ${VAR_COUNT} CI/CD variable(s)"

UNPROTECTED=0
UNMASKED=0
RAW_EXPOSED=0

echo "${VARIABLES}" | jq -c '.[]' 2>/dev/null | while IFS= read -r var; do
  KEY=$(echo "${var}" | jq -r '.key')
  PROTECTED=$(echo "${var}" | jq -r '.protected')
  MASKED=$(echo "${var}" | jq -r '.masked')
  RAW=$(echo "${var}" | jq -r '.raw // false')

  ISSUES=""
  if [ "${PROTECTED}" != "true" ]; then
    ISSUES="${ISSUES} unprotected"
  fi
  if [ "${MASKED}" != "true" ]; then
    ISSUES="${ISSUES} unmasked"
  fi
  if [ "${RAW}" == "true" ]; then
    ISSUES="${ISSUES} raw-exposed"
  fi

  if [ -n "${ISSUES}" ]; then
    warn "2.1 Variable '${KEY}':${ISSUES}"
  else
    pass "2.1 Variable '${KEY}': protected + masked"
  fi
done

# Summary counts (re-parse for totals since while-loop runs in subshell)
UNPROTECTED=$(echo "${VARIABLES}" | jq '[.[] | select(.protected != true)] | length' 2>/dev/null || echo "0")
UNMASKED=$(echo "${VARIABLES}" | jq '[.[] | select(.masked != true)] | length' 2>/dev/null || echo "0")
RAW_EXPOSED=$(echo "${VARIABLES}" | jq '[.[] | select(.raw == true)] | length' 2>/dev/null || echo "0")

info "2.1 Unprotected: ${UNPROTECTED}, Unmasked: ${UNMASKED}, Raw-exposed: ${RAW_EXPOSED}"
Code Pack: CLI Script
hth-gitlab-2.01-secure-variable-usage.yml View source on GitHub ↗
# .gitlab-ci.yml - Secure variable usage

variables:
  # Never hardcode secrets
  # Reference protected CI/CD variables

deploy_production:
  stage: deploy
  script:
    - echo "Deploying with protected credentials"
    - ./deploy.sh  # Uses $PROD_API_KEY from CI/CD settings
  environment:
    name: production
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  # Only run on protected branch with protected variables

2.2 Implement Pipeline Security Controls

Profile Level: L1 (Baseline) NIST 800-53: CM-7, SI-7

Description

Restrict pipeline execution and prevent unauthorized CI/CD modifications.

ClickOps Implementation

Step 1: Require Pipeline Approval for Forks

  1. Navigate to: Project → Settings → CI/CD → General pipelines
  2. Enable: Protect CI/CD variables in pipeline subscriptions
  3. Enable: CI/CD job token scope: Limit access to necessary projects

Step 2: Configure Merge Request Pipelines

  1. Navigate to: Project → Settings → Merge requests
  2. Enable: Pipelines must succeed before merge
  3. Enable: All discussions must be resolved

Step 3: Limit Who Can Run Pipelines

  1. Navigate to: Project → Settings → CI/CD
  2. Configure: Who can run pipelines on protected branches
  3. Restrict manual job triggers

2.3 Harden .gitlab-ci.yml Configuration

Profile Level: L2 (Hardened) NIST 800-53: CM-7

Description

Implement secure CI/CD configuration practices. See the CLI Code Pack below for a security-hardened .gitlab-ci.yml example.

Code Pack: CLI Script
hth-gitlab-2.03-hardened-ci-config.yml View source on GitHub ↗
# .gitlab-ci.yml - Security hardened example

default:
  # Use specific image tags, not :latest
  image: ruby:3.2.0-alpine@sha256:abc123...

  # Limit job timeout
  timeout: 30 minutes

  # Run in isolated environment
  tags:
    - docker
    - isolated

# Prevent secret leakage in logs
variables:
  GIT_STRATEGY: clone
  SECURE_LOG_LEVEL: "warn"

# Security scanning stages
stages:
  - test
  - security
  - build
  - deploy

sast:
  stage: security
  allow_failure: false  # Block on security issues

dependency_scanning:
  stage: security
  allow_failure: false

container_scanning:
  stage: security
  allow_failure: false

# Restrict production deployment
deploy_production:
  stage: deploy
  script:
    - ./deploy.sh
  environment:
    name: production
    url: https://prod.company.com
  rules:
    # Only from main branch
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual  # Require manual approval
  # Prevent concurrent deployments
  resource_group: production

3. Runner Security

3.1 Isolate CI/CD Runners

Profile Level: L1 (Baseline) NIST 800-53: SC-7

Description

Deploy isolated runners for different trust levels and environments.

Implementation

Step 1: Create Runner Tiers

  1. shared-runners – general use, Docker executor, ephemeral containers
  2. group-runners – team-specific, isolated per business unit
  3. project-runners – sensitive projects, dedicated to single project
  4. production-runners – deployment only, network access to production, limited users
Code Pack: CLI Script
hth-gitlab-3.01-isolate-cicd-runners.sh View source on GitHub ↗
# Register runner with specific tags
gitlab-runner register \
  --url "https://gitlab.company.com" \
  --registration-token "${RUNNER_TOKEN}" \
  --executor "docker" \
  --docker-image "alpine:3.18" \
  --tag-list "isolated,security-sensitive" \
  --run-untagged="false" \
  --locked="true"
[[runners]]
  name = "secure-runner"
  executor = "docker"
  [runners.docker]
    image = "alpine:3.18"
    privileged = false  # Never enable unless absolutely required
    disable_entrypoint_overwrite = true
    volumes = ["/cache"]
    # Limit network access
    network_mode = "bridge"
    # Read-only root filesystem
    read_only = true
    # Drop capabilities
    cap_drop = ["ALL"]

3.2 Rotate Runner Tokens

Profile Level: L1 (Baseline) NIST 800-53: IA-5(1)

Description

Implement regular runner token rotation to limit exposure from compromised tokens.

ClickOps Implementation

Step 1: Reset Runner Token

  1. Navigate to: Admin → CI/CD → Runners → [Runner]
  2. Click Reset registration token
  3. Update runner configuration with new token
Code Pack: CLI Script
hth-gitlab-3.02-rotate-runner-token.sh View source on GitHub ↗
# Reset project runner token
curl -X POST -H "PRIVATE-TOKEN: ${ADMIN_TOKEN}" \
  "https://gitlab.company.com/api/v4/projects/${PROJECT_ID}/runners/reset_registration_token"

# Re-register runner
gitlab-runner unregister --all-runners
gitlab-runner register --non-interactive \
  --url "https://gitlab.company.com" \
  --registration-token "${NEW_TOKEN}" \
  --executor "docker"

4. Repository Security

4.1 Enable Push Rules

Profile Level: L1 (Baseline) NIST 800-53: CM-3

Description

Configure push rules to prevent accidental secret commits and enforce commit hygiene.

ClickOps Implementation

Step 1: Configure Project Push Rules

  1. Navigate to: Project → Settings → Repository → Push rules
  2. Enable:
    • Prevent pushing secret files: Enable
    • Reject unsigned commits: Enable (L2)
    • Check author email against verified: Enable

Step 2: Configure Secret Detection

See the CLI Code Pack below for the .gitlab-ci.yml secret detection configuration.

Code Pack: API Script
hth-gitlab-4.01-enable-push-rules.sh View source on GitHub ↗
# Configure push rules: L1 enables prevent_secrets and deny_delete_tag;
# L2 additionally enables reject_unsigned_commits for commit signing enforcement.
info "4.1 Configuring push rules..."

PAYLOAD='{
  "prevent_secrets": true,
  "deny_delete_tag": true'

# L2: Add reject_unsigned_commits
if should_apply 2 2>/dev/null; then
  info "4.1 L2: Enabling reject_unsigned_commits (commit signing required)"
  PAYLOAD="${PAYLOAD}"',"reject_unsigned_commits": true'
fi

PAYLOAD="${PAYLOAD}"'}'

if [ -n "${EXISTING}" ] && [ "${EXISTING}" != "null" ]; then
  # Update existing push rules
  RESULT=$(gl_put "/projects/${PROJECT_ID}/push_rule" "${PAYLOAD}" 2>/dev/null) || {
    fail "4.1 Failed to update push rules"
    increment_failed
    summary
    exit 0
  }
else
  # Create new push rules
  RESULT=$(gl_post "/projects/${PROJECT_ID}/push_rule" "${PAYLOAD}" 2>/dev/null) || {
    fail "4.1 Failed to create push rules"
    increment_failed
    summary
    exit 0
  }
fi
Code Pack: CLI Script
hth-gitlab-4.01-secret-detection.yml View source on GitHub ↗
# .gitlab-ci.yml
secret_detection:
  stage: security
  variables:
    SECRET_DETECTION_HISTORIC_SCAN: "true"
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Code Pack: Sigma Detection Rule
hth-gitlab-4.01-enable-push-rules.yml View source on GitHub ↗
detection:
    selection:
        entity_type: 'PushRule'
        action:
            - 'create'
            - 'update'
            - 'destroy'
    condition: selection
fields:
    - author_name
    - entity_path
    - target_details
    - ip_address
    - created_at

4.2 Enable Commit Signing

Profile Level: L2 (Hardened) NIST 800-53: AU-10

Description

Require GPG or SSH signed commits to verify commit authorship.

ClickOps Implementation

Step 1: Configure Signature Requirements

  1. Navigate to: Project → Settings → Repository → Push rules
  2. Enable: Reject unsigned commits
  3. Enable: Reject unverified users

Step 2: User Setup

  1. Navigate to: User Settings → GPG Keys
  2. Add GPG public key
  3. Configure git client (see CLI Code Pack below)
Code Pack: CLI Script
hth-gitlab-4.02-commit-signing.sh View source on GitHub ↗
git config --global commit.gpgsign true
git config --global user.signingkey YOUR_KEY_ID

5. Secrets Management

5.1 Use External Secrets Management

Profile Level: L2 (Hardened) NIST 800-53: SC-28

Description

Integrate with external secrets managers instead of storing secrets in GitLab.

HashiCorp Vault Integration

Code Pack: CLI Script
hth-gitlab-5.01-vault-integration.yml View source on GitHub ↗
# .gitlab-ci.yml
deploy:
  stage: deploy
  secrets:
    DATABASE_PASSWORD:
      vault: production/db/password@secret
    API_KEY:
      vault: production/api/key@secret
  script:
    - echo "Using secrets from Vault"
    - ./deploy.sh

Step 1: Configure Vault Integration

  1. Navigate to: Project → Settings → CI/CD → Secure Files
  2. Configure JWT authentication with Vault
  3. Map CI/CD variables to Vault paths

6. Monitoring & Detection

6.1 Enable Audit Events

Profile Level: L1 (Baseline) NIST 800-53: AU-2, AU-3

Description

Configure comprehensive audit logging for GitLab operations.

ClickOps Implementation

Step 1: Configure Audit Event Streaming

  1. Navigate to: Group → Security & Compliance → Audit events
  2. Enable streaming to SIEM
  3. Configure: All event types

Step 2: Alert on Critical Events

  • Repository deletion
  • Protected branch modification
  • Runner registration
  • Admin privilege changes

Detection Queries

See the DB Code Pack below for SQL queries that detect unusual repository cloning and pipeline variable modifications.

Code Pack: API Script
hth-gitlab-6.01-enable-audit-events.sh View source on GitHub ↗
# Query group-level audit events and verify audit logging is active.
# GitLab Premium/Ultimate exposes audit events via the REST API.
info "6.1 Retrieving recent audit events..."
AUDIT_EVENTS=$(gl_get "/groups/${GROUP_ID}/audit_events?per_page=20" 2>/dev/null) || {
  fail "6.1 Failed to retrieve audit events -- requires GitLab Premium/Ultimate and admin token"
  increment_failed
  summary
  exit 0
}

EVENT_COUNT=$(echo "${AUDIT_EVENTS}" | jq 'length' 2>/dev/null || echo "0")
info "6.1 Retrieved ${EVENT_COUNT} recent audit event(s)"

if [ "${EVENT_COUNT}" -gt 0 ]; then
  # Show recent security-relevant events
  echo "${AUDIT_EVENTS}" | jq -r '.[] | "  - [\(.created_at)] \(.author.name // .author_id): \(.entity_type)/\(.details.action // .details.custom_message // "event")"' 2>/dev/null || true

  # Check for key security event types
  info "6.1 Checking for security-relevant event categories..."
  AUTH_EVENTS=$(echo "${AUDIT_EVENTS}" | jq '[.[] | select(.details.action // "" | test("auth|login|session"; "i"))] | length' 2>/dev/null || echo "0")
  PERM_EVENTS=$(echo "${AUDIT_EVENTS}" | jq '[.[] | select(.details.action // "" | test("permission|role|access"; "i"))] | length' 2>/dev/null || echo "0")
  REPO_EVENTS=$(echo "${AUDIT_EVENTS}" | jq '[.[] | select(.details.action // "" | test("push|merge|branch|tag"; "i"))] | length' 2>/dev/null || echo "0")

  info "6.1 Event breakdown: auth=${AUTH_EVENTS}, permissions=${PERM_EVENTS}, repository=${REPO_EVENTS}"
fi

# Check for audit event streaming destinations (L2)
if should_apply 2 2>/dev/null; then
  info "6.1 L2: Checking external audit event streaming destinations..."
  STREAM_DESTS=$(gl_get "/groups/${GROUP_ID}/audit_events/streaming/destinations" 2>/dev/null || echo "[]")
  DEST_COUNT=$(echo "${STREAM_DESTS}" | jq 'length' 2>/dev/null || echo "0")

  if [ "${DEST_COUNT}" -gt 0 ]; then
    pass "6.1 Found ${DEST_COUNT} audit event streaming destination(s)"
    echo "${STREAM_DESTS}" | jq -r '.[] | "  - \(.destination_url // "unknown") (verification: \(.verification_token | if . then "set" else "unset" end))"' 2>/dev/null || true
  else
    warn "6.1 No external audit event streaming destinations configured"
    warn "6.1 Configure via Settings > General > Audit events > Streaming to forward to your SIEM"
  fi
fi
Code Pack: Sigma Detection Rule
hth-gitlab-6.01-enable-audit-events.yml View source on GitHub ↗
detection:
    selection_destination:
        entity_type: 'ExternalAuditEventDestination'
        action: 'destroy'
    selection_header:
        entity_type: 'AuditEventsStreamingHeader'
        action: 'destroy'
    condition: selection_destination or selection_header
fields:
    - author_name
    - entity_path
    - target_details
    - ip_address
    - created_at

7. Compliance Quick Reference

SOC 2 Mapping

Control ID GitLab Control Guide Section
CC6.1 SSO enforcement 1.1
CC6.2 Project permissions 1.2
CC7.2 Audit events 6.1
CC8.1 Protected branches 1.2

NIST 800-53 Mapping

Control GitLab Control Guide Section
IA-2(1) SSO with MFA 1.1
AC-6 Role-based access 1.2
CM-3 Push rules 4.1
SC-28 CI/CD variable protection 2.1

Appendix A: Edition Compatibility

Control Free Premium Ultimate
SAML SSO
Push Rules Basic
Audit Events
SAST/DAST
Compliance Dashboard

Appendix B: References

Official GitLab Documentation:

API & Developer Tools:

Compliance Frameworks:

Security Incidents:

  • CVE-2023-7028 (Jan 2024): Critical account takeover vulnerability (CVSS 10.0) via password reset emails to unverified addresses; actively exploited in the wild. Patched in GitLab 16.7.2+.
  • Red Hat Consulting GitLab Instance Breach (Sep 2025): Attacker accessed Red Hat’s self-managed GitLab CE instance, exposing consulting data for organizations such as Bank of America, T-Mobile, and U.S. government agencies. GitLab confirmed no breach of its managed SaaS infrastructure.

Community Resources:


Changelog

Date Version Maturity Changes Author
2026-02-19 0.1.2 draft Migrate all remaining inline code to Code Packs (2.1, 2.3, 3.1, 4.1, 4.2, 6.1); zero inline blocks Claude Code (Opus 4.6)
2026-02-19 0.1.1 draft Migrate inline code to CLI Code Packs (1.1, 3.1, 3.2, 5.1) Claude Code (Opus 4.6)
2025-12-14 0.1.0 draft Initial GitLab hardening guide Claude Code (Opus 4.5)