Skip to main content
INS // Insights

GitHub Deploy Keys and Token Elimination

Updated August 2026 · 5 min read

GitHub deploy keys and token elimination is CI/CD credential work most SOC 2 programs never sample until a leaked PAT shows up in a gist. Deploy keys are SSH keys living on a repo forever. Classic PATs are user-owned, often repo scoped, copied into Actions secrets and Lambda env. Both are standing credentials. Federation and short-lived tokens are the replacement.

This is the GitHub-shaped sibling of hardcoded credentials in CI/CD and GitHub Actions OIDC to AWS. AWS keys in Actions are half the story. GitHub's own keys are the other half.

Inventory: Org, Repo, User, Runner

We scan:

  • Org and repo deploy keys (GET /repos/{owner}/{repo}/keys)
  • Org/repo/environment secrets (names only; we do not exfiltrate values)
  • Workflow files for secrets. usage and curl to metadata
  • Classic PATs in secret names like GH_TOKEN, MACHINE_USER_PAT
  • Self-hosted runner registration tokens (should be ephemeral)
  • GitHub App vs OAuth app credentials

GitHub documents deploy keys as repo-scoped SSH. Read-only deploy keys for public clone are a different risk than write deploy keys on a private monorepo. Write deploy keys are production admin.

Replacement Map

Old New
Write deploy key for Actions deploy GitHub App with least permissions, or GITHUB_TOKEN with explicit permissions: in the workflow
Classic PAT in GH_TOKEN for cross-repo GitHub App installation token, or fine-grained PAT time-boxed and org-owned
PAT for AWS deploy Delete; OIDC to AWS
Deploy key on a jump host Deploy from CI with OIDC; no SSH key on a box
Machine user GitHub App. Machine users accumulate PATs and 2FA exceptions
# workflow: default GITHUB_TOKEN is enough for many deploys
permissions:
  contents: read
  id-token: write   # for cloud OIDC
# do not set secrets.GH_TOKEN if GITHUB_TOKEN + App can do the job

If a job needs to open a PR in another repo, that is an App permission, not a human PAT.

Elimination Sequence

  1. Dual-run: App token path in parallel with the PAT, metric on PAT usage (API X-GitHub-Api-Version plus audit log).
  2. Remove PAT from Actions secrets (blank) — do not leave a dummy that someone "fixes."
  3. Delete deploy keys that have not been used (GitHub last-used is imperfect; pair with server auth logs).
  4. Org policy: deny classic PATs if the org can; require SSO on remaining tokens.
  5. Audit log alert on git.clone / deploy key add.

GitHub audit log is the CC6/CC7 evidence for who added a deploy key. If you do not retain audit log, you cannot investigate. Retention is a policy choice; "default" is not a control.

Talk to us about your credential elimination pilot → rutagon.com/contact · 907-841-8407 · contact@rutagon.com.

Org Policy Levers

GitHub Enterprise and some org settings can block classic PATs, require SSO, and restrict deploy keys. Turn those on after dual-run, not before, or you lock out the one integration you forgot. We inventory Apps and OAuth apps at the org: leftover OAuth apps with repo scope are equivalent to a PAT with extra steps.

Self-hosted runners should not hold deploy keys on disk. Ephemeral runners + OIDC is the target. A persistent runner with a write deploy key is a standing production admin.

Audit Log Retention

If GitHub audit log retention is short, export to S3 on a schedule (GitHub provides APIs / streaming on higher SKUs). CC6 evidence six months later cannot depend on a UI that rolled off. Same idea as CloudTrail: the evidence store is yours.

CODEOWNERS plus environment protection (required reviewers) on production stops a stolen GITHUB_TOKEN with write from deploying if the workflow is designed correctly. Fork PRs never get id-token: write to prod roles.

GitHub deploy keys and token elimination without locking the org

GitHub deploy keys and token elimination has a dual-run window for a reason. Turn on “no classic PATs” and “no deploy keys” only after the last production workflow has moved to GitHub Apps or OIDC. We have seen a forgotten Terraform Cloud / partner integration take down deploys on a Friday because the org policy flipped first.

GitHub documents deploy keys as repo-scoped SSH. Write deploy keys on a private monorepo are production admin. Read-only keys on a public clone are a different risk class. Inventory both. Self-hosted runners with a write key on disk are standing admin even if Actions “uses OIDC” in one workflow.

OIDC to AWS is the replacement we implement most: id-token: write only on protected production environments, audience and sub conditions tight, no repo:* on the trust policy. Fork PRs never get id-token: write to prod roles. Fine-grained PATs, if they must exist, expire and are owned. Classic PATs in org secrets are the finding.

Export GitHub audit log to S3 if retention in the UI is shorter than your observation window. CC6 evidence cannot depend on a SaaS UI that rolled off.

Frequently Asked Questions

Are read-only deploy keys still a problem?

Lower blast radius than write keys, still standing secrets on a box. Prefer HTTPS + short-lived credentials from CI. Read-only keys on a forgotten host are how source walks out.

Is GITHUB_TOKEN enough for everything?

No. Cross-repo and org-admin jobs need an App. That is still better than a human PAT. Scope the App.

What about Dependabot and Actions from forks?

Fork PRs should not get write GITHUB_TOKEN or cloud OIDC. That is a workflow design issue, not a deploy-key issue. Lock permissions and environment gates.

Can we keep one org-wide PAT for emergencies?

If you do, vault it, SSO-enforce, expire it, and alarm on use. Treat it as break-glass, not as CI.

Does this replace secret scanning?

No. Scanning finds what you missed. Elimination removes the class of secret. Run both — see secrets sprawl sprints.