Skip to content
shellhacks.dev
Git

How to Set Up SSH Keys for GitHub and GitLab the Safe Way

Set up SSH keys for GitHub and GitLab: generate an Ed25519 key, add it to the agent, upload the public key, test the connection, and use one key per host.

6 min read
Summarize in AI
Illustration for "How to Set Up SSH Keys for GitHub and GitLab the Safe Way": a terminal session, a padlock and key, a git branch graph.

An SSH key replaces the username and token on every push and pull, and it cannot be phished with a fake login page. The setup is the same for GitHub, GitLab, Bitbucket and self-hosted servers. The commands use OpenSSH 9.7 and Git 2.45; the key choice is explained in Ed25519 versus RSA versus ECDSA.

Generate a key

Shell
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/id_ed25519_github
Text
Enter passphrase (empty for no passphrase):
Your identification has been saved in /home/alice/.ssh/id_ed25519_github
Your public key has been saved in /home/alice/.ssh/id_ed25519_github.pub

Ed25519 keys are short, fast and supported by all current hosts. Choose a passphrase, because it protects the key if the laptop or a backup is stolen. The comment after -C is only a label. OpenSSH writes the private key with mode 600 and refuses to use one that others can read, covered in SSH bad owner or permissions.

Add it to the agent

Shell
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519_github
ssh-add -l

The agent holds the decrypted key in memory so you type the passphrase once. On macOS, add UseKeychain yes and AddKeysToAgent yes to your config to store it in the Keychain, and on Linux desktops the session usually starts an agent already. A message such as "agent refused operation" or "admitted failure to sign" points to an agent problem, see SSH agent admitted failure to sign.

Upload the public key and test

Shell
cat ~/.ssh/id_ed25519_github.pub
ssh -T [email protected]
Text
Hi alice! You've successfully authenticated, but GitHub does not provide shell access.

Paste the single line from the .pub file into Settings, SSH and GPG keys, on GitHub, or Preferences, SSH Keys, on GitLab. Never upload the file without .pub. On the first connection, SSH asks you to confirm the host fingerprint; compare it with the value the host publishes, such as those on GitHub's SSH key fingerprints page, before typing yes. A "Permission denied (publickey)" reply means the host did not accept any key offered, covered in git permission denied publickey.

Several accounts or hosts

Text
# ~/.ssh/config
Host github-work
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519_work
  IdentitiesOnly yes

Host github.com
  IdentityFile ~/.ssh/id_ed25519_github
  IdentitiesOnly yes

IdentitiesOnly yes stops SSH from offering every key in the agent, which otherwise can exhaust the server's attempt limit. Clone work repositories with git@github-work:org/repo.git, where the alias selects the key. More patterns are in multiple SSH accounts for Git, and the file's syntax in SSH config shortcuts.

Switch an existing clone

Shell
git remote set-url origin [email protected]:alice/project.git
git remote -v

What the key proves, and what it does not

The private key never leaves your machine. When SSH connects, the server sends a challenge, the client signs it with the private key, and the server checks the signature against the public key you uploaded. Nothing secret crosses the network, and there is nothing to type or to leak in a URL. Compare that with an HTTPS token: it is a bearer secret, whoever holds the string can use it, and it ends up in shell history, CI logs and credential helpers. A key protected by a passphrase and loaded in an agent is harder to steal and easier to revoke, because deleting the public key from the account ends its access at once.

What a key does not prove is who is using the machine. Anyone with access to a logged-in session that has the agent running can use the loaded key without knowing the passphrase. Lock the screen, set a timeout on keys with ssh-add -t 3600, and avoid forwarding the agent to hosts you do not control, since a root user there can ask your agent to sign. Using ProxyJump instead of agent forwarding for bastion hosts avoids that exposure.

Hardware keys and signing

OpenSSH supports keys held on a security device, with types ed25519-sk and ecdsa-sk. Creating one with ssh-keygen -t ed25519-sk stores a handle on disk and the private part on the device, and each use requires a touch. A stolen laptop then gives an attacker nothing without the physical token. It is the strongest option for accounts that can push to important repositories, and the cost is one more thing to carry and a backup token to register.

Git can also sign commits with an SSH key since version 2.34. Set gpg.format ssh, user.signingkey to the public key path and commit.gpgsign true, and GitHub shows a verified badge once the same key is added as a signing key. Authentication and signing are separate uploads in GitHub's settings, which surprises people who expect one key entry to cover both.

Troubleshooting a failing connection

When Permission denied (publickey) appears, run ssh -vT [email protected] and read which keys were offered and what the server answered. The common causes are few. The agent holds no key because the shell was started before it was loaded. The wrong key was offered because the config does not name one. The permissions on the private key file or the .ssh directory are too open. Or the public key was added to a different account than the one named in the remote URL. Each shows up plainly in the verbose output. Another check is ssh-keygen -lf ~/.ssh/id_ed25519_github.pub, whose fingerprint can be compared with the one in the account settings to be sure that the uploaded key is the one in use.

Housekeeping

Keep one key per device, named for the device, so a lost laptop means removing one entry. Review the list of keys in the account every few months and delete the ones nobody recognises. Rotate when a key is old, a device is retired or someone leaves; rotation is generate, upload, test, then delete the old public key. Back up the private key only in encrypted form, or better, generate a new one for a new machine and skip the copy entirely. For organisation access, many hosts support SSH certificates signed by an internal authority, which give keys an expiry and remove the need to upload each one by hand.

Using the key from scripts and CI

Automation needs a key no person types a passphrase for, and that raises the stakes of scoping it. Create a key used by one job only, add it to the repository as a deploy key with read-only access unless the job must push, and store the private half in the CI system's secret store. Load it into an agent at job start with ssh-add reading from standard input, and never write it to a persistent location on a shared runner. Pin the host's key in known_hosts from a trusted copy, since disabling host key checking removes the protection against a man in the middle exactly where the key is most valuable. ssh-keyscan output should be compared with the fingerprints the provider publishes before it is stored.

When the job ends, the secret should be unusable: short-lived runners that are destroyed after each job are best, and for others, remove the key from the agent and the filesystem in a final step that runs even on failure.

Where this does not apply

Corporate networks sometimes block port 22; GitHub accepts SSH on port 443 at ssh.github.com, set with Port 443 and HostName ssh.github.com. In CI, use a deploy key limited to one repository rather than a personal key. A key without a passphrase is acceptable only for unattended automation with narrow permissions. The commands are described in the GitHub SSH documentation and ssh-keygen(1).

Taggedgitssh
Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.