Skip to content
Casey Labs

GitLab is where many developers experience the platform. They push branches, open merge requests, wait for pipelines, review scan results, and merge through protected branches. That makes GitLab a high-leverage place to enforce delivery rules.

In this platform, GitLab is not treated as a UI where administrators manually craft one-off project settings. Terraform models the baseline, and GitLab enforces the workflow.

For readers new to GitLab, these are the main concepts used in this guide:

Concept Meaning
Project A repository plus issues, merge requests, CI/CD, settings, and security features
Group A namespace that can contain projects and subgroups
Merge request A proposed code change with discussion, review, approvals, and pipeline results
Protected branch A branch with restricted push, merge, or pipeline behavior
Runner The agent that executes CI jobs
Security policy A centrally managed rule that can inject pipelines, require scans, or require approvals

Governance means deciding which of those settings should be consistent across the organization, which should be delegated to teams, and which should require exceptions.

The governance target is straightforward: reduce permission sprawl while making required controls hard to bypass.

Permission sprawl happens when too many people need broad access to get normal work done. If every team needs Maintainer access to ship, the platform has converted delivery friction into permission risk. A better platform gives teams self-service delivery paths while reserving elevated access for rare, audited cases.

Required controls should also be difficult to remove accidentally. A scanner that lives only in a copied CI template is easy to delete. A policy-injected pipeline is harder to bypass. A branch protection rule created by Terraform is easier to audit than a checkbox changed by hand.

The project models GitLab groups and projects through Terraform. A simplified excerpt from the foundation module shows the intent:

resource "gitlab_project" "this" {
for_each = local.projects
name = each.value.name
namespace_id = gitlab_group.this[each.value.group_key].id
default_branch = each.value.default_branch
visibility_level = each.value.visibility_level
merge_method = "ff"
only_allow_merge_if_pipeline_succeeds = true
only_allow_merge_if_all_discussions_are_resolved = true
lifecycle {
prevent_destroy = true
}
}

This makes project creation reviewable and repeatable. It also puts guardrails on destructive changes. A project is not casually destroyed because a variable disappeared from a local file.

Protected branches are also modeled:

resource "gitlab_branch_protection" "this" {
for_each = local.protected_branches
project = gitlab_project.this[each.value.project_ref].id
branch = each.value.branch
push_access_level = each.value.push_access_level
merge_access_level = each.value.merge_access_level
}

The exact access levels can vary by organization, but the pattern should not: branch protection is part of platform state, not a manual checklist item.

The platform assumes Active Directory or another enterprise identity provider is the source of truth for users and group membership. GitLab consumes that identity through SSO and SCIM. Access should be group-based, not assigned project by project.

The default developer path is intentionally narrow:

  • Developers usually receive the Developer role.
  • Teams delegate code ownership and delivery decisions through CODEOWNERS, approval rules, and default reviewers.
  • Senior team members act as CODEOWNERS and reviewers for sensitive areas.
  • The platform team owns shared GitLab policies, runners, CI/CD components, templates, and baseline project settings.
  • Maintainer access is exceptional, not the normal way to ship.

This distinction matters for both security and developer experience. If developers need elevated access to perform common tasks, they will ask for it often. If the platform provides a working delivery path, elevated access can stay rare.

The first access-control audit should answer a blunt question: who has Maintainer or Owner access today, and why?

Common reasons include repository settings, CI/CD variables, protected branches, release management, runner configuration, or emergency deployment work. Some of those reasons are real. Many are leftovers from a time when the platform did not offer a narrower path.

The preferred remediation is least privilege:

  • Remove dormant users and stale direct memberships.
  • Move project-level access to group-based access.
  • Replace broad Maintainer access with custom roles where GitLab supports the needed permission set.
  • Create narrow roles such as Release Manager for release-specific work.
  • Review Owners, Maintainers, custom roles, and service accounts on a recurring schedule.

GitLab security policies carry controls that must be injected or enforced consistently. The reference policy project uses a pipeline execution policy to inject the platform pipeline, scan execution policy for mandatory scanners, and approval policy for high-risk findings:

pipeline_execution_policy:
- name: Enforce paved-road platform pipeline
enabled: true
pipeline_config_strategy: inject_policy
content:
include:
- project: platform/security-policy-project
ref: main
file: pipeline-policies/enforced-controls.yml
approval_policy:
- name: Block critical and high security findings
enabled: true
rules:
- type: scan_finding
vulnerabilities_allowed: 0
severity_levels:
- critical
- high

Policy is not copied into every application repository. The policy project becomes the enforcement surface, and application projects inherit the platform contract.

No governance model survives without exceptions. A team may need a temporary scanner waiver, a special runner, or a protected variable for a migration. The platform needs a visible path for that work.

Exceptions must be visible and time-bound. They should have an owner, reason, review date, and evidence trail. Hidden exceptions are worse than no exceptions policy because they teach teams that the real process lives outside the platform.

Good GitLab governance does not mean every repository looks identical. It means every repository has the same minimum delivery contract: protected refs, required scans, approval gates, standard pipeline stages, and a traceable path for exceptions.

GitLab is strongest when it enforces the workflow. Terraform is strongest when it models the baseline. Keeping that boundary clear is what makes governance scalable.

That governance model has to show up in a pipeline teams can actually use.