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.

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
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/id_ed25519_githubEnter 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
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519_github
ssh-add -lThe 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
cat ~/.ssh/id_ed25519_github.pub
ssh -T [email protected]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
# ~/.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
git remote set-url origin [email protected]:alice/project.git
git remote -vWhat 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).


