Granting temporary elevated access with expiry Jump to heading
Someone always needs more access than usual for a while: an engineer running a repository migration needs admin rights, an incident responder needs to push a fix past a broken required check, a contractor needs write access for a two-week engagement. The access is granted in a hurry and removed β in theory β when the work is done. In practice it accumulates. A year later the audit finds eleven people with admin on the payments repository and nobody remembers why. The fix is to make every elevation carry an expiry that is enforced by automation, not memory, and to record what was done with it. This page builds that, within repository access control and policy.
When to use this approach Jump to heading
- People are occasionally granted admin, maintain or bypass rights for a specific task.
- An access review has found elevated permissions nobody could justify.
- Incidents sometimes require bypassing branch protection, and you want that to be possible but bounded.
- You already audit who can push to protected branches, as in auditing who can push to protected branches, and want fewer surprises in the results.
Step 1 β Grant through a team, never to an individual directly Jump to heading
Direct collaborator grants are hard to find and harder to expire. Create one team per kind of elevation per repository, keep it empty by default, and add people to it when they need the access.
# A standing, normally-empty team that holds admin on one repository
gh api -X POST "orgs/$ORG/teams" -f name="payments-admin-temporary" -f privacy=closed
gh api -X PUT "orgs/$ORG/teams/payments-admin-temporary/repos/$ORG/payments" -f permission=admin
# Verification: the team exists, holds admin, and has no members
gh api "orgs/$ORG/teams/payments-admin-temporary/members" --jq 'length' # 0 Step 2 β Record each grant with a reason and an expiry Jump to heading
Keep a small file in a policy repository listing current elevations. Granting access is a pull request to that file; the merge adds the person to the team. The file is the source of truth, and the team is made to match it.
# access/elevations.yml
- user: priya
team: payments-admin-temporary
reason: "Repository migration to new org, ticket OPS-2291"
approved_by: maria
expires: 2026-10-16
- user: contractor-jlee
team: web-write-temporary
reason: "Accessibility engagement, SOW-118"
approved_by: tom
expires: 2026-10-31 Requiring an approver other than the requester, enforced through code-owner review on this file, is what turns it from a list into a control.
Step 3 β Reconcile teams with the file, and expire automatically Jump to heading
A scheduled job compares each teamβs members with the file. Members not in the file, or whose entry has expired, are removed. Entries in the file whose user is not yet a member are added. Run it every hour so expiry is close to exact.
#!/bin/sh
# reconcile-elevations.sh
set -eu
today=$(date +%F)
yq -r '.[] | [.team, .user, .expires] | @tsv' access/elevations.yml > /tmp/wanted
for team in $(cut -f1 /tmp/wanted | sort -u) $(gh api "orgs/$ORG/teams" --paginate --jq '.[].slug' | grep -- '-temporary$'); do
current=$(gh api "orgs/$ORG/teams/$team/members" --paginate --jq '.[].login')
for u in $current; do
exp=$(awk -F'\t' -v t="$team" -v u="$u" '$1==t && $2==u {print $3}' /tmp/wanted)
if [ -z "$exp" ] || [ "$exp" \< "$today" ]; then
gh api -X DELETE "orgs/$ORG/teams/$team/memberships/$u" && echo "removed $u from $team"
fi
done
awk -F'\t' -v t="$team" -v d="$today" '$1==t && $3>=d {print $2}' /tmp/wanted | while read -r u; do
echo "$current" | grep -qx "$u" || gh api -X PUT "orgs/$ORG/teams/$team/memberships/$u" -f role=member
done
done # Verification: an entry dated yesterday is removed on the next run
gh api "orgs/$ORG/teams/payments-admin-temporary/members" --jq '.[].login' Step 4 β Bound break-glass bypasses the same way Jump to heading
Incident responders sometimes need to merge past a failing required check. Rather than letting admins bypass rules permanently, create a ruleset bypass list that contains only the temporary team, and give the team members only during an incident.
# Ruleset bypass actors: only the incident team, only via pull request
gh api "repos/$ORG/payments/rulesets/$RULESET_ID" --jq '.bypass_actors'
# [{"actor_type":"Team","actor_id":<incident-team-id>,"bypass_mode":"pull_request"}] bypass_mode: pull_request means the bypass still requires opening a pull request, which leaves a reviewable record of exactly what was merged under emergency rules.
Step 5 β Review what the elevated access was used for Jump to heading
Expiry limits how long access lasts; the audit log shows what it was used for. After each elevation expires, pull the relevant audit events and attach them to the original request.
gh api "orgs/$ORG/audit-log?phrase=actor:priya+repo:$ORG/payments+created:>=2026-10-02" \
--jq '.[] | [.created_at, .action, (.ref // .name // "")] | @tsv' Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
What if someone needs access at 3am and nobody can approve? Jump to heading
Pre-approve an incident path: a small on-call group may merge elevation requests for the incident team with a short expiry β hours, not days β and the request is reviewed retrospectively. The automation still removes access on schedule.
Can people be added to the team directly through the web interface? Jump to heading
They can, but the next reconcile run removes anyone not in the file. That makes the file the only lasting way to grant access, which is the intent. Alert on removals so a mistaken manual grant is noticed.
Does this replace regular access reviews? Jump to heading
No, but it makes them shorter. Standing permissions still need periodic review; temporary ones no longer accumulate, so the review covers far fewer surprises.
Related Jump to heading
- Repository Access Control & Policy β the parent topic.
- Managing Repository Permissions as Code β the same reconcile pattern for standing permissions.
- Offboarding a Developer from Every Repository β the leaver case, where all access ends at once.
- Designing Branch Protection Rulesets for an Org β where bypass lists are defined.