The GitLab CI runner is failing to push to a protected branch because the CI_JOB_TOKEN it’s using lacks the necessary permissions. This is the core issue: the token generated for your CI job isn’t inherently authorized to modify protected branches, which are locked down by default for safety.

Common Causes and Fixes

  1. CI_JOB_TOKEN Permissions Not Explicitly Granted:

    • Diagnosis: Navigate to your GitLab project’s Settings > Repository. Under Protected branches, check if the branch your CI is trying to push to is listed. If it is, look at the "Allowed to push" setting for that branch. By default, it’s usually set to "Maintainers" or "No one." The CI_JOB_TOKEN is not automatically granted these permissions.
    • Fix:
      • Go to Settings > Repository > Protected branches.
      • Find the protected branch (e.g., main, develop).
      • For "Allowed to push," select "Maintainers" or "Developers + Maintainers" if you want your CI to push.
      • Why it works: This explicitly grants the necessary role-based access to the branch, which the CI_JOB_TOKEN (associated with the project’s default user role) can then leverage.
      • Command line (if using API/Terraform etc.): You’d typically use the GitLab API to update branch protection rules. For instance, using curl to update the main branch to allow "developers" and "maintainers" to push:
        curl --request PUT "https://gitlab.example.com/api/v4/projects/YOUR_PROJECT_ID/repository/branches/main/protection" \
          --header "PRIVATE-TOKEN: YOUR_ADMIN_ACCESS_TOKEN" \
          --form "push_access_level=30" # 30 for Developers + Maintainers
        
        Replace YOUR_PROJECT_ID with your project’s ID and YOUR_ADMIN_ACCESS_TOKEN with a token that has project administration privileges.
  2. CI Job Token Scope Too Limited (Project Access Token Not Used):

    • Diagnosis: You might be trying to push to a protected branch in a different project (e.g., a deployment project). The default CI_JOB_TOKEN has project-level access only within its own repository.
    • Fix: Create a Project Access Token or a Personal Access Token with write_repository scope in the target project (the one with the protected branch). Then, in your CI/CD settings, configure your pipeline to use this token.
      • Project Access Token: In the target project, go to Settings > Access Tokens. Create a new token, give it a name (e.g., ci-deploy-token), set an expiration date, and grant it the write_repository scope. Copy the generated token immediately.
      • CI/CD Variable: In the source project (where the pipeline runs), go to Settings > CI/CD > Variables. Add a new variable:
        • Key: GITLAB_ACCESS_TOKEN (or any descriptive name)
        • Value: Paste the Project Access Token you just created.
        • Flags: Check "Protect variable" and "Mask variable".
      • Pipeline Configuration: In your .gitlab-ci.yml, use this variable to authenticate the push:
        deploy_job:
          stage: deploy
          script:
            - git remote add deploy "https://oauth2:${GITLAB_ACCESS_TOKEN}@gitlab.example.com/target/project.git"
            - git push deploy HEAD:main
          only:
            - main # Or your deployment trigger branch
        
      • Why it works: This bypasses the default CI_JOB_TOKEN limitations by using a dedicated token with explicit write_repository permissions on the target project. The oauth2 prefix is crucial for authenticating with GitLab using tokens.
  3. Incorrect Branch Name in CI Configuration:

    • Diagnosis: The CI_COMMIT_REF_NAME variable might not be what you expect, or your only/rules configuration is directing the push to a branch that is unexpectedly protected. Double-check which branch your pipeline is actually running for when the push occurs.
    • Fix:
      • Add a echo statement to your CI script to reveal the current branch:
        before_script:
          - echo "Running on branch: $CI_COMMIT_REF_NAME"
        
      • Review your .gitlab-ci.yml only or rules sections to ensure they correctly target the intended branch and that this branch isn’t accidentally protected in a way that prevents pushes.
      • Why it works: This is a debugging step to confirm the environment context. If the branch is wrong, you adjust your only/rules or the trigger for the pipeline. If the branch is correct but protected, you fall back to cause #1.
  4. Runner Permissions (Less Common for Push, More for Checkout):

    • Diagnosis: While less common for pushing (which is usually token-based), ensure your GitLab Runner itself has the necessary permissions to interact with GitLab. This is more relevant if the runner is self-hosted and has network issues or is misconfigured.
    • Fix:
      • If using a shared runner, this is unlikely to be the issue.
      • If using a specific/project runner:
        • Check the runner’s registration token and ensure it’s still valid.
        • Verify the runner can reach your GitLab instance (ping gitlab.example.com).
        • Ensure the runner is not paused or disabled under Admin Area > Runners (for self-hosted) or Settings > CI/CD > Runners (for shared/group runners).
      • Why it works: The runner is the agent executing the job. If it cannot communicate with GitLab properly, it can’t perform actions like pushing code, regardless of token permissions.
  5. Protected Branch Rules Overwritten by Wildcard:

    • Diagnosis: You might have a wildcard protected branch (e.g., release/*) that is inadvertently matching the branch you’re trying to push to, and its push permissions are too restrictive.
    • Fix:
      • Go to Settings > Repository > Protected branches.
      • Examine all branch protection rules, especially any using wildcards (*).
      • If a wildcard rule is matching your target branch (e.g., release/* matching release/v1.0), adjust the "Allowed to push" setting for that wildcard rule, or create a more specific rule for your exact branch that takes precedence.
      • Why it works: GitLab applies the most specific matching rule. If a wildcard rule is catching your branch and has restrictive permissions, you need to either broaden the wildcard’s permissions or create an explicit rule for your branch that grants the necessary push access.
  6. Git Strategy Mismatch (e.g., git clone --depth 1):

    • Diagnosis: Some CI configurations might use shallow clones (git clone --depth 1). While this speeds up cloning, it can sometimes interfere with certain Git operations, though direct push failures are less common. More likely, if you’re doing complex history manipulation before pushing, a shallow clone could be an issue.
    • Fix: In your .gitlab-ci.yml before_script or script section, ensure you’re not using a shallow clone if you need full history, or explicitly fetch necessary history:
      variables:
        GIT_DEPTH: 0 # Fetches full history
      
      deploy_job:
        stage: deploy
        script:
          # ... your push commands ...
      
      • Why it works: Setting GIT_DEPTH: 0 ensures the runner fetches the entire Git history, which can resolve subtle issues with Git operations that rely on complete history, although it’s rarely the direct cause of a permission error.

After fixing the CI_JOB_TOKEN permissions, the next error you might encounter is a 403 Forbidden if the pipeline tries to perform another action that requires higher privileges, such as updating release tags or modifying project settings.

Want structured learning?

Take the full Gitlab-ci course →