Safeguard
Incident Analysis•

One Stolen Login: Auditing Every Credential Path Into Your GitHub Org

Meta says one employee's stolen credentials exposed code and personal data of about 1,500 people. Here's how to list and revoke every token, key and app that reaches your GitHub org.

Safeguard Security Team
7 min read

On 13 October 2026, Meta began notifying about 1,500 people, mostly US employees, that an outside group had used "an employee's stolen credentials" to access some of its GitHub repositories. Meta told SecurityWeek that about 99% of the data taken was business data, mainly source code, and the rest was personal data placed in repositories by mistake. It detected the access a few weeks before notifications went out.

You don't need Meta's scale to have Meta's problem. The useful question is this: if one engineer's credentials leaked today, could you list every way they reach your code and cut each one off within the hour? This guide walks through that audit with gh and the REST API.

What's affected

Any GitHub organization where a member's credentials reach private repositories. A password reset doesn't end most of that access. GitHub's credential authorization API lists the types that matter on SSO organizations: personal access token, SSH key, OAuth app token and GitHub app token. On top of those sit deploy keys, which belong to a repository rather than a person, and GitHub App installations.

Two documented details catch teams out. First, GitHub says that unless you restrict them, "any personal access token (classic) can access organization resources until the token expires." Second, when an owner revokes a fine-grained token, "SSH keys created by the token will continue to work."

How to check if you're exposed

Set two variables for the person you're investigating:

ORG=your-org
LOGIN=suspect-login

1. Pull Git events first. They expire. GitHub's audit log reference says that on GitHub Enterprise Cloud, Git events are available through the REST API "only with 7-day retention." The git.clone event doesn't appear in the web interface at all.

gh api --method GET "orgs/$ORG/audit-log" --paginate \
  -f phrase="actor:$LOGIN action:git.clone" -f include=git -f per_page=100 \
  --jq '.[] | [.created_at, .repo, .transport_protocol_name] | @tsv'

Run it again with action:repo.download_zip, which records source archive downloads. If you hold a leaked token value, hash it with echo -n TOKEN | openssl dgst -sha256 -binary | base64 and search with hashed_token:"VALUE", URI-escaping the hash. GitHub notes that token searches don't return Git events, so export those separately.

2. List the user's SSO credential authorizations.

gh api --method GET "orgs/$ORG/credential-authorizations" -f login="$LOGIN" --paginate \
  --jq '.[] | [.credential_type, .credential_id, .token_last_eight, .credential_accessed_at, .authorized_credential_expires_at] | @tsv'

A token with no expiry and a recent credential_accessed_at you can't explain goes to the top of the list.

3. Review fine-grained personal access tokens. In the UI: organization Settings, Personal access tokens, Active tokens, then filter by Owner. The REST endpoint GET /orgs/ORG/personal-access-tokens only accepts GitHub App tokens. With an app installation token in APP_TOKEN:

curl -s -H "Authorization: Bearer $APP_TOKEN" \
  "https://api.github.com/orgs/$ORG/personal-access-tokens?last_used_after=2026-09-01T00:00:00Z" |
  jq -r '.[] | [.owner.login, .token_name, .repository_selection, .token_expires_at, .token_last_used_at] | @tsv'

4. Sweep deploy keys. The REST response includes who added each key and when it was last used, which gh repo deploy-key list doesn't show.

gh repo list "$ORG" --limit 1000 --json nameWithOwner --jq '.[].nameWithOwner' |
while read -r REPO; do
  gh api "repos/$REPO/keys" \
    --jq '.[] | [.id, .title, .read_only, .added_by, .last_used] | @tsv' |
    awk -v r="$REPO" '{print r "\t" $0}'
done

5. List GitHub App installations and outside collaborators.

gh api "orgs/$ORG/installations" --paginate \
  --jq '.installations[] | [.app_slug, .repository_selection, .created_at] | @tsv'
gh api "orgs/$ORG/outside_collaborators?filter=2fa_insecure" --paginate --jq '.[].login'
gh api "orgs/$ORG/members?filter=2fa_disabled" --paginate --jq '.[].login'

An app with repository_selection set to all reaches every repository, including ones created after you approved it.

What to do now

  1. Cut the user's SSO authorizations in one call. This revokes their PATs, SSH keys, OAuth app tokens and app user tokens for the org. It runs as a background job, and they must re-authorize anything legitimate.

    gh api -X POST "orgs/$ORG/credential-authorizations/$LOGIN/revoke"
    

    To remove a single credential instead, use DELETE /orgs/ORG/credential-authorizations/CREDENTIAL_ID.

  2. Revoke fine-grained tokens from the Active tokens page, or with POST /orgs/ORG/personal-access-tokens and a body of "action": "revoke" plus pat_ids, from a GitHub App.

  3. Delete deploy keys you can't account for with gh api -X DELETE repos/OWNER/REPO/keys/KEY_ID. Prefer read_only keys for anything that stays.

  4. Restrict classic tokens. Under Settings, Personal access tokens, Settings, open the Tokens (classic) tab and set "Restrict personal access tokens (classic) from accessing your organizations". On the same page, set a maximum lifetime and keep administrator approval on for fine-grained tokens. GitHub notes the lifetime policy blocks non-compliant tokens but doesn't revoke them.

  5. Confirm OAuth app access restrictions are on. GitHub warns that without them, "any OAuth app authorized by an organization member can also access the organization's private resources."

  6. Keep Git events longer than seven days. Export or stream the audit log to your own storage so a breach found weeks later still has clone records.

  7. Rotate secrets found in the affected repositories, as Meta did, and scan history for personal data that shouldn't be there.

Why it matters

Meta's spokesperson put it plainly: "This was not a vulnerability in any Meta product." The entry point was a valid login, and valid logins come with tokens, keys and app grants that outlive the password. An audit that stops at "reset the password and enforce 2FA" leaves most of those in place.

The retention window is the quiet risk. Meta found the access weeks after it began. With a seven-day window on Git events, an organization that doesn't export them can lose the record of what was cloned before anyone starts looking.

Try it yourself

Run step 2 against your own login. Count the credentials you have authorized for the organization, note how many have no expiry, and check when each was last used. Then decide which ones you'd revoke if your laptop disappeared tonight.

Click through it at your own pace, or open it full screen.

Quick check

Does revoking a fine-grained token also cut off SSH keys it created?

No. GitHub's documentation says SSH keys created by a revoked fine-grained token continue to work, so review SSH key authorizations separately.

Can you see git.clone events in the audit log web interface?

No. GitHub lists git.clone as available only through the REST API, audit log streaming, or JSON and CSV exports, with 7-day REST retention on Enterprise Cloud.

Which endpoint revokes all of one user's SSO credential authorizations at once?

POST /orgs/ORG/credential-authorizations/USERNAME/revoke, which queues the revocation as a background job.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.