GitHub Repo Deployment Keys Rotated
More Info:
Deploy keys can have significant access to a repository and should be rotated on a regular basis.
Risk Level
Medium
Address
Security
Compliance Standards
- Cloudanix Best Practice
Triage and Remediation
- Remediation
Remediation
Using Console
To remediate a “Repo Deployment Keys Rotated” finding in GitHub, you essentially need to create a new SSH deploy key pair and replace the old deploy key in the repository via the GitHub web console.
Below are the minimal, step‑by‑step instructions.
1. Generate a New SSH Key Pair (Deploy Key)
Run this on a secure machine that will hold the private key (e.g., your CI server or a secure admin machine):
ssh-keygen -t ed25519 -C "deploy-key-for-<repo-name>" -f ~/.ssh/<repo-name>-deploy-key
- Press Enter for no passphrase (deploy keys usually must be non‑interactive).
- This creates:
- Private key:
~/.ssh/<repo-name>-deploy-key - Public key:
~/.ssh/<repo-name>-deploy-key.pub
- Private key:
Copy the public key:
cat ~/.ssh/<repo-name>-deploy-key.pub
Keep the private key secure; you’ll configure it wherever the deployment/CI runs.
2. Add the New Deploy Key in GitHub Console
- Go to the repository in GitHub.
- Click Settings (top of repo).
- In the left menu, click Deploy keys.
- Click Add deploy key.
- Fill in:
- Title: Something clear (e.g.,
CI Deploy Key (rotated 2026-07-25)). - Key: Paste the public key you copied.
- Allow write access:
- Enable only if this key must push to the repo; otherwise leave unchecked.
- Title: Something clear (e.g.,
- Click Add key.
3. Update the System That Uses the Deploy Key
On the system that actually performs the deployment (CI/CD tool, build server, etc.):
-
Replace the old private key with the new private key (
<repo-name>-deploy-key). -
Ensure SSH config (if used) points to this new key, for example in
~/.ssh/config:Host github.comHostName github.comUser gitIdentityFile ~/.ssh/<repo-name>-deploy-key -
Test connectivity:
ssh -T git@github.comYou should see a message like “Hi
<user>! You've successfully authenticated…”
4. Remove the Old Deploy Key (Complete the Rotation)
Once you confirm your deployments/CI work with the new key:
- In the same repo, go to Settings → Deploy keys.
- Find the old deploy key entry.
- Click Delete (or the trash icon) next to the old key.
- Confirm deletion.
5. (Optional) Enforce Better Practices
- Use read-only keys unless write is strictly needed.
- Rotate keys on a regular schedule.
- Track which system each deploy key belongs to via clear titles.
If you tell me your CI/CD tool (e.g., GitHub Actions, Jenkins, GitLab CI, etc.), I can give the exact steps to plug the new private key into that system.
Using CLI
Here’s how to remediate a “Repo Deployment Keys Rotated” issue by rotating GitHub deploy keys using the GitHub CLI (gh).
Assumptions:
- You have
ghinstalled and authenticated (gh auth login). - You have
ssh-keygenavailable.
1. Identify the repo and existing deploy keys
# Set variables
OWNER="your-org-or-username"
REPO="your-repo-name"
# List current deploy keys
gh api repos/$OWNER/$REPO/keys --jq '.[] | {id, title, read_only, created_at}'
Note the id and title of the key(s) you want to rotate.
2. Generate a new SSH key pair
# Create a new key pair specifically for this repo
ssh-keygen -t ed25519 -C "deploy-key-$OWNER-$REPO-$(date +%Y%m%d)" -f ~/.ssh/deploy_key_$OWNER_$REPO -N ""
Files created:
- Private:
~/.ssh/deploy_key_$OWNER_$REPO - Public:
~/.ssh/deploy_key_$OWNER_$REPO.pub
3. Add the new deploy key via GitHub CLI
NEW_KEY_TITLE="deploy-key-$OWNER-$REPO-$(date +%Y%m%d)"
NEW_PUB_KEY=$(cat ~/.ssh/deploy_key_$OWNER_$REPO.pub)
# Add as read-only (set to false if you truly need write access)
gh api repos/$OWNER/$REPO/keys \
-X POST \
-f title="$NEW_KEY_TITLE" \
-f key="$NEW_PUB_KEY" \
-F read_only=true
Verify it was added:
gh api repos/$OWNER/$REPO/keys --jq '.[] | {id, title, read_only, created_at}'
4. Update automation/CI to use the new private key
Wherever the old deploy key was used (CI/CD tool, server, etc.):
-
Replace the old private key with:
~/.ssh/deploy_key_$OWNER_$REPO -
Ensure permissions are strict:
chmod 600 ~/.ssh/deploy_key_$OWNER_$REPO -
Test connectivity:
GIT_SSH_COMMAND="ssh -i ~/.ssh/deploy_key_$OWNER_$REPO -o IdentitiesOnly=yes" \git ls-remote git@github.com:$OWNER/$REPO.git
5. Remove the old deploy key via GitHub CLI
Once the new key is confirmed working:
-
Get the ID of the old key:
gh api repos/$OWNER/$REPO/keys --jq '.[] | {id, title}' -
Delete the old key (replace
OLD_KEY_ID):OLD_KEY_ID=12345678gh api repos/$OWNER/$REPO/keys/$OLD_KEY_ID -X DELETE -
Confirm removal:
gh api repos/$OWNER/$REPO/keys --jq '.[] | {id, title}'
6. (Optional) Enforce a rotation practice
- Document:
- Key name pattern
- Rotation interval (e.g., 90 days)
- Procedure above as standard runbook.
- If “GitHub IAM” is a scanner/policy engine, re-run it to confirm the misconfiguration is cleared.
Using Python
Below is a concrete, step‑by‑step way to “remediate” a Repo Deployment Keys Rotated finding for GitHub using Python – i.e., automatically rotate (replace) deploy keys on a repo via the GitHub API.
1. Prerequisites
- Personal Access Token (PAT) (or GitHub App token) with:
reposcope (for private repos)- Or at least
public_repo(for public repos)
- Python 3.8+ installed.
requestsandcryptography(for generating SSH keys):
pip install requests cryptography
Set your GitHub token as an environment variable (safer than hard‑coding):
export GITHUB_TOKEN="ghp_XXXXXXXXXXXXXXXXXXXX"
2. High‑Level Rotation Flow
For each affected repository:
- Generate a new SSH keypair.
- Add the new public key as a deploy key via GitHub API.
- Update any systems/CI/CD pipelines that use the private key.
- Remove the old deploy key from the repo via GitHub API.
3. Generate a New SSH Keypair in Python
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
def generate_ssh_keypair():
# Generate private key
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=4096,
backend=default_backend()
)
# Private key PEM
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
).decode("utf-8")
# Public key in OpenSSH format
public_key = private_key.public_key()
public_openssh = public_key.public_bytes(
encoding=serialization.Encoding.OpenSSH,
format=serialization.PublicFormat.OpenSSH,
).decode("utf-8")
return private_pem, public_openssh
# Example usage
private_key_pem, public_key_ssh = generate_ssh_keypair()
print("Public key:", public_key_ssh)
# Store/print private_key_pem securely for your CI/CD or deployment system
4. Add the New Deploy Key via GitHub API
GitHub API endpoint:
POST /repos/{owner}/{repo}/keys
import os
import requests
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
REPO_OWNER = "your-org-or-user"
REPO_NAME = "your-repo"
def add_deploy_key(title, public_key, read_only=True):
url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/keys"
headers = {
"Authorization": f"token {GITHUB_TOKEN}",
"Accept": "application/vnd.github+json",
}
payload = {
"title": title,
"key": public_key,
"read_only": read_only, # False if you need write
}
resp = requests.post(url, json=payload, headers=headers)
resp.raise_for_status()
return resp.json()
# Example: add newly-generated key
private_key_pem, public_key_ssh = generate_ssh_keypair()
new_key = add_deploy_key("rotated-deploy-key-2026-07-25", public_key_ssh)
print("New deploy key ID:", new_key["id"])
Important:
Save private_key_pem securely (secret manager, CI/CD secret, etc.) and update any service that uses the old deploy key to now use this new private key.
5. List Existing Deploy Keys (Find Old Key to Remove)
GitHub API endpoint:
GET /repos/{owner}/{repo}/keys
def list_deploy_keys():
url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/keys"
headers = {
"Authorization": f"token {GITHUB_TOKEN}",
"Accept": "application/vnd.github+json",
}
resp = requests.get(url, headers=headers)
resp.raise_for_status()
return resp.json()
keys = list_deploy_keys()
for k in keys:
print(k["id"], k["title"], k["key"])
Use this to identify the old key either by title, a known substring in the key, or manually.
6. Remove the Old Deploy Key
GitHub API endpoint:
DELETE /repos/{owner}/{repo}/keys/{key_id}
def delete_deploy_key(key_id: int):
url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/keys/{key_id}"
headers = {
"Authorization": f"token {GITHUB_TOKEN}",
"Accept": "application/vnd.github+json",
}
resp = requests.delete(url, headers=headers)
if resp.status_code not in (204, 404):
# 204 = success; 404 = already gone
resp.raise_for_status()
# Example: delete all keys with a specific old title pattern
OLD_TITLE = "old-deploy-key"
for k in list_deploy_keys():
if OLD_TITLE in k["title"]:
print(f"Deleting key {k['id']} ({k['title']})")
delete_deploy_key(k["id"])
7. Putting It All Together (Rotation Script)
- Generate new keypair.
- Add new public key as deploy key.
- Update your CI/CD/deployment system with the new private key.
- Confirm it can clone/push.
- Delete the old deploy key.
You can wrap the above steps into a single script that:
- Accepts
owner,repo, and theold_key_title(orold_key_id) as input. - Outputs the new private key so you can inject it into your secrets store.
8. Optional: Automate Regular Rotation
To make this a recurring remediation:
- Put the rotation script into a secure automation environment (e.g., GitHub Actions, Jenkins, or a separate runner).
- Store the PAT or GitHub App token in a secret manager.
- Schedule the job (e.g., run monthly/quarterly).
- After each run, automatically update the target systems’ SSH private key secrets.
If you share the name pattern of the old keys and how your CI/CD currently stores the private key (GitHub Actions secret, Jenkins credential, etc.), I can tailor the Python script to fully automate the update on that side as well.
Using Terraform
# GitHub provider configuration
provider "github" {
owner = "GITHUB_ORG_OR_USER" # e.g. "my-org"
token = "GITHUB_TOKEN_WITH_REPO_SCOPE" # or use environment variable GITHUB_TOKEN
}
# Existing repository
resource "github_repository" "repo" {
name = "REPOSITORY_NAME" # e.g. "my-service"
visibility = "private"
description = "REPOSITORY_DESCRIPTION"
}
# NEW deploy key (rotation target)
# Generate a new SSH key pair outside Terraform; put the PUBLIC key here.
resource "github_repository_deploy_key" "new_deploy_key" {
repository = github_repository.repo.name
title = "NEW_DEPLOY_KEY_TITLE" # e.g. "ci-deploy-key-2024-07"
key = "NEW_PUBLIC_SSH_KEY" # e.g. "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI..."
read_only = false # set to true if write is not required
# rotation note: changing `key` forces replacement of this resource
}
# Remove the OLD deploy key from Terraform state/config
# 1. Replace the old resource block in your code with the new one above, OR
# 2. If you still have it declared, delete the old block:
#
# resource "github_repository_deploy_key" "old_deploy_key" { ... } # <-- DELETE this block
#
# Any applications using the old key must be reconfigured to use the
# NEW private key that matches NEW_PUBLIC_SSH_KEY before applying.
Rotating the key is done by generating a new SSH key pair, updating the applications to use the new private key, then removing the old github_repository_deploy_key resource from Terraform so terraform plan shows the old key being destroyed and the new key created.