DEV Community

Cover image for "Stop swapping SSH keys for two GitLab accounts. Use host aliases"
Ajay Vishwakarma
Ajay Vishwakarma

Posted on

"Stop swapping SSH keys for two GitLab accounts. Use host aliases"

Two GitLab accounts on one laptop, and git push to the work repo fails with Permission denied (publickey), or authenticates as the wrong account.

The usual fix is swapping keys by hand: rename ~/.ssh/id_rsa, or re-add the other key before every push. It works until you forget.

The rule: the repository's remote URL should select the SSH identity. One host alias per account, one key per alias, nothing to switch by hand.

Scope: gitlab.com, one work and one personal account, Windows and macOS. Not CI keys or key rotation.

Why does Git use the wrong GitLab account?

Both accounts live on the same server, so both remotes look the same to SSH:

git@gitlab.com:company/project.git
git@gitlab.com:username/project.git
Enter fullscreen mode Exit fullscreen mode

Nothing in either URL says which account you mean. SSH connects to gitlab.com as user git both times. If your agent holds both keys, SSH can offer more than one, and GitLab authenticates whichever it accepts first.

The model behind key swapping is "SSH has one identity, and I change it before I push." That turns the active account into hidden global state. Nothing in the repository records which account it belongs to, so every push depends on what you did last.

What should choose the key instead?

Treat it as routing, not switching. The repository names an alias, the alias names a key, and the key belongs to one account.


git pull
  ↓
origin = git@gitlab-work:company/project.git
  ↓
SSH config: Host gitlab-work
  ↓
IdentityFile ~/.ssh/id_gitlab_work
  ↓
local proof made with the private key
  ↓
GitLab verifies it with the public key on the work account
Enter fullscreen mode Exit fullscreen mode

gitlab-work is not a DNS hostname. It is a name in your SSH config that maps to gitlab.com plus one specific key.

Two facts keep this model honest:

  • The private key never leaves your machine. SSH uses it locally to produce a proof (a signature). GitLab checks that proof with the public key you registered.
  • ssh-agent is optional. It keeps an unlocked key available locally so you don't retype the passphrase. It does not send the key anywhere.

How do you set up one key and one alias per account?

1. Generate two keys and load them. Give each a passphrase. The -C comment is only a label; it does not decide which account gets the key.

Windows (PowerShell; the two service lines need an elevated session):

New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.ssh"
ssh-keygen -t ed25519 -C "work@example.com" -f "$env:USERPROFILE\.ssh\id_gitlab_work"
ssh-keygen -t ed25519 -C "personal@example.com" -f "$env:USERPROFILE\.ssh\id_gitlab_personal"

Get-Service ssh-agent | Set-Service -StartupType Automatic
Start-Service ssh-agent

ssh-add "$env:USERPROFILE\.ssh\id_gitlab_work"
ssh-add "$env:USERPROFILE\.ssh\id_gitlab_personal"
ssh-add -l
Enter fullscreen mode Exit fullscreen mode

macOS:

ssh-keygen -t ed25519 -C "work@example.com" -f ~/.ssh/id_gitlab_work
ssh-keygen -t ed25519 -C "personal@example.com" -f ~/.ssh/id_gitlab_personal

ssh-add --apple-use-keychain ~/.ssh/id_gitlab_work
ssh-add --apple-use-keychain ~/.ssh/id_gitlab_personal
ssh-add -l
Enter fullscreen mode Exit fullscreen mode

A running agent and a loaded key are different states. Trust ssh-add -l, not the service status.

2. Register each public key with the matching account. id_gitlab_work.pub goes into the work account's SSH key settings, id_gitlab_personal.pub into the personal one. Never the file without .pub. (GitLab SSH docs)

3. Write ~/.ssh/config.

Host gitlab-work
    HostName gitlab.com
    User git
    IdentityFile ~/.ssh/id_gitlab_work
    IdentitiesOnly yes

Host gitlab-personal
    HostName gitlab.com
    User git
    IdentityFile ~/.ssh/id_gitlab_personal
    IdentitiesOnly yes
Enter fullscreen mode Exit fullscreen mode
  • Host is the alias you will put in the remote URL.
  • HostName is the real server.
  • User git is the SSH user GitLab expects.
  • IdentityFile is the private key for this alias.
  • IdentitiesOnly yes tells SSH to use the identity configured here instead of offering every key the agent holds. It does not turn the agent off.

On macOS, add AddKeysToAgent yes and UseKeychain yes under each Host for Keychain integration. Those two lines are macOS-only.

4. Point each repository at its alias.

git remote set-url origin git@gitlab-work:company/project.git
Enter fullscreen mode Exit fullscreen mode

Personal repositories use git@gitlab-personal:username/project.git. From here on, the remote is part of the authentication design, not just metadata.

How do you prove each layer before the first push?

Test SSH directly before you debug Git. If ssh -T fails, Git is not the problem yet.

Command Expect What it proves
ssh -G gitlab-work hostname gitlab.com, user git, your work identityfile, identitiesonly yes SSH reads the alias the way you meant
ssh -T git@gitlab-work GitLab's welcome message for your work username The work key authenticates as the work account
ssh -T git@gitlab-personal The welcome message for your personal username The personal key authenticates as the personal account
git remote -v git@gitlab-work:... for fetch and push This repository routes through the right alias
git fetch No error, no prompt for the other key The whole chain works through Git

On the first connection, check the host key fingerprint against GitLab's published fingerprints before you type yes.

Why can ssh -T pass while Git still prompts or fails?

This one cost me the most time, on Windows.

My machine had MSYS2's OpenSSH installed next to Windows OpenSSH. Both keys were loaded in the Windows agent, and I had already cleaned up PATH. Git still kept asking for the key passphrase.

The lesson: the ssh your terminal resolves is not proof of which SSH command Git runs. Check both:

Get-Command ssh
where.exe ssh
git config --show-origin --get core.sshCommand
$env:GIT_SSH_COMMAND
Enter fullscreen mode Exit fullscreen mode

On macOS the first two are command -v ssh and which -a ssh. If GIT_SSH_COMMAND is set, it overrides core.sshCommand (git-config).

What fixed it on that machine was telling Git exactly which client to launch:

git config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe"
Enter fullscreen mode Exit fullscreen mode

After that, Git used the same OpenSSH as the Windows agent, and the prompts stopped. This setting does not change the protocol, move keys, or replace your SSH config. It only makes Git's choice of SSH executable explicit.

That is one machine, not a rule. If your default setup already works, leave it alone.

Where else does this break?

  • An old remote. A repository still on git@gitlab.com:... bypasses both aliases. Run git remote -v first.
  • The IDE terminal. An IDE can inherit a different PATH and different agent access than your system terminal. Run the same checks inside it before blaming the config.
  • Commit author. The SSH identity decides which account authenticates. user.name and user.email decide what gets written into commits. Fixing one does nothing for the other.
  • macOS. I ran the Windows path on my own machine. The macOS steps are checked against current documentation, not run by me on a Mac. If --apple-use-keychain behaves differently on your version, read ssh-add's own help.
  • Shortcuts that make it worse. StrictHostKeyChecking=no and a passphrase stored in .env both make the prompt go away by removing the protection.

What should you check in the next 20 minutes?

  1. Run git remote -v in one work repo and one personal repo. Is anything still on git@gitlab.com:?
  2. Run ssh -G gitlab-work. Do identityfile and identitiesonly say what you intended?
  3. Run ssh -T against each alias. Does each one greet the right username?
  4. Run git config --show-origin --get core.sshCommand and check GIT_SSH_COMMAND. Do you know which ssh Git launches?
  5. Run git config user.email in each repo. Does the commit identity match the account?

Next: debugging Permission denied (publickey) one layer at a time.

Has Git ever run a different ssh than your terminal did? What was installed, and how did you catch it?

Top comments (0)