Git Deploy Keys Explained: GitHub, GitLab, and Bitbucket Compared

Devops & Infrastructure, Git, Security, and What Is

Git Deploy Keys Explained: GitHub, GitLab, and Bitbucket Compared

Every automated deployment has the same first problem: something that is not a human needs to pull your code. A CI runner, a deployment service, a build box in a cupboard. Whatever it is, it needs credentials.

The lazy answer is to hand it a developer's SSH key. That works immediately and is a genuinely bad idea — you have just given a build server the same repository access as the person who set it up, and when that person leaves, deployments break alongside their account.

A deploy key is the narrow fix. It is an SSH key pair whose public half is registered against one repository rather than a user account. The machine holding the private half can clone that repository and nothing else.

That much is standard. What is not standard is how the three major hosts implement it — they disagree on write access and on key reuse, which are the two things most likely to bite you.

What a deploy key actually is

There is nothing special about the key itself. You generate an ordinary SSH key pair:

ssh-keygen -t ed25519 -C "deploy key for example-app" -f ~/.ssh/example-app-deploy

What makes it a deploy key is where you register the public half. Add it to a user account and it inherits everything that user can reach. Add it to a repository's deploy keys and its reach stops at that repository's boundary.

The private key then lives on the machine doing the deploying, and ssh-agent or an explicit IdentityFile entry in ~/.ssh/config points Git at it:

Host github.com-example-app
  HostName github.com
  User git
  IdentityFile ~/.ssh/example-app-deploy
  IdentitiesOnly yes

IdentitiesOnly yes matters more than it looks. Without it, SSH offers every key in your agent in turn, and the host authenticates you as whichever one matches first — which may not be the deploy key you intended.

Deploy key vs personal SSH key vs access token

Scope Auth method Revoking it affects
Personal SSH key Everything the user can see SSH That user, everywhere
Deploy key One repository SSH One repository
Access token Configurable, often account-wide HTTPS Everything in the token's scope

The practical difference is blast radius. Revoking a deploy key breaks exactly one deployment. Revoking a personal key breaks every machine that borrowed it, and you will find out which ones the hard way.

Where the three hosts disagree

This is the part worth bookmarking. All three call this feature something slightly different and implement it differently:

GitHub GitLab Bitbucket Cloud
Called Deploy keys Deploy keys Access keys
Write access? Optional — Allow write access checkbox Optional — Grant write permissions to this key No. Read-only only
Reuse across repos? No — You can't reuse a deploy key for multiple repositories Project keys are single-project; public deploy keys can be shared instance-wide Yes — Can be added to multiple repositories
UI path Settings → Deploy keys Settings → Repository → Deploy keys Repository settings → Security → Access keys

Two consequences fall out of that table.

GitHub's uniqueness rule is the one that surprises people. If you try to add the same public key to a second repository, GitHub rejects it with a key is already in use error. This is not a bug or a quota — a given public key can be a deploy key on exactly one GitHub repository, ever. Teams that generate one CI key and try to wire it into twelve repositories hit this wall on repository number two.

Bitbucket simply will not give you write access via an access key. If your deployment process needs to push — tagging releases, committing build artifacts, updating a submodule pointer — an access key cannot do it on Bitbucket Cloud. You need a different mechanism entirely.

Setting one up

The flow is the same everywhere once you know the UI path:

  1. Generate the pair on the machine that will deploy, not on your laptop. The private key should never travel.
  2. Copy the public half (.pub), paste it into the host's deploy-key screen, and name it after the machine so future-you knows what to revoke.
  3. Leave write access off unless you have a specific reason to enable it.
  4. Test before wiring it into anything:
ssh -T -i ~/.ssh/example-app-deploy git@github.com

GitHub answers with the repository the key is authorised for, which is a useful confirmation that you pasted the right key against the right repo.

If you are doing this on Bitbucket specifically, our walkthrough on how to set up SSH keys for Bitbucket covers the key generation and connection test end to end.


Wiring keys by hand across a dozen repositories is the kind of task that is fine once and miserable at scale. DeployHQ generates a dedicated key pair per project and hands you the public half to paste in — one key, one project, no reuse collisions to reason about.


When a deploy key is the wrong tool

Deploy keys are deliberately narrow, and that narrowness becomes a problem in three situations:

You need one identity across many repositories. On GitHub, the uniqueness rule makes this impossible with deploy keys. GitHub's current recommendation is a GitHub App rather than the older machine user pattern — Apps give you fine-grained, revocable permissions without consuming a human seat. On GitLab, a public deploy key covers the same ground natively. On Bitbucket, access keys are reusable, so the problem does not arise.

You need write access on Bitbucket. Covered above — access keys cannot do it.

You need more than Git. Deploy keys authenticate Git over SSH and nothing else. If your pipeline also pulls from a package registry or container registry, GitLab's deploy tokens are the right tool: they work over HTTPS and cover the registries, where deploy keys do not.

Read-only by default is a feature, not a limitation

Both GitHub and GitLab default deploy keys to read-only, and you should usually leave them there.

A deployment pipeline almost never needs to write to the repository it is deploying. It clones, it builds, it ships. The temptation to enable write access usually comes from wanting the pipeline to push a version bump or a changelog commit back — which is convenient right up until a compromised build box can rewrite your main branch.

If you do need write access, scope it to a repository that exists for that purpose rather than granting it on your primary application repo. The same instinct applies to everything else a deployment touches: our practical hardening checklist for Linux deployment servers walks through the equivalent reasoning on the server side.

Rotating and revoking

Deploy keys have a quiet failure mode: they work forever, so nobody thinks about them. A key added in 2021 for a CI provider you stopped using in 2022 is still sitting there with read access.

Two habits fix this:

  • Name keys after the machine or service, never deploy key or CI. A list of eight keys called deploy key is unauditable.
  • Review the deploy key list whenever you decommission anything. Retiring a build server means removing its key in the same change, not eventually.

Rotation itself is simple because the scope is small — generate a new pair, add the new public key, update the deploying machine, delete the old key. Because the key only covers one repository, there is no coordination cost with other teams.

If GitHub is refusing to let you add a key at all, the usual cause is organisation policy rather than the key itself; our support note on deploy keys being disabled for a repository covers that case.

How this fits a real deployment setup

Deploy keys solve repository access. They do not solve the other half of the problem, which is how the deploying machine authenticates to your server — that is a separate SSH key in the other direction, and conflating the two is a common source of confusion when a deployment fails and it is unclear which end rejected the connection.

They also do not solve secrets. A deploy key gets your code out of the repository; it does nothing about the database password that code needs at runtime. Those belong in your deployment platform's environment configuration — see managing secrets with .env files for how that layer works, and keeping API keys out of your Git repository for how to stop them leaking into commits in the first place.

If you are assembling this stack yourself, setting up Git-based deployment on a VPS covers the full path from repository to running application. If the target server is not publicly reachable, deploying to servers behind a firewall compares the approaches.

And if you are currently doing all of this inside GitHub Actions, it is worth reading why SSH-based deploy steps in GitHub Actions tend to break once you have more than one environment to think about.

Wrapping up

A deploy key is a small idea with a large payoff: one key, one repository, one thing to revoke. The complications are not conceptual, they are per-platform — GitHub will not let you reuse a key, Bitbucket will not let you write with one, and GitLab gives you an escape hatch for both.

Get the scope right once and the thing mostly disappears from your life, which is the highest praise any piece of deployment infrastructure can earn.

DeployHQ handles the key management side for you — it generates a per-project key pair, and once you have pasted the public half into your repository, automatic deployments run on every push. You can deploy from GitHub or deploy from GitLab with the same setup.


Questions about deploy keys or anything else deployment-shaped? Email us at support@deployhq.com or find us on @deployhq.