Skip to main content

Excessive number of Repo admins

More Info:

Allowing multiple users admin or push access to organization repositories places the organization at risk for contributions that can be pushed without review.

Risk Level

Low

Address

Security

Compliance Standards

  • Cloudanix Best Practice

Triage and Remediation

Remediation

Using Console

Below are concise, step‑by‑step instructions to reduce excessive repository admins in GitHub using the web console (GitHub.com UI).


1. Confirm You Have Permission

You must be:

  • An Organization Owner (for org repos), or
  • A Repository Admin (for personal or org repos)

to change admin permissions.


2. List Current Admins for a Repository

  1. Sign in to GitHub.com.
  2. Open the repository:
    https://github.com/<org-or-user>/<repo-name>
  3. Click Settings (top menu in the repo).
  4. In the left sidebar, click:
    • Collaborators and teams (for org repos: “Manage access”)
  5. Under Manage access, review:
    • Teams with access and their permission level
    • Individuals with access and their permission level

Anyone with Admin permission here is effectively a repo admin.


3. Reduce Individual Admins to Lower Roles

For each user who doesn’t truly need admin rights:

  1. In Settings → Manage access, find the person with Admin access.
  2. Click the gear icon (or three-dot menu) next to that user.
  3. Under Repository access, change:
    • From: Admin
    • To: Maintain (for maintainers) or Write (for regular contributors)
  4. Click Save or Update access.

Repeat for all users who should not be admins.


4. Use Teams Instead of Individual Admins (Org Repos)

If you are in an organization:

  1. Go to the org page: https://github.com/<org>
  2. Click Teams.
  3. Create or choose a team (e.g. repo-maintainers).
  4. Click the team → RepositoriesAdd a repository to this team.
  5. Select the repository and set permission to:
    • Maintain for people who manage issues/PRs but don’t need admin, or
    • Write for regular contributors.
  6. Remove individual Admin access for those now covered by the team (same method as step 3).

5. Remove Unnecessary Teams with Admin Access

  1. In the repository, go to Settings → Manage access.
  2. Under Teams, find any team with Admin level.
  3. Click the gear icon next to the team.
  4. Change permission:
    • From Admin
    • To Maintain or Write, as appropriate.
  5. Save changes.

6. Restrict Who Can Be a Repo Admin at Org Level (Optional)

For organizations to keep admin counts low going forward:

  1. Go to the Organization page.
  2. Click Settings.
  3. In the left sidebar, review:
    • Member privileges / Repository creation / Base permissions.
  4. Set:
    • Base permissions to Read (or lower), not Admin.
    • Restrict who can create repositories or manage settings to Organization owners only.
  5. Ensure only a small set of Organization Owners exist (Org Settings → People → change role from Owner to Member where appropriate).

7. Periodically Audit Admins

  1. On a schedule (e.g. monthly), for each critical repo:
    • Go to Settings → Manage access.
    • Export or screenshot current access.
  2. Remove or downgrade permissions that are no longer needed.

If you share your repo type (personal vs organization) and current number of admins/teams, I can suggest an exact target layout (e.g., “1 owner + 1 maintainers team + write-only contributors”).

Using CLI

Below is a concise, step‑by‑step way to identify and reduce excessive repo admins using the GitHub CLI (gh).

Assumptions:

  • You have gh installed and authenticated (gh auth login).
  • You’re an admin/owner on the repo or org.
  • Replace OWNER (user/org) and REPO with your values.

1. List who has admin access on a repository

A. List collaborators and their permissions (classic collaborators)

gh api \
repos/OWNER/REPO/collaborators \
--paginate \
--jq '.[] | select(.permissions.admin == true) | {login, permissions}'

This shows logins that have admin: true via direct collaborator access.

B. List teams with admin access (for org repos)

gh api \
repos/OWNER/REPO/teams \
--paginate \
--jq '.[] | select(.permission == "admin") | {name, slug, permission}'

Members of these teams may effectively be “repo admins” depending on team permission.

C. (Optional) List org members with admin role (org‑wide)

gh api \
orgs/OWNER/members \
--paginate \
--jq '.[] | select(.role == "admin") | .login'

Use this to identify who can manage repos/org settings globally.


2. Decide the minimal required admins

Offline step:

  • For each admin user or team, decide:
    • Keep as admin,
    • Downgrade to maintain or push, or
    • Remove.

Document the desired target state: user/team → new permission.


3. Downgrade or remove individual repo admins

A. Change a collaborator’s permission (e.g., admin → maintain)

gh api \
--method PUT \
-H "Accept: application/vnd.github+json" \
repos/OWNER/REPO/collaborators/USERNAME \
-f permission=maintain

Valid values: pull, triage, push, maintain, admin.

B. Remove a collaborator entirely

gh api \
--method DELETE \
repos/OWNER/REPO/collaborators/USERNAME

4. Downgrade or remove admin teams

A. Change team permission on the repo (admin → maintain, for example)

gh api \
--method PUT \
-H "Accept: application/vnd.github+json" \
orgs/OWNER/teams/TEAM_SLUG/repos/OWNER/REPO \
-f permission=maintain

B. Remove team access to the repo

gh api \
--method DELETE \
orgs/OWNER/teams/TEAM_SLUG/repos/OWNER/REPO

5. Reduce organization‑wide admins (if needed)

Use this carefully—org admins lose global powers.

A. List current org admins

gh api \
orgs/OWNER/members \
--paginate \
--jq '.[] | select(.role == "admin") | .login'

B. Change a user’s org role from admin → member

gh api \
--method PUT \
orgs/OWNER/memberships/USERNAME \
-f role=member

C. Remove a user from the org (if appropriate)

gh api \
--method DELETE \
orgs/OWNER/members/USERNAME

6. Re‑verify effective repo admins

Re‑run checks:

# Direct collaborators with admin
gh api repos/OWNER/REPO/collaborators --paginate \
--jq '.[] | select(.permissions.admin == true) | {login, permissions}'

# Teams with admin
gh api repos/OWNER/REPO/teams --paginate \
--jq '.[] | select(.permission == "admin") | {name, slug, permission}'

Confirm the list is limited to the strictly necessary admins.

Using Python

Below is a practical, step‑by‑step way to reduce excessive repo admins in GitHub using Python (via the GitHub REST API and/or PyGithub).


1. Preparation

  1. Decide the policy you want to enforce, for example:
    • Only a specific team(s) can be admin.
    • Or max N admin users per repo.
  2. Create a GitHub Personal Access Token (PAT) with at least:
    • repo
    • admin:org (if managing org repos, teams, etc.)

Export it as an environment variable (recommended):

export GITHUB_TOKEN="ghp_XXXXXXXXXXXXXXXXXXXX"
export GITHUB_ORG="your-org-name"

2. Install Python dependencies

pip install PyGithub requests

3. Enumerate Admins and Decide Remediations

3.1. Using PyGithub to list repos and their admins

from github import Github
import os

token = os.environ["GITHUB_TOKEN"]
org_name = os.environ["GITHUB_ORG"]

g = Github(token)
org = g.get_organization(org_name)

for repo in org.get_repos():
print(f"\nRepo: {repo.full_name}")

# Direct collaborators
print(" Direct admins:")
for collab in repo.get_collaborators(permission="admin"):
print(f" USER: {collab.login}")

# Teams with admin permission
print(" Teams with admin:")
for team in repo.get_teams():
if team.permission == "admin": # 'admin', 'push', 'pull', 'maintain', etc.
print(f" TEAM: {team.slug}")

Use this output to decide which users/teams should no longer have admin rights.


4. Implement Least-Privilege Changes

4.1. Define allowed admins (policy)

Example: only one “core-admins” team plus maybe some specific user(s) per repo.

ALLOWED_ADMIN_TEAMS = {"core-admins"} # team slugs
ALLOWED_ADMIN_USERS = {"repo-owner-1"} # usernames allowed to stay admin
DEFAULT_USER_PERMISSION = "push" # downgrade excess admins to 'push'

4.2. Downgrade or remove excessive admins

from github import Github
import os

token = os.environ["GITHUB_TOKEN"]
org_name = os.environ["GITHUB_ORG"]

ALLOWED_ADMIN_TEAMS = {"core-admins"}
ALLOWED_ADMIN_USERS = {"repo-owner-1"}
DEFAULT_USER_PERMISSION = "push" # or 'pull'

g = Github(token)
org = g.get_organization(org_name)

for repo in org.get_repos():
print(f"\nProcessing repo: {repo.full_name}")

# --- Handle teams with admin permissions ---
for team in repo.get_teams():
if team.permission == "admin":
if team.slug not in ALLOWED_ADMIN_TEAMS:
print(f" Downgrading/removing TEAM admin: {team.slug}")
# Option 1: downgrade to 'push'
team.update_team_repository(repo, permission="push")
# Option 2 (alternative): remove team access completely
# team.remove_from_repos(repo)

# --- Handle direct users with admin permissions ---
for collab in repo.get_collaborators(permission="admin"):
if collab.login not in ALLOWED_ADMIN_USERS:
print(f" Downgrading USER admin: {collab.login}")
# Option 1: downgrade to 'push'
repo.add_to_collaborators(collab.login, permission=DEFAULT_USER_PERMISSION)
# Option 2 (alternative): remove access completely
# repo.remove_from_collaborators(collab.login)

Run in dry-run mode first by commenting out the change lines and just printing what would happen.


5. Safer: Dry-run / Audit Mode

Add a DRY_RUN = True flag:

DRY_RUN = True # Set False to apply changes

for repo in org.get_repos():
print(f"\nProcessing repo: {repo.full_name}")

for team in repo.get_teams():
if team.permission == "admin" and team.slug not in ALLOWED_ADMIN_TEAMS:
print(f" Would downgrade TEAM {team.slug} from admin to push")
if not DRY_RUN:
team.update_team_repository(repo, permission="push")

for collab in repo.get_collaborators(permission="admin"):
if collab.login not in ALLOWED_ADMIN_USERS:
print(f" Would downgrade USER {collab.login} from admin to {DEFAULT_USER_PERMISSION}")
if not DRY_RUN:
repo.add_to_collaborators(collab.login, permission=DEFAULT_USER_PERMISSION)

6. (Optional) Using Raw REST API via requests

If you prefer not to use PyGithub:

  • List collaborators with permission: GET /repos/{owner}/{repo}/collaborators?permission=admin
  • Change a collaborator’s permission: PUT /repos/{owner}/{repo}/collaborators/{username} with body: {"permission": "push"}

Minimal example:

import os
import requests

token = os.environ["GITHUB_TOKEN"]
org = os.environ["GITHUB_ORG"]
headers = {"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"}

def list_repos():
url = f"https://api.github.com/orgs/{org}/repos"
r = requests.get(url, headers=headers)
r.raise_for_status()
return r.json()

def list_admin_collaborators(repo):
url = f"https://api.github.com/repos/{org}/{repo}/collaborators"
r = requests.get(url, headers=headers, params={"permission": "admin"})
r.raise_for_status()
return [c["login"] for c in r.json()]

def downgrade_user(repo, user, perm="push"):
url = f"https://api.github.com/repos/{org}/{repo}/collaborators/{user}"
r = requests.put(url, headers=headers, json={"permission": perm})
r.raise_for_status()

for r in list_repos():
repo_name = r["name"]
admins = list_admin_collaborators(repo_name)
for user in admins:
if user not in {"repo-owner-1"}:
print(f"Downgrading {user} on {repo_name}")
downgrade_user(repo_name, user, "push")

7. Governance

  • Run the audit/remediation script periodically (e.g., as a GitHub Action or CI job).
  • Log all changes.
  • Optionally notify affected users/teams before/after changes.

If you describe your exact policy (who should remain admin and in what cases), I can adjust the Python logic and permissions transitions precisely to match it.

Using Terraform
# GitHub repository (maps to github-applicationintegration-scm-repository)
resource "github_repository" "repo" {
name = "REPO_NAME" # replace with the repository name
description = "REPO_DESCRIPTION"
visibility = "private"
}

# SINGLE admin (keep admin here)
resource "github_repository_collaborator" "admin" {
repository = github_repository.repo.name
username = "PRIMARY_ADMIN_USERNAME" # replace with the single intended admin
permission = "admin"
}

# All other users: drop down from admin/push to lower permission (e.g., pull)
resource "github_repository_collaborator" "read_only_collaborators" {
for_each = toset([
"USER_1_USERNAME", # replace with each non-admin user that previously had admin/push
"USER_2_USERNAME",
# ...
])
repository = github_repository.repo.name
username = each.value
permission = "pull" # or "triage" / "maintain" as policy allows, but not "admin" or "push"
}

Changing a collaborator’s permission does not replace the repository; it updates access in place, but it is immediately effective and may block previous admin/push actions.

For verification, terraform plan should show:

  • No change to github_repository.repo
  • github_repository_collaborator.admin with permission = "admin"
  • All other github_repository_collaborator.* changing from admin/push to the lower permission you chose (e.g., pull).

Additional Reading: