ply.wtf
Software & DevelopmentStep-by-step guide

Cloning a private GitHub repository over SSH on Ubuntu

11 min read
  • ubuntu
  • git
  • github
  • ssh
  • deploy-keys
  • linux

Cloning a public repository is one command and no credentials. A private one needs GitHub to know who is asking, and over SSH that means a key it recognises. The mechanics are simple; the error messages are not, which is where the time goes.

This guide is the narrow version of the task. For installing Git, tokens, the gh CLI, commit signing and running two accounts on one machine, see the full Git and GitHub guide.

The short version

ssh-keygen -t ed25519 -C "your@email.com"
cat ~/.ssh/id_ed25519.pub          # paste this into GitHub
ssh -T git@github.com              # verify
git clone git@github.com:owner/repo.git

If that works, you are done. The rest of this page is for when it does not, and for the case where the machine doing the cloning is a server rather than your laptop.

First decide which kind of key

This is the choice that matters, and it is usually made by accident.

Personal key Deploy key
Scope Every repo your account can reach One repository
Lives on Your own machines A server
Write access Yes Optional, off by default
If the machine is compromised Your whole GitHub account That one repository, read-only

Your laptop: personal key. You need to reach many repositories and push to them.

A server: deploy key. A VPS that pulls one repository to deploy it has no business holding credentials for everything else you own. This is not paranoia — a leaked personal key on a web server is one of the more common ways a GitHub account gets taken over.

Cloning onto your own machine

1. Check for a key you already have

ls -la ~/.ssh

id_ed25519 and id_ed25519.pub mean you have one. Skip to step 3.

2. Generate one

ssh-keygen -t ed25519 -C "your@email.com"

Press Enter for the default path, and set a passphrase — on a personal machine there is no good reason not to. The agent in the next step means you type it once per session.

3. Load it into the agent

eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519

To avoid repeating that in every terminal, put this in ~/.ssh/config:

Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes
    AddKeysToAgent yes

4. Give GitHub the public key

cat ~/.ssh/id_ed25519.pub

Copy the whole line. On GitHub: Settings → SSH and GPG keys → New SSH key. Key type Authentication Key, name it after the machine.

Only the .pub file. The other one never leaves the machine.

5. Test before cloning

ssh -T git@github.com
Hi yourname! You've successfully authenticated, but GitHub does not provide shell access.

That is success. It exits with status 1 and it still worked — GitHub has no shell to give you.

Testing here rather than going straight to git clone is worth the extra ten seconds: it separates “my key is wrong” from “my repository path is wrong”, and those have very different fixes.

6. Clone

Get the URL from the repository page: green Code button → SSH tab. It looks like git@github.com:owner/repo.git. Then:

git clone git@github.com:owner/repo.git

Into a specific directory:

git clone git@github.com:owner/repo.git /opt/myapp

Cloning onto a server with a deploy key

This is the case worth getting right. Run these on the server.

1. Generate a dedicated key

ssh-keygen -t ed25519 -C "deploy@myserver" -f ~/.ssh/myapp_deploy -N ""

-N "" means no passphrase, and here that is deliberate: an unattended git pull from cron or a deploy script cannot type one. That is precisely why this key must be scoped to a single repository and read-only — the passphrase is not what protects you, the scope is.

2. Add it as a deploy key

cat ~/.ssh/myapp_deploy.pub

On GitHub, in the repository — not in your account settings: Settings → Deploy keys → Add deploy key. Paste it, title it after the server, and leave Allow write access unticked unless the server genuinely needs to push.

3. Point SSH at it with a host alias

Since this key is not the default, give it a name in ~/.ssh/config:

Host github-myapp
    HostName github.com
    User git
    IdentityFile ~/.ssh/myapp_deploy
    IdentitiesOnly yes

Lock the permissions, or SSH will refuse to use the files:

chmod 700 ~/.ssh && chmod 600 ~/.ssh/myapp_deploy ~/.ssh/config && chmod 644 ~/.ssh/myapp_deploy.pub

4. Test and clone through the alias

ssh -T git@github-myapp

The greeting for a deploy key names the repository rather than a user — something like Hi owner/repo! You've successfully authenticated. That is the confirmation that the key is bound to the right repository.

git clone git@github-myapp:owner/repo.git /opt/myapp

Note the alias replaces the hostname, but the owner/repo part stays exactly as it is.

One deploy key, one repository

A deploy key can be attached to only one repository across the whole of GitHub. Try to add the same public key to a second repo and you get:

Key is already in use

This surprises people setting up a server that deploys three services. Two ways out:

  • One key per repository, each with its own Host alias. Fine for a handful, tedious beyond that.
  • A machine user — a separate GitHub account created for the server, given read access to the repositories it needs, with one normal SSH key. This is what GitHub recommends at scale, and it costs a seat on paid plans.

For a personal VPS running one or two projects, one key per repository is the simpler answer.

Adding a second repository to a server that already has one

This is worth its own section, because the symptom points nowhere near the cause.

The setup: a machine already deploying one project — call it conduit — with a deploy key sitting at the default path, ~/.ssh/id_ed25519. You clone a second, unrelated repository the obvious way:

git clone git@github.com:owner/newproject.git /opt/newproject
ERROR: Repository not found.

Nothing is broken. SSH found the default key, offered it, and GitHub accepted it — as the conduit deploy key, which has no visibility of newproject. A key scoped to one repository asking about another gets the same 404 as a stranger.

The diagnosis

ssh -T git@github.com

Read the greeting carefully, because it names who GitHub thinks you are:

Greeting Meaning
Hi username! A personal account key. Access follows your account
Hi owner/repo! A deploy key, scoped to exactly that repository
Permission denied (publickey) No key GitHub recognises

Being greeted by the name of a different repository is the whole answer: the key works, it is simply the wrong one for what you are cloning.

The fix

A second key, and an alias so the two never compete.

ssh-keygen -t ed25519 -C "server-newproject" -f ~/.ssh/newproject_deploy -N ""
cat ~/.ssh/newproject_deploy.pub

Add it under the new repository’s Settings → Deploy keys. Then in ~/.ssh/config:

Host github-newproject
    HostName github.com
    User git
    IdentityFile ~/.ssh/newproject_deploy
    IdentitiesOnly yes
ssh -T git@github-newproject

The greeting should now name the new repository. Then clone through the alias:

git clone git@github-newproject:owner/newproject.git /opt/newproject

The existing project is untouched — it still resolves github.com to the default key, and the new block only applies to the alias.

Why IdentitiesOnly yes is doing the real work here

Without it, SSH offers every key it can find, in whatever order it likes. On a machine with two deploy keys that means it will frequently present the first one, get authenticated as the wrong repository, and hand you Repository not found — while the correct key sits unused two lines down in the same directory. IdentitiesOnly yes restricts the alias to exactly the key you named, which turns an intermittent, order-dependent failure into a deterministic one.

The alias URL is written into .git/config when you clone, so subsequent git pull runs need no special treatment.

Make the first key explicit too

The remaining weakness is that the original project still depends on being whichever key happens to be the default. That is a fact about your filesystem, not a decision, and it will not survive contact with a third project.

Give it a name as well:

Host github-conduit
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes

Then repoint the existing clone:

cd /opt/conduit && git remote set-url origin git@github-conduit:owner/conduit.git

Now each repository names its own key, nothing relies on a default, and adding a fourth project is the same three steps as the second. Once that list gets long enough to annoy you, that is the point to move to a machine user.

Cloning without touching ~/.ssh/config

For a one-off, point Git at a key directly:

GIT_SSH_COMMAND="ssh -i ~/.ssh/myapp_deploy -o IdentitiesOnly=yes" git clone git@github.com:owner/repo.git

Useful in a script or a Dockerfile. If you want that key to keep being used for pull and push in the resulting clone, persist it in the repository’s own config:

git config core.sshCommand "ssh -i ~/.ssh/myapp_deploy -o IdentitiesOnly=yes"

Cloning as a different user

Deploys usually run as a dedicated unprivileged user, and the key has to belong to that user — SSH looks in the home directory of whoever is running the command, so a key in /root/.ssh is invisible to deploy.

sudo -u deploy git clone git@github-myapp:owner/repo.git /opt/myapp

If the target directory does not exist yet, create it and hand it over first:

sudo mkdir -p /opt/myapp && sudo chown deploy:deploy /opt/myapp

Running the clone as root and fixing ownership afterwards works too, but it leaves a .git directory that root wrote and the deploy user then has to be able to update. Cloning as the right user from the start avoids a class of permission problems later.

Troubleshooting

Repository not found

This is the confusing one, and on private repositories it is almost never about the repository existing.

ERROR: Repository not found.
fatal: Could not read from remote repository.

GitHub deliberately does not distinguish between “this does not exist” and “this exists but you cannot see it” — telling you which would leak the existence of private repositories to anyone guessing names. So an authentication problem surfaces as a 404.

In practice it means one of:

  • The key is not attached to an account with access. Check who GitHub thinks you are:

    ssh -T git@github.com
  • You used the wrong host alias, so SSH offered a key scoped to a different repository.

  • A deploy key belonging to another repository — very common on a server that already deploys something else. If ssh -T greets you as Hi owner/some-other-repo!, that is your answer; see adding a second repository to a server that already has one.

  • The owner or repository name is wrong. Case matters in the path.

  • An organisation with SSO. The key has to be authorised for the org separately — Settings → SSH keys → Configure SSO → Authorize. Until you do, access silently behaves as if you have none.

Permission denied (publickey)

The key is not being offered, or not accepted. Ask SSH what it tried:

ssh -vT git@github.com

Read the Offering public key: lines. If yours is not among them, the agent does not have it:

ssh-add -l

Could not open a connection to your authentication agent means the agent is not running:

eval "$(ssh-agent -s)"

Too many authentication failures

SSH is walking through every key it can find and GitHub disconnects after five attempts. Add IdentitiesOnly yes to the relevant Host block so it offers only the one you named.

Host key verification failed

The host key is unknown or changed. Reset and reconnect:

ssh-keygen -R github.com
ssh -T git@github.com

Compare the fingerprint against GitHub’s published list before accepting it.

Port 22 is blocked

Common on corporate and hotel networks. GitHub also serves SSH on 443:

Host github.com
    HostName ssh.github.com
    Port 443
    User git
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes

Bad owner or permissions on ~/.ssh/config

SSH ignores configuration files that others can write:

chmod 600 ~/.ssh/config

Switching an existing clone from HTTPS to SSH

If you cloned over HTTPS and are tired of the token prompts, no need to re-clone:

git remote -v
git remote set-url origin git@github.com:owner/repo.git
git remote -v

Everything else — branches, history, stashes — is untouched.

Cheat sheet

# personal machine
ssh-keygen -t ed25519 -C "you@example.com"
cat ~/.ssh/id_ed25519.pub                    # -> GitHub, Settings, SSH keys
ssh -T git@github.com
git clone git@github.com:owner/repo.git

# server, deploy key
ssh-keygen -t ed25519 -C "deploy@server" -f ~/.ssh/app_deploy -N ""
cat ~/.ssh/app_deploy.pub                    # -> repo, Settings, Deploy keys
# ~/.ssh/config:  Host github-app / IdentityFile ~/.ssh/app_deploy / IdentitiesOnly yes
ssh -T git@github-app
git clone git@github-app:owner/repo.git /opt/app

Comments

Corrections, additions and "this broke on my machine" reports are all welcome. You can post anonymously — no account needed.