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.
Prerequisites
Before configuring anything, make sure you have:
Branch protection and approval settings require at minimum the Maintainer role on the project.
You'll apply rules to specific branches or wildcard patterns like release/*.
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.
You can use wildcards: main, release/*, or v*.*.* all work. Wildcards apply to all matching branches, including ones created in the future.
Choose who can merge MRs into this branch: No one, Developers + Maintainers, or Maintainers only. For main, Maintainers only is the strictest option.
Controls direct pushes (bypassing MRs entirely). For production branches, set this to No one to enforce the MR workflow without exception.
Should almost always be disabled for shared branches. Force-pushing rewrites history and can silently destroy work.
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 |
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.
Give it a descriptive name, like Code Review or Security Team.
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.
Specify individual users, groups, or roles. Using a group (like backend-reviewers) is more maintainable than listing individuals.
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:
| Setting | Recommended | Why 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 |
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.
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:
- Direct pushes to
mainare rejected at the Git level - The author cannot approve their own MR
- Adding a new commit resets existing approvals
- The "Merge" button stays grayed out until all rules are satisfied
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"