Skip to main content

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​

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

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​

  1. Go to the repository in GitHub.
  2. Click Settings (top of repo).
  3. In the left menu, click Deploy keys.
  4. Click Add deploy key.
  5. 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.
  6. 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.):

  1. Replace the old private key with the new private key (<repo-name>-deploy-key).

  2. Ensure SSH config (if used) points to this new key, for example in ~/.ssh/config:

    Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/<repo-name>-deploy-key
  3. Test connectivity:

    ssh -T git@github.com

    You 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:

  1. In the same repo, go to Settings → Deploy keys.
  2. Find the old deploy key entry.
  3. Click Delete (or the trash icon) next to the old key.
  4. 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 gh installed and authenticated (gh auth login).
  • You have ssh-keygen available.

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.):

  1. Replace the old private key with:
    ~/.ssh/deploy_key_$OWNER_$REPO

  2. Ensure permissions are strict:

    chmod 600 ~/.ssh/deploy_key_$OWNER_$REPO
  3. 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:

  1. Get the ID of the old key:

    gh api repos/$OWNER/$REPO/keys --jq '.[] | {id, title}'
  2. Delete the old key (replace OLD_KEY_ID):

    OLD_KEY_ID=12345678

    gh api repos/$OWNER/$REPO/keys/$OLD_KEY_ID -X DELETE
  3. 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​

  1. Personal Access Token (PAT) (or GitHub App token) with:
    • repo scope (for private repos)
    • Or at least public_repo (for public repos)
  2. Python 3.8+ installed.
  3. requests and cryptography (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:

  1. Generate a new SSH keypair.
  2. Add the new public key as a deploy key via GitHub API.
  3. Update any systems/CI/CD pipelines that use the private key.
  4. 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)​

  1. Generate new keypair.
  2. Add new public key as deploy key.
  3. Update your CI/CD/deployment system with the new private key.
  4. Confirm it can clone/push.
  5. Delete the old deploy key.

You can wrap the above steps into a single script that:

  • Accepts owner, repo, and the old_key_title (or old_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.

Additional Reading:​