GitHub deploy keys and user SSH keys never expire, are often added once by someone who has since moved on, and keep working until a person deletes them. This guide shows how to list every deploy key in an organisation with its last use date, how to see the SSH keys members have authorised for a SAML-protected organisation, how to separate keys that are idle from keys that are quietly running deployments, and how to remove them without breaking a release. It is for GitHub organisation owners, platform engineers and identity teams running an NHI inventory or cleaning up after an incident. For the wider GitHub picture see the GitHub and GitHub Actions Security Guide, and for key lifecycle in general the SSH Key Management Guide.
Key takeaways
- Deploy keys belong to the repository, not a person: GitHub states that if the user who created a deploy key is removed from the repository, the key stays active, so offboarding a person does not remove the keys they added.
- The REST API gives you last use: the deploy key object returned by
GET /repos/{owner}/{repo}/keysincludeslast_used,added_by,read_onlyandenabled, which is enough to triage most keys without touching servers. - Write access is close to admin: GitHub documents that a deploy key with write access can perform the same actions as an organisation member with admin access on that repository, so write keys come first.
- User SSH keys are visible to owners only through SSO: in GitHub Enterprise Cloud with SAML SSO, the credential authorisations API lists SSH keys members have authorised, with a fingerprint, authorisation date and
credential_accessed_at. - Deletion is final, so stage it: a deleted deploy key cannot be restored, only replaced with a new key pair, so confirm the consumer before deleting and keep the replacement ready.
When you need this
- NHI inventory or access review: deploy keys are machine credentials with no owner field beyond
added_by, so they are easy to miss in a people-centred review. See the NHI Inventory Reconciliation guide. - Leaver or team change: the person who added a key leaves, but the key, and the server holding its private half, remain.
- Server or build host compromise: GitHub's own guidance notes that deploy keys are usually not protected by a passphrase, so the private key is easily reached if the server is compromised. Every key whose private half sat on that host is in scope.
- Leaked key material: keys that do not expire stay valid for as long as nobody deletes them. In the leaked GitHub App private keys research, 474 leaked App keys still authenticated, some years after they leaked. Deploy keys have the same property.
- Policy change: before an enterprise owner restricts deploy keys, because GitHub warns that disabling the policy disables existing deploy keys in every repository in the enterprise.
Before you start
- Roles: repository admin to list and delete deploy keys; organisation owner to read SAML credential authorisations and the organisation audit log; enterprise owner for the enterprise deploy key policy.
- Plan: the audit log REST API and the credential authorisations API require GitHub Enterprise Cloud. On GitHub Enterprise Server, the same deploy key endpoints exist on your instance hostname.
- A read-only token: use a token that can read repository administration data for the organisation, ideally a short-lived one created for the audit, and record it so you can revoke it afterwards.
- A findings register: one row per key with repository, key ID, title, fingerprint, created, last used, read only or write, added by, consumer host, owner team and decision.
- A change window for deletions: discovery is read-only; removal is not, and needs agreement from the team that owns each deployment.
Step-by-step
1. Know which key types you are looking for
| Key type | Attached to | Where to list it | Last use data | Expiry |
|---|---|---|---|---|
| Deploy key | One repository | Repository Settings > Security > Deploy keys; GET /repos/{owner}/{repo}/keys | last_used field in the API | None |
| User SSH authentication key | A personal account, access follows the user's repository permissions | The user's own Settings > SSH and GPG keys; GET /user/keys for the authenticated user | last_used for the key owner only | None |
| SSH key authorised for SAML SSO | A personal account, authorised for one organisation | GET /orgs/{org}/credential-authorizations (Enterprise Cloud) | credential_accessed_at | None on the key itself |
| SSH certificate from an organisation CA | A user, signed by a CA you add to the organisation or enterprise | Your CA's issuance records; CA list in organisation settings | Your CA logs and Git events | Validity period set at signing |
| SSH signing key | A personal account, used to sign commits | GET /user/ssh_signing_keys | Not provided | None |
Signing keys do not authenticate Git operations, so they are out of scope for an access cleanup, but they often get confused with authentication keys in user reports.
2. Enumerate deploy keys across every repository (read-only)
- List every repository in the organisation, including archived and private ones.
- For each repository, call
GET /repos/{owner}/{repo}/keysand page through the results. For a single repository,gh repo deploy-key list --repo OWNER/REPOprints the same keys, and its JSON output exposesid,key,title,createdAtandreadOnly. - Record for each key:
id,title,created_at,read_only,added_by,last_used,enabledandverified. The API schema marksadded_byandlast_usedas nullable, so a null value is a finding to investigate, not proof the key was never used. - Compute the SHA256 fingerprint of each public key so you can match it to servers later (for example by saving the
keyvalue to a file and runningssh-keygen -lfon it). - Flag any fingerprint that appears on more than one repository. GitHub does not let the same deploy key be added to two repositories, so a repeated fingerprint usually means a key was deleted and re-added, or the inventory has mixed in user keys.
3. Classify each deploy key
| Signal | Meaning | Default decision |
|---|---|---|
last_used older than your idle period (for example 90 days) | No deployment has used it recently | Disable path: confirm owner, then delete |
last_used null and created_at old | Never used, or used before the field was populated | Treat as idle, confirm with the repository team |
read_only false | Can push, and acts like a repository admin | Priority review; justify or replace with read-only or a GitHub App |
added_by is a departed user | No living owner | Find the consumer host; assign an owner or delete |
enabled false | Disabled, for example by the enterprise policy | Delete once nothing depends on re-enabling it |
| Title gives no host or purpose | Nobody can tell what breaks if it goes | Rename on replacement with host, purpose and team |
| Repository archived | Deployments should have stopped | Delete any remaining keys after a short notice period |
4. Review SSH keys authorised for the organisation (Enterprise Cloud with SAML SSO)
- Call
GET /orgs/{org}/credential-authorizations, or open each member's page under People, where GitHub shows the member's linked identity, active sessions and authorised credentials. - Filter to SSH key credentials. Each entry carries the
login,fingerprint,authorized_credential_title,credential_authorized_atandcredential_accessed_at. - Flag keys with an old or null
credential_accessed_at, keys belonging to members who have left the identity provider, and members holding many keys. - Remember the limit: GitHub explains that to reach organisation resources through Git, a member must use an SSH key authorised for that organisation, so this list covers access to your organisation, but not keys a user holds for their personal repositories.
Without SAML SSO, owners cannot list members' SSH keys with last use. Ask each user to run the self-review GitHub documents (Settings > SSH and GPG keys, compare with ssh-add -l -E sha256 locally, delete unknown keys) and record the result, or require SSH certificates (step 7).
5. Correlate with Git activity before deleting
- Git events: in Enterprise Cloud,
git.clone,git.fetchandgit.pushevents are available only through the audit log REST API, streaming or exports, not in the web interface, and GitHub retains them for seven days. If you need a longer window, stream the audit log to your SIEM before the review. - Key lifecycle events: the
public_keycategory recordspublic_key.create,public_key.deleteandpublic_key.updatefor both user SSH keys and deploy keys, which tells you who added a key and when. - Consumer hosts: search configuration management, CI runner images and
~/.ssh/confighost aliases for the fingerprints. GitHub's own guidance for multiple repositories on one server uses one key pair and one host alias per repository, so a host alias such asgithub.com-repo-0is a strong clue.
6. Remove idle keys (destructive, change-controlled)
- Notify the repository team with the key title, fingerprint and last use, and a deletion date.
- For keys still in use but over-privileged, create the replacement first: a new read-only deploy key, or a GitHub App installation, which GitHub recommends for finer control. Switch the consumer and confirm a successful clone or deploy.
- Destructive: delete the old key with
DELETE /repos/{owner}/{repo}/keys/{key_id}or Delete on the Deploy keys page. - Delete the private key from the consumer host, so a host backup cannot bring it back into use if the public half is re-added.
- For SSO-authorised user keys, an owner can remove the authorisation with
DELETE /orgs/{org}/credential-authorizations/{credential_id}. That ends the key's access to the organisation; the key stays on the user's account.
7. Triage summary
- Delete now: idle beyond your period, added by a departed user with no consumer found, on an archived repository, or on a host known to be compromised.
- Replace: write keys that only need to pull, and keys whose private half is shared across hosts.
- Move: automation that touches many repositories to a GitHub App or short-lived federation. See the GitHub Actions OIDC Federation Hardening Guide.
- Accept with exception: named owner, documented consumer host, review date and audit log monitoring.
Verify it worked
| Metric | Source | Target |
|---|---|---|
| Deploy keys idle beyond policy | last_used from the deploy keys API | Zero after each review |
| Write-enabled deploy keys | read_only false count | Falling, each with a documented reason |
| Keys added by departed users | added_by joined with directory status | Zero without an assigned owner |
| SSO-authorised SSH keys with no recent access | credential_accessed_at | Zero after each review |
| Key additions outside change control | public_key.create audit events | Every event matched to a ticket |
Re-run the enumeration after the change window and diff it against the register. Watch build and deployment pipelines for authentication failures for a week; a failure that names a deleted key means a consumer was missed.
Rollback and common errors
- Deletion cannot be undone: rollback means generating a new key pair, adding the public key and updating the consumer. Have the replacement ready before deleting anything in active use.
- Trusting a null last use:
last_usedis nullable. Check Git events and consumer hosts before treating a null as never used on a key that guards production deploys. - Flipping the enterprise policy first: GitHub warns that changing the deploy key policy to disabled disables existing deploy keys in all repositories and breaks scripts, apps and workflows that use them. Inventory and migrate first, then restrict.
- Reading Git events too late: Git events are kept for seven days, so a review based on the audit log alone sees only the last week of clones and pushes.
- Removing a person and assuming their keys are gone: removing the user from the repository does not remove deploy keys they added.
- Deleting the public key but not the private one: the private key on the host remains a credential if anyone re-adds the public half, and it is still material an attacker can take from a backup.
Hardening afterwards
- Prefer GitHub Apps or federation: GitHub recommends a GitHub App over deploy keys for finer control of repository access and permissions, and App installation tokens are short-lived. Inventory those Apps too: see How to audit GitHub Apps and OAuth apps in an organisation.
- Use the enterprise deploy key policy deliberately: under Enterprise Policies > Repositories > Deploy keys, allow, restrict or delegate creation to organisation owners once the migration is done.
- Require SSH certificates for people: with an SSH certificate authority added to the organisation or enterprise, GitHub validates the certificate's validity period, so access ends when the certificate expires. GitHub gives the example of a daily certificate. See the SSH Protocol and Certificate Authority Guide.
- Name keys for their consumer: a title with host, purpose and team makes the next review a matter of reading, not investigating.
- Stream the audit log: keep
public_key.*andgit.*events beyond seven days and alert on new write-enabled deploy keys. See the GitHub Audit Log Detection Guide. - Schedule the export: run the enumeration monthly and feed results into offboarding. See the NHI Offboarding and Decommissioning Guide.
| Framework | Reference | Relevance |
|---|---|---|
| NIST CSF 2.0 | PR.AA-01, PR.AA-05, DE.CM-03 | Credentials for services managed; access reviewed for least privilege; usage monitored |
| NIST SP 800-53 Rev 5 | IA-5, AC-2, AC-2(3), AC-6, AU-6 | Authenticator management, account review, disabling unused access, least privilege, audit review |
| CIS Controls v8.1 | Controls 5 and 6 | Account management and access control management, including service accounts |
| MITRE ATT&CK v20 | T1552.004 Private Keys; T1213.003 Code Repositories | Theft of private keys from hosts; collection from repositories once a key is taken |
Practitioner checklist
- Enumerate deploy keys in every repository, including archived ones, with
last_used,added_byandread_only. - Fingerprint every key and match it to a consumer host and owning team.
- Review every write-enabled deploy key first and justify or replace it.
- On Enterprise Cloud with SAML SSO, export SSH key credential authorisations and review
credential_accessed_at. - Stream the audit log so Git events outlive the seven-day retention.
- Create replacements before deleting keys in use; delete private keys from hosts too.
- Restrict deploy key creation with the enterprise policy only after migration.
- Move multi-repository automation to GitHub Apps or federation and require SSH certificates for people where practical.
- Repeat the review monthly and on every leaver and host compromise.
Standards and references
- GitHub Docs: Managing deploy keys
- GitHub Docs: REST API endpoints for deploy keys
- GitHub Docs: Reviewing your SSH keys
- GitHub Docs: Viewing and managing a member's SAML access to your organization
- GitHub Docs: Audit log events for your organization
- GitHub Docs: Enforcing repository management policies in your enterprise
- GitHub Docs: About SSH certificate authorities
- NIST: SP 800-53 Rev. 5 Security and Privacy Controls
Related NHI Mgmt Group resources: GitHub Personal Access Tokens Security Guide, How to revoke a leaked GitHub personal access token and check what it touched, SSH Key Management Guide, Service Account Inventory Template.