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: