Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXXXIV · Git SecuritySSHKeys

SSH keys and deploy keys — personal keys, deploy keys, host keys, and the read-only distinction

Advanced⏱ ~24 mingitssh-keygenssh-add

What you'll learn

  • Distinguish personal account keys, deploy keys, and host keys by scope and lifetime
  • Generate an ed25519 SSH key pair with ssh-keygen and load it into the agent with ssh-add
  • Configure a read-only deploy key for a CI runner and a read/write deploy key for a deploy host
  • Recognise the failure modes of key reuse, key-without-passphrase on shared hosts, and unrotated keys

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

SSH authentication for Git is built from three classes of key, each with a different scope and a different lifetime: the personal account key, the deploy key, and the host key. The class is the scope: a personal key has the rights of the account, a deploy key is scoped to one repository, and a host key authenticates the server rather than the client. Confusing the three classes is the most common SSH mistake in an infrastructure repository.

Personal account keys

A personal account key is an SSH key pair whose public half is registered against a user’s account on the forge. The forge treats a successful challenge with that key as “this user is acting”, and the user can clone, fetch, push, and merge against every repository their account can see. The private half lives on the engineer’s workstation, typically at ~/.ssh/id_ed25519 or under a named path, and is loaded into the SSH agent so the engineer does not re-enter the passphrase for every push.

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519
ssh-add ~/.ssh/id_ed25519
ssh -T git@github.com

The diagnostic ssh -T git@${GIT_HOST} connects to the forge and confirms that the key registered against the engineer’s account is the key the agent is presenting. A successful response names the account and notes that the forge does not provide shell access; any other response (or a Permission denied (publickey)) is a key-or-account mismatch and the place to start debugging.

The personal account key is the right choice for engineer workstations where the engineer’s account is the right scope. It is the wrong choice for any machine that should not inherit the engineer’s permissions — a CI runner, a deploy host, a shared bastion.

Deploy keys

A deploy key is an SSH key pair whose public half is registered against a single repository on the forge. The forge treats a successful challenge as “the holder of this key is acting on this one repository”, and the holder can either read the repository or read and write it depending on how the deploy key was added. The deploy key does not inherit any account’s permissions; it has exactly the rights granted at registration time.

flowchart LR
    A["deploy key pair"] --> B["public half registered on repo X"]
    B --> C{"scope at registration"}
    C -->|read-only| D["git clone works"]
    C -->|read-only| E["git push refused"]
    C -->|read/write| F["git clone works"]
    C -->|read/write| G["git push works"]

Two scoping options, set at registration:

  • Read-only. The key can clone and fetch but cannot push. This is the right scope for a CI runner that only needs to check out the repository to run builds and tests. A read-only deploy key cannot be used to push a malicious commit to the default branch.
  • Read/write. The key can clone, fetch, and push. This is the right scope for a deploy host that pulls and pushes infrastructure state, or for a release pipeline that cuts signed tags.

The discipline is to register a deploy key with the narrowest scope the use case actually requires. A CI runner that only needs to check out code wants a read-only deploy key, not a read/write one; a deploy host that only needs to clone wants a read-only deploy key plus a separate write path for the deploy step. The blast radius of a deploy-key compromise is the scope of the deploy key — read-only deploy keys on production repositories cannot push, by construction.

# On the CI runner
ssh-keygen -t ed25519 -f ~/.ssh/deploy_readonly
ssh-add ~/.ssh/deploy_readonly

# The public key (~/.ssh/deploy_readonly.pub) is registered
# against the repository at Settings -> Deploy keys -> Read-only

A single SSH key cannot be a deploy key for multiple repositories in GitHub — each repository requires its own key registration. For multi-repository deploy access from a single machine, the patterns are: one key per repository and Host aliases in ~/.ssh/config to keep the URLs clean; or a forge-side machine user with a personal account key that has been granted access to each repository.

Host keys

A host key is the SSH server’s identity key: it authenticates the server to the client during the initial handshake. The client checks the server’s host key against ~/.ssh/known_hosts before sending any authentication material; a host key that does not match is either a first connection (the client prompts to add the key) or a man-in-the-middle attempt (the client refuses the connection).

The host key is the forge’s identity, not the client’s. Compromising the host key lets an attacker impersonate the forge to clients; compromising a client key lets an attacker impersonate the client to the forge. The two threats are different and the responses are different: a host-key compromise is a forge-side incident; a client-key compromise is the per-client incident response covered in XXXIV-05.

Generation, passphrase, and rotation

Every SSH key for a production use case should be:

  • ed25519. ssh-keygen -t ed25519 is the modern algorithm; it is shorter than RSA for the same security level and has better performance.
  • Passphrase-protected. A key without a passphrase is a credential whose decrypted form lives on disk; a passphrase-protected key whose decrypted form lives only in the agent is a credential that requires the agent’s owner to use it.
  • Rotated on a schedule. The rotation cadence is the team’s policy: quarterly for personal account keys, per-deploy for deploy keys on long-lived hosts, and immediately on any suspected compromise.
ssh-keygen -t ed25519 -f ~/.ssh/deploy_production
ssh-add ~/.ssh/deploy_production

Rotation is a four-step dance: generate the new key, add it to the forge (account keys: register against the account; deploy keys: add to the repository with the right scope), update every client to load the new key, and revoke the old key once every client is migrated. The oldest-key-first removal is what stops an unrotated key from being a permanent credential on a lost laptop.

Production discipline

  1. Use ed25519 for every new key. ssh-keygen -t ed25519 is the modern default; legacy RSA keys can be kept but new keys should be ed25519.
  2. Passphrase every key that touches a shared host. A key without a passphrase on a shared runner is a credential that any process on the runner can read.
  3. Register the deploy key with the narrowest scope the use case allows. Read-only for CI runners; read/write only when the machine must push.
  4. Provision known_hosts on every production deploy host. StrictHostKeyChecking yes and a populated known_hosts is the right default for a host that cannot ask.
  5. Rotate keys on a schedule and on any suspected compromise. The rotation cadence is the team’s policy; the immediate rotation is non-negotiable.

Cross-course references

  • Git, CI/CD & GitOps — Part XXXIV-01 (Authentication options) — the SSH-vs-HTTPS framing that this lesson extends.
  • Git, CI/CD & GitOps — Part XXVI-06 (Signing configuration) — SSH signing reuses the same key pair; the rotation policy applies to both.
  • Git, CI/CD & GitOps — Part XXXIII-06 (Signing policy) — the joiners-movers-leavers process that governs key removal.
  • Linux for Production Sysadmins — Part XII (RepositorySecurity) — the apt/dnf trust-store pattern that mirrors the deploy-key pattern.

Quiz

Knowledge check · 4 questions

  1. Q1. A CI runner needs to check out an infrastructure repository to run terraform plan in pull-request builds. The runner must not be able to push to the repository under any circumstance. Which SSH key class and scope is the right choice?

  2. Q2. A single SSH key can be registered as a deploy key against multiple repositories on GitHub and will work against all of them once registered.

  3. Q3. Name the three classes of SSH key used in Git workflows and the scope each one grants on a successful challenge.

  4. Q4. Diagnose why a CI runner is pushing commits to the production repository, and recommend the deploy-key rotation that contains the incident.

    A team operates a CI runner that builds and tests pull requests. The runner authenticates to the forge with a deploy key registered against the production repository. The deploy key was registered with read/write scope because the team did not think carefully about it at registration time; the runner only needs to check out code. An attacker compromises the runner via a vulnerable build step, finds the deploy key, and pushes a malicious commit to a feature branch. Branch protection refuses the merge, but the attacker has now demonstrated write access to the production repository — a far worse blast radius than the runner's actual job required.

Passing score: 75%. Answers are checked in this browser.