A private key committed to a repository is a machine identity handed to everyone who can clone it, and unlike an API key no provider will revoke it for you. PEM files, PKCS #12 bundles, OpenSSH keys and signing keys end up in Git through test fixtures, deployment scripts, Docker contexts and "temporary" commits, and they stay in history long after the file is deleted. Private key rules are among the highest volume findings in secret scanners, and each one needs a decision: what the key unlocks, whether it is encrypted, who must revoke it and whether history should be rewritten. This guide explains the key formats and what they identify, who holds privileged access to them, how scanners detect them and where they miss, how to respond and how to stop keys reaching repositories. It is written for application security, platform and PKI teams. It goes deeper than the Git Secret Scanning Guide on this one credential type.
Key takeaways
- Revoke first, rewrite second. GitHub's own guidance is that rotating or revoking the secret is the first step and may be enough; rewriting history has costs and never reaches existing clones and forks.
- Nobody revokes generic private keys for you. GitHub's generic patterns (
rsa_private_key,ec_private_key,openssh_private_key,pgp_private_key,generic_private_key) have no partner notification and no validity check, so the owning team must act. - Encrypted is not safe. A passphrase-protected PEM, an OpenSSH key with a bcrypt KDF or a password-protected PKCS #12 file can be attacked offline indefinitely; treat it as exposed.
- The key type decides the response. A TLS key needs certificate revocation and reissue, an SSH key needs removal from every
authorized_keysand forge account, a signing key may require re-signing releases. - Prevention is generation in the right place. Keys created inside an HSM, KMS, SSH certificate authority or CI secret store never exist as files a developer can commit.
How private keys work as identities in repositories
A private key proves possession of an identity: a server name in a certificate, an SSH user or deploy key, a signing identity for code or packages, a token issuer, or a cloud service account. Finding one in Git therefore starts with identifying the format and then the identity bound to it.
| Format | How it looks in a repository | Typical identity | Can be encrypted |
|---|---|---|---|
| PKCS #8 (RFC 5208, RFC 5958) | PEM label PRIVATE KEY, or ENCRYPTED PRIVATE KEY | TLS server or client keys, JWT signing keys, cloud keys | Yes (EncryptedPrivateKeyInfo) |
| PKCS #1 RSA | Legacy PEM label RSA PRIVATE KEY | Older TLS and application keys | Yes, via legacy PEM encryption headers |
| SEC1 elliptic curve | Legacy PEM label EC PRIVATE KEY | TLS, device and signing keys | Yes, legacy headers |
OpenSSH (openssh-key-v1) | PEM-style label OPENSSH PRIVATE KEY, often named id_ed25519 or id_rsa | SSH user, deploy and automation keys | Yes: cipher plus bcrypt KDF with salt and rounds |
| OpenPGP | PGP PRIVATE KEY BLOCK | Release signing, package repository signing, email | Yes (passphrase) |
| PKCS #12 | Binary .p12 or .pfx file | Code signing and client certificates with chain | Yes (password) |
| Embedded keys | A PEM string inside JSON, YAML or code, such as a service account key file's private_key field | Cloud service accounts, OAuth client assertions | Usually not |
RFC 7468 defines the standard textual labels (PRIVATE KEY for PKCS #8 and ENCRYPTED PRIVATE KEY); the RSA PRIVATE KEY and EC PRIVATE KEY labels are older formats that remain common in the wild. The OpenSSH format is described in OpenSSH's PROTOCOL.key: a header naming the cipher and KDF, the public keys, then the private keys, which may be stored with cipher none. Matching the public half is how you identify the key: derive the public key or fingerprint and search certificate inventories, Certificate Transparency logs, authorized_keys files and forge accounts for it. See the Machine Identity, PKI and Certificate Lifecycle Guide for inventory approaches.
Admin roles and privileged access
Two groups hold privileged access in this problem: people who can read the repository and its history, and people who can revoke the identity the key represents. They are rarely the same people.
| Key found | Who can revoke | Revocation action | Blast radius if missed |
|---|---|---|---|
| TLS server key | PKI or certificate owner | Revoke certificate with the CA, generate a new key, reissue | Impersonation of the service, interception where traffic can be redirected |
| SSH user or automation key | System owners, identity team | Remove the public key from all authorized_keys, bastions and forge accounts | Lateral movement wherever the key is trusted |
| Forge deploy key | Repository admins | Delete the deploy key, create a new one with read-only access where possible | Read or write access to the repository |
| Code or package signing key | Release engineering, CA | Revoke the certificate or PGP key, re-sign current releases, publish the revocation | Malware signed as your organisation |
| JWT or SAML signing key | Application or IdP owners | Rotate the key, remove the old key from JWKS or metadata, invalidate sessions | Forged tokens for every user |
| Cloud service account key | Cloud platform team | Delete the key in the cloud console or API | Everything the service account can reach |
- Repository readers are key holders: every collaborator, CI system, fork owner and mirror holds a copy. A private repository narrows that group but does not make the key secret.
- Machine accounts with broad read access are a concentrated risk: in the GitHub code signing certificate theft a token for a machine account was used to clone repositories that held encrypted, password-protected code signing certificates, and GitHub revoked the certificates anyway.
- Name a revoking owner in advance: for each key class, record which team revokes and how fast, so a scanner alert is not left with the developer who committed it.
Integrations, API tokens and service accounts
Most keys reach repositories because an integration needed one and generating it locally was the quickest path. Replace each pattern with a design where the key never exists as a file in a working tree.
| Why the key was committed | Better pattern | Related guide |
|---|---|---|
| CI deploys over SSH | Short-lived SSH certificates issued per job, or OIDC federation to the target | SSH Protocol and Certificate Authority Guide |
| Application signs JWTs | Signing in a KMS or HSM, with the application calling a sign API | JWT Security Guide |
| TLS for a service or test environment | Automated issuance at deploy time (ACME or an internal CA), test keys generated during the test run | ACME and Automated Certificate Management Guide |
| Release signing in a pipeline | Signing service or HSM-backed key, or keyless signing tied to the workload identity | Code Signing Security Guide |
| Cloud service account key file | Workload identity federation; disable key creation by policy | Google Cloud Service Account Keys Security Guide |
| Encrypted secrets file with its key alongside | Keep the decryption key in a KMS or secret manager, never in the same repository | Cryptographic Key Management Guide |
Test fixtures are the most common legitimate-looking source. Keys generated only for unit tests are still keys: if a test key is ever reused in a real environment, or trusted by a staging system, it is a real credential. Generate them at test time, or mark committed fixtures clearly and allowlist them by path in the scanner so real findings stand out. The Zeabur second breach is a reminder that long-lived credentials kept in plaintext in a configuration repository can survive a rotation exercise unnoticed.
Logging and detection
| Scanner | Rule or type | What it matches | Limits |
|---|---|---|---|
| GitHub secret scanning | rsa_private_key, ec_private_key, openssh_private_key, pgp_private_key, generic_private_key | PEM-style headers for each format; rated high precision | Generic patterns: no default push protection (configurable), no partner notification, no validity check, no base64 support |
| GitHub secret scanning | github_ssh_private_key | SSH private keys that authenticate to GitHub | Provider pattern with push protection and validity check |
| Gitleaks | private-key | Any -----BEGIN ... PRIVATE KEY----- or PRIVATE KEY BLOCK with at least 64 characters of body | Misses keys with headers stripped or split across strings |
| Gitleaks | pkcs12-file | Files named *.p12 or *.pfx (path rule) | Binary keystores under other names, such as .jks, need custom rules |
| Gitleaks | age-secret-key | AGE-SECRET-KEY-1 keys for the age encryption tool | Format specific |
- Turn on push protection for generic patterns: GitHub lets you configure push protection for generic patterns; private key headers have few false positives, so they are good candidates. See the Push Protection Guide.
- Scan beyond Git: container image layers, build artefacts, wiki attachments and backups of repositories hold the same files.
- Watch use, not only presence: after a finding, look for authentication with the matching SSH public key in server logs, new certificates or signatures using the key, and cloud API calls by the service account. ATT&CK tracks this as T1552.004 Private Keys, with follow-on use such as T1021.004 SSH and T1553.002 Code Signing.
Metrics: private key findings per month by key class; median time from finding to revocation (not to file deletion); share of findings where the identity was confirmed; number of repositories with push protection on generic key patterns; share of signing and SSH automation keys held in HSM, KMS or CA-issued short-lived form.
Hardening baseline
- Baseline scan: scan all repositories, including full history, archived repositories and forks you control, for private key headers and keystore files. Expect a long tail of old findings.
- Triage by identity: for each key derive the public key or fingerprint, find what trusts it and assign a revoking owner. Test fixtures that nothing trusts can be allowlisted by path.
- Revoke and replace: revoke certificates, remove SSH keys, delete deploy and service account keys, rotate signing keys. Deploy and test the replacement before revoking where the service allows it, so revocation does not cause an outage; if something breaks, roll forward with another new key, never back to the leaked one.
- Decide on history rewrite: rewrite only when rotation is impossible or the key protects data that stays sensitive (for example decryption keys for archived data). Follow GitHub's process:
git-filter-repo2.47 or later with--sensitive-data-removal, a forced mirror push, a support request to purge cached views and pull request references, and collaborators rebasing rather than merging. - Prevent recurrence: push protection for generic key patterns, pre-commit hooks,
.gitignoreentries for*.pem,*.key,*.p12,*.pfxandid_*, and organisation policy disabling cloud key file creation. - Move key generation: issue SSH certificates and TLS certificates from automated authorities, and hold signing keys in HSM or KMS.
Small teams should focus on steps 1, 3 and 5. Larger organisations need a key-class runbook with named revoking owners and service levels, plus central reporting on findings to revocation time.
Leak response
- Identify the format and the identity, and whether the key is encrypted. Treat encrypted keys as exposed.
- Revoke or remove trust using the action for that key class; record the time.
- Search logs for use of the key since the earliest commit date, not just since detection.
- Remove the file from the default branch and decide on history rewrite; if forks exist, contact their owners.
- Record root cause and the preventive control added. The Secrets Rotation Runbook Template provides the evidence fields.
Framework mapping
| Control area | NIST SP 800-53 Rev 5 | NIST CSF 2.0 | ISO/IEC 27001:2022 Annex A | MITRE ATT&CK |
|---|---|---|---|---|
| Key generation, storage and destruction | SC-12 Cryptographic Key Establishment and Management | PR.DS-01 | A.8.24 Use of cryptography | T1552.004 Private Keys |
| No keys embedded in code | IA-5(7) No Embedded Unencrypted Static Authenticators | PR.AA-01 | A.8.4 Access to source code | T1552.001 Credentials In Files |
| Public key authentication and certificates | IA-5(2) Public Key-based Authentication, SC-17 | PR.AA-01 | A.5.17 Authentication information | T1649 Steal or Forge Authentication Certificates |
| SSH trust and signing keys | AC-6, CM-3 | PR.AA-05 | A.8.2 Privileged access rights | T1098.004 SSH Authorized Keys, T1553.002 Code Signing |
| Detection and response | SI-4, AU-6 | DE.CM-09, RS.MI-02 | A.8.16 Monitoring activities | T1021.004 SSH |
Common misconfigurations
- Deleting the file and closing the alert: the key is still in history, clones and forks, and still trusted.
- Rewriting history before revoking: it delays the one action that ends the exposure and alerts anyone with a clone to look at the removed data.
- "It was encrypted": passphrases on committed keys are often weak or sit in the same repository or CI configuration.
- Test keys trusted by real systems: a fixture key added to a staging
authorized_keysor trust store becomes a production credential. - Keystores the scanner cannot see: binary
.jksor renamed PKCS #12 files pass text-based rules. - Signing keys on developer laptops: they are committed, backed up and synchronised; signing belongs in an HSM or signing service.
Practitioner checklist
- Scan all repository history, images and artefacts for private key headers and keystore files.
- Enable push protection for GitHub generic private key patterns and add pre-commit scanning.
- Maintain a key-class runbook naming the revoking owner and the revocation action for each class.
- Measure time from finding to revocation, not to file deletion.
- Treat encrypted keys and keystores as exposed.
- Rewrite history only when rotation cannot mitigate, following the git-filter-repo sensitive data process.
- Replace SSH key files in automation with short-lived certificates or federation.
- Move code, package and token signing keys into HSM, KMS or a signing service.
- Disable cloud service account key creation where federation is available.
Standards and references
- IETF: RFC 7468, Textual Encodings of PKIX, PKCS and CMS Structures
- NIST: SP 800-57 Part 1 Rev 5, Recommendation for Key Management
- GitHub Docs: Supported secret scanning patterns
- GitHub Docs: Removing sensitive data from a repository
- OpenSSH: PROTOCOL.key, private key format
- Gitleaks: Project repository and default rules
- MITRE ATT&CK: T1552.004 Private Keys
Related NHI Mgmt Group resources: Git Secret Scanning Guide · SSH Key Management Guide · Cryptographic Key Management Guide · Secrets Management Guide