Enforcing Branch Protection and Approval Rules in GitLab

A shared repository without guardrails is an accident waiting to happen. Direct pushes to main, a single junior dev accidentally approving their own merge request, a hotfix that skips CI — you've either seen it, or you're about to. GitLab's branch protection and approval rules are the enforcement layer that makes your workflow auditable, consistent, and safe at scale.

Most teams implement some version of code review policy in their team norms or wikis. The problem is that norms are optional. GitLab's protected branches and approval rules are enforced at the platform level — they can't be bypassed by a distracted dev in a rush, and they produce an audit trail by default.

Key insight
Protected branches aren't just about preventing mistakes — they're about making your process legible to future teammates, auditors, and your future self.

Prerequisites

Before configuring anything, make sure you have:

1
Maintainer or Owner role

Branch protection and approval settings require at minimum the Maintainer role on the project.

2
A project with at least one branch

You'll apply rules to specific branches or wildcard patterns like release/*.

3
GitLab 16.x or later

Some newer features like multiple approval rules require 16.0+. Free tier covers the fundamentals; Premium/Ultimate unlock more granular controls.

Part 1: Protected Branches

A protected branch controls who can push, merge, and force-push to that branch. GitLab applies this at the Git protocol level — even if someone has write access to the repo, protection rules can block specific operations.

Enabling Branch Protection via UI

Navigate to your project and go to Settings → Repository → Protected branches. You'll see a form at the top and a list of any existing protected branches below.

1
Select or type a branch name

You can use wildcards: main, release/*, or v*.*.* all work. Wildcards apply to all matching branches, including ones created in the future.

2
Set "Allowed to merge"

Choose who can merge MRs into this branch: No one, Developers + Maintainers, or Maintainers only. For main, Maintainers only is the strictest option.

3
Set "Allowed to push and merge"

Controls direct pushes (bypassing MRs entirely). For production branches, set this to No one to enforce the MR workflow without exception.

4
Toggle "Allowed to force push"

Should almost always be disabled for shared branches. Force-pushing rewrites history and can silently destroy work.

5
Click "Protect"

The branch now appears in the protected list. Settings take effect immediately.

Configuring via API

If you manage multiple projects or want this in an IaC/automation workflow, use the GitLab REST API:

bash# Protect the 'main' branch — no direct pushes, merge by Maintainers only
curl --request POST \
  --header "PRIVATE-TOKEN: <your_token>" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "main",
    "push_access_level": 0,
    "merge_access_level": 40,
    "allow_force_push": false
  }' \
  "https://gitlab.example.com/api/v4/projects/<project_id>/protected_branches"

Access level codes: 0 = No one, 30 = Developers + Maintainers, 40 = Maintainers, 60 = Admins.

Access Level Reference

Level Code Typical use Tier
No one 0 Lock production — no direct pushes ever Free
Developers + Maintainers 30 Feature branches, staging Free
Maintainers only 40 Release branches Free
Admins only 60 Emergency break-glass access Free
Specific roles/users/groups Fine-grained per-team control Premium
Tip
For release branches that follow semver, use the wildcard pattern v*.*.* once and it covers all future releases automatically.

Part 2: Merge Request Approval Rules

Approval rules define who must approve a merge request before it can be merged. They're separate from branch protection (which controls the merge action itself) — think of approvals as the required signoff step.

Setting Up Approval Rules via UI

Go to Settings → Merge requests → Merge request approvals. You'll find two sections: project-level rules that apply to all MRs, and options to allow individual MRs to add their own rules.

1
Click "Add approval rule"

Give it a descriptive name, like Code Review or Security Team.

2
Set the approvals required count

How many people must approve. For most teams, 1 or 2 is the right number. More than 2 tends to create bottlenecks without adding meaningful safety.

3
Add eligible approvers

Specify individual users, groups, or roles. Using a group (like backend-reviewers) is more maintainable than listing individuals.

4
Save the rule

It applies to all subsequent MRs. Existing open MRs will also pick up the rule.

Key Approval Settings to Lock Down

Just below the rule list, you'll find several checkboxes that are easy to overlook but critical for a secure workflow:

SettingRecommendedWhy it matters
Prevent approval by author ✅ Enable Stops developers from approving their own MRs
Prevent approvals by users who add commits ✅ Enable Stops someone from approving after pushing a sneaky commit
Remove all approvals when commits are added ✅ Enable Forces re-review if the MR is modified after approval
Require user password for approvals Situational Useful for regulated industries; adds friction
Allow MR authors to override rules ❌ Disable Should not be enabled for production workflows
Watch out
"Remove all approvals when commits are added" is disabled by default. Without it, an approved MR can be silently modified — a common vector for slipping in last-minute changes that bypass review.

Multiple Approval Rules (Premium+)

On Premium and Ultimate tiers, you can add multiple named rules with different approver groups. This is useful when you need cross-functional sign-off:

yaml# Example: two approval rules for a security-sensitive service
rules:
  - name: Backend Review
    approvals_required: 1
    approvers:
      - group: backend-engineers

  - name: Security Signoff
    approvals_required: 1
    approvers:
      - group: security-team

Both rules must be satisfied before the MR can merge. This is how you enforce, for example, that every database migration needs both a backend engineer and a DBA to approve.

Part 3: Code Owner Approvals

Code Owners let you tie approvals to specific files or directories — so changes to /infra require the platform team, and changes to /payments require the payments squad, automatically.

Setting Up a CODEOWNERS File

Create a CODEOWNERS file at the root of your repo (or in .gitlab/). The syntax follows .gitignore patterns:

CODEOWNERS# Default: all files require backend-team approval
*                          @backend-team

# Infrastructure changes require platform team
/infra/                    @platform-engineers

# Payment code requires both payments squad and security
/src/payments/             @payments-squad @security-team

# Frontend files require frontend team
*.tsx                      @frontend-engineers
*.css                      @frontend-engineers

# CI config requires DevOps sign-off
.gitlab-ci.yml             @devops-team

After committing the file, go to Settings → Repository → Protected branches and enable "Require approval from code owners" for your target branch. From that point on, any MR touching a file with a defined owner will automatically require approval from that owner before merging.

Tip
The last matching rule in CODEOWNERS wins. Put more specific patterns below general ones, not above.

Part 4: Putting It All Together

Here's a recommended configuration matrix for common branch types in a real-world project:

Branch Push Merge Force Push Approvals Required
main No one Maintainers Disabled 2 + code owner
release/* No one Maintainers Disabled 1 + code owner
develop No one Devs + Maintainers Disabled 1
feature/* Devs Devs Author only 0 (optional)

Validating Your Configuration

After setup, always verify with a real test. Create a test branch, open a draft MR to main, and confirm that:

You can also audit all protected branch rules programmatically:

bash# List all protected branches for a project
curl --header "PRIVATE-TOKEN: <your_token>" \
  "https://gitlab.example.com/api/v4/projects/<project_id>/protected_branches"