Permission denied (publickey) for git@github.com on a Mac: the server refused every offered key
Frank Zhang
- 9 minutes read - 4044 wordsTL;DR
The refusal is at user authentication. See which key the client offered, compare its SHA256 fingerprint with the GitHub account, and only then change the agent, the keychain, or the key.
Match this line before you generate a key or edit ~/.ssh. It shows up in Terminal on a Mac, including a MacBook, and in VS Code or any other program on that Mac that runs git over SSH:
git@github.com: Permission denied (publickey).
The TCP connection was accepted, and the host key check already passed. The server then rejected every user key the client offered. GitHub’s page for this sentence is Error: Permission denied (publickey). Their summary is that the server rejected the connection. In the verbose sample on that page, the method that can continue is publickey.
These commands were not re-run on a Mac or a VPS for this page. Flag meanings come from the manuals. The debug strings come from OpenSSH’s ssh.c, sshconnect2.c, ssh-add.c, and auth.c, as read for this page. Those links are the master tree at that reading. macOS ships its own build of OpenSSH. If man on the machine in front of you disagrees, that copy wins. If GitHub edits a quoted line, the doc page wins.
Thirty seconds: three sentences, three layers
| Text on screen | Layer | First read-only command |
|---|---|---|
ssh: connect to host … port 22: Connection refused | TCP. The account and the keys have not been tried. | The order is ssh: connect to host port 22: Connection refused, from a Mac or into a Mac. |
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED, or Host key verification failed | Host key. The key the server just sent does not match known_hosts for that host. | Compare the fingerprint, then remove only that entry: WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED on Mac: check before you delete known_hosts. The known_hosts fix command generator fills in the command. |
Permission denied (publickey) | User authentication. The server rejected the public keys the client offered. | ssh -vT git@github.com |
The third sentence means the first two layers already succeeded. A Connection refused session is the wrong time to rotate a user key. A host-key warning is the wrong time to delete a public key from GitHub’s SSH keys page.
Read the remote and the user before you change a key
Nothing in this section generates a key, writes ~/.ssh/config, or pastes a public key into GitHub.
The URL git is actually using
git remote -v
Each remote prints a fetch line and a push line. An SSH URL looks like git@github.com:OWNER/REPO.git or ssh://git@github.com/OWNER/REPO.git. An https://github.com/OWNER/REPO.git URL is HTTPS, and it does not produce git@github.com: Permission denied (publickey). Once that sentence is on screen, the remote you just used is the SSH one. If fetch and push differ, read the line for the command you ran.
GitLab.com has the same shape: git@gitlab.com:GROUP/PROJECT.git. Swap the hostname to gitlab.com in the GitHub checks below. A self-managed GitLab hostname is whatever that instance uses.
The user is git
The same GitHub page says every connection, including remote URLs, has to be made as the git user. Connecting with your GitHub username fails. Their example is:
ssh -T GITHUB-USERNAME@github.com
Permission denied (publickey).
The check is:
ssh -T git@github.com
ssh(1) -T disables pseudo-terminal allocation. GitHub uses it because the server does not offer a shell. Testing your SSH connection prints this on success:
Hi USERNAME! You've successfully authenticated, but GitHub does not
provide shell access.
USERNAME is a placeholder for your GitHub login. The troubleshooting page shortens the same sentence to Hi USERNAME! You've successfully authenticated.... The testing page also says the remote command should exit with status 1. Success is the Hi USERNAME! line. Exit status 1 is the result GitHub documents for that command.
GitLab’s check is ssh -T git@gitlab.com. Use SSH keys with GitLab shows the welcome line as Welcome to GitLab, <username>!. The name in angle brackets is the GitLab account. The SSH user is still git. That page says a self-managed administrator can change the SSH username. GitLab.com, as documented there, uses git. This page did not inventory self-managed instances.
If the first connection instead asks The authenticity of host 'github.com' can't be established, you are still on the host-key layer. Compare the fingerprint with the page GitHub publishes, using the host-key article linked above. After that prompt has been accepted, the next result is either the Hi USERNAME! line or Permission denied (publickey).
Git stays on your user, without sudo
The troubleshooting page asks whether sudo or other elevated privileges should be used with Git, and answers that they should not. The reason they give: keys generated without sudo, followed by sudo git push, are not the keys that command will use. sudo runs the command as root, and root has a different ~/.ssh. sudo ssh -T git@github.com is the same split. The read-only commands in this section do not use sudo.
What the config rewrites, without opening a connection, is ssh(1) -G, the same switch as the earlier notes in this series:
ssh -G github.com | grep -iE '^(user|hostname|identityfile|identitiesonly) '
Read which identityfile paths are configured, and whether identitiesonly is yes. A URL written as git@github.com:… sends the user that is in the URL, git.
How to read ssh -vT
GitHub’s troubleshooting page uses this to see whether a key is offered. When the private key file is not the default name, the same page adds -i:
ssh -vT git@github.com
ssh -i ~/.ssh/KEY -vT git@github.com
Replace KEY with the private key’s filename, without .pub. -i selects the identity for that one command. Hi USERNAME! with -i, and Permission denied (publickey) without it, means that key works and the default selection did not use it. IdentityFile in the config section is how you select it for later commands.
Read these four kinds of lines. The integer after type belongs to the ssh you are running.
| Line in the verbose log | Where it comes from, as read for this page | How to read it |
|---|---|---|
identity file … type -1 | load_public_identity_files in ssh.c: debug("identity file %s type %d", filename, public ? public->type : -1). The value -1 is what it prints when no public key was loaded. | On GitHub’s published sample, a trailing -1 means SSH could not find a file to use. type -1 on a default name you never created, such as id_rsa or id_dsa, describes that path only. A later Offering public key line means a different file did load. |
Offering public key: … | sshconnect2.c prints Offering public key: when id->key != NULL. | A public key was sent. If the session still ends in Permission denied (publickey), the server rejected a key that was offered. |
Trying private key: … | The other branch in that file prints Trying private key: when there is no public key yet, then reads the private key file. | GitHub’s page reads this line, in the sample where every identity is type -1 and authentication then ends, as “no file was found.” In the source, the line means there was no public key loaded, so the client opened the private key file. A passphrase prompt after it means the file exists and has a passphrase. No Offering public key before the next row means this key was not sent. |
No more authentication methods to try. | The same file prints this when the remaining server methods do not match. | The next line is Permission denied (publickey). GitHub’s sample shows the two together. |
In GitHub’s sample, a loaded RSA file prints identity file … type 1 and then Offering RSA public key. That 1 is the type number in their RSA example. A loaded Ed25519 key prints the type number your OpenSSH build uses for Ed25519, which does not have to be 1. Read -1 against a non-negative type, and read whether the later line is Offering public key or Trying private key.
When ssh -i already succeeds, the account already has that key, and git still fails, the failure is in the ssh that git invoked. The GIT_SSH_COMMAND form is in the VS Code section below.
The fingerprint on the GitHub account
The troubleshooting page lists the agent’s keys and tells you to compare them with the keys on the website:
ssh-add -l -E sha256
ssh-add(1) -l lists fingerprints. -E sha256 selects the hash. The ssh-add.c read for this page prints the following when the list is empty:
The agent has no identities.
The agent is reachable and holds no keys. The same file prints Could not open a connection to your authentication agent. when it cannot connect to an agent. That second sentence means this process has no agent. A Mac app launched from the Dock and a shell inside Terminal can land on different sides of that split. General guidance: this page did not measure the environment of a particular VS Code version.
With no identities in the agent, the public key file on disk still has a fingerprint. ssh-keygen(1) -l prints it, -f selects the file, and -E selects the hash:
ssh-keygen -lf ~/.ssh/id_ed25519.pub -E sha256
Use the .pub file you actually have. A missing file is the same fact as type -1 for that path in the verbose log. Compare the SHA256: string with GitHub. The troubleshooting page’s path is: profile photo, Settings, then SSH and GPG keys under Access. The list needs the same fingerprint. If it is absent, add the contents of that .pub file to the account. The form is Adding a new SSH key to your GitHub account. Paste the .pub file.
On GitLab.com the list is avatar, Edit profile, Access, SSH keys. The GitLab page says uploading a public key does not reveal the private key, and tells you to paste the public key rather than the private key file.
A fingerprint that matches, plus an Offering public key line, followed by Permission denied (publickey), means the key was sent and not accepted. An organization with SAML single sign-on requires the key to be authorized, in Authorizing an SSH key for use with single sign-on: Configure SSO next to that key on the SSH and GPG keys page, then Authorize for the organization. Deploy keys are a separate setup. This page does not walk through them.
The warning on the troubleshooting page says: if you see an SSH key you do not recognize on GitHub, delete it immediately and contact GitHub through the Support portal. An unidentified public key may be a security concern. That paragraph points at Reviewing your SSH keys.
Keychain and config, after the read agrees
The steps below write ~/.ssh/config, generate a key, or load a private key into the agent. Do them when the read-only output says the key was not selected, the agent has no identities, or the account does not list that fingerprint.
Use the ssh-add that ships with macOS
Generating a new SSH key and adding it to the ssh-agent says to add the key with the default macOS ssh-add, not one installed by MacPorts, Homebrew, or another external source. See which binary is first on PATH:
which -a ssh ssh-add
The lines you want are /usr/bin/ssh and /usr/bin/ssh-add. When /opt/homebrew/bin or /opt/local/bin comes first, call /usr/bin/ssh-add by its path. The other binary does not implement --apple-use-keychain. GitHub documents that failure as Error: ssh-add: illegal option – apple-use-keychain.
The config block that page gives for macOS Sierra 10.12.2 or later is:
Host github.com
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_ed25519
Point IdentityFile at your private key. If ~/.ssh/config is missing, their example creates it with touch ~/.ssh/config. The three lines:
AddKeysToAgent yes, in ssh_config(5), adds a key to a running ssh-agent when the key is loaded from a file.UseKeychain yesis Apple’s option, as written on that GitHub page: store the passphrase in the keychain. The same page says to omitUseKeychainwhen the key has no passphrase.IdentityFileis the identity to read. The manual’s default list includes~/.ssh/id_ed25519and several names you may never have generated.
Add the private key to the agent and store the passphrase in the keychain. For macOS Monterey (12.0) and later, GitHub’s command is:
ssh-add --apple-use-keychain ~/.ssh/id_ed25519
The same page says to drop --apple-use-keychain when the key has no passphrase. Before Monterey, that flag was -K. Their sentence: prior to 12.0, --apple-use-keychain and --apple-load-keychain were -K and -A.
On Bad configuration option: usekeychain, GitHub’s fix is an IgnoreUnknown UseKeychain line in the Host github.com block. ssh_config(5) says IgnoreUnknown suppresses options ssh does not recognize, and that it should be listed early because it does not apply to unknown options that appear before it. Put it above UseKeychain:
Host github.com
IgnoreUnknown UseKeychain
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_ed25519
That error is also a sign the ssh reading the file may not be /usr/bin/ssh. Go back to which -a.
readconf.c calls fatal("Bad owner or permissions on %s", filename) when the config file is owned by neither root nor you, or when the mode has group or other write (mode & 022). chmod 777 on ~/.ssh/config hits that check. chmod 777 ~/.ssh is the pattern the safety section tells you to skip; on a server it is the world-writable case StrictModes is about.
GitHub’s troubleshooting page also shows eval "$(ssh-agent -s)", and notes that the command may fail when an agent is already a system service. On a Mac, run ssh-add -l first. A fingerprint list, or The agent has no identities., means an agent is already there. ssh-add --apple-use-keychain then adds the key to that agent. Starting another agent attaches it to the shell that ran the command. That last sentence is general guidance.
Then run ssh -T git@github.com again. After the Hi USERNAME! line, run the original git fetch or git push in the repository.
A new key, without overwriting one you still use
Generate a key when the account has no matching fingerprint and the disk has no private key you still intend to keep. The command on GitHub’s generating page is:
ssh-keygen -t ed25519 -C "your_email@example.com"
The email after -C is a comment stored at the end of the public key, so you can tell keys apart. It is not the SSH username. The username stays git.
At Enter a file in which to save the key, an existing file makes ssh-keygen ask before it overwrites. GitHub’s sentence: if you created keys before, ssh-keygen may ask to rewrite another key, and they recommend a custom filename instead. Overwriting replaces the private key those logins depend on. A custom filename has to be the IdentityFile above. Type a passphrase when that page prompts for one.
The same page says GitHub dropped older key types on 15 March 2022. From that date, DSA keys (ssh-dss) are not supported, and a new DSA key cannot be added to a personal account. The changelog is Removed unencrypted Git protocol and certain SSH keys (15 March 2022): support for DSA keys was removed. If the file you still have is id_dsa, generate an ed25519 key with the command above and add only the new .pub to the account. Leave the old private key file where it is until you know no other host still uses it.
GitLab’s page lists ED25519 as the preferred type and marks DSA deprecated. Their generate command is the same shape: ssh-keygen -t ed25519 -C "<comment>".
More than one key, and programs that are not Terminal
Two GitHub accounts, or one key for GitHub and another for GitLab, need a Host alias that pins IdentityFile. ssh_config(5) describes IdentitiesOnly yes as the setting for when ssh-agent offers many identities: ssh uses only the identity files from configuration or the command line. The default is no.
Host github.com-work
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
AddKeysToAgent yes
UseKeychain yes
The remote URL becomes git@github.com-work:ORG/REPO.git. github.com-work is a client alias. HostName is the real host, github.com. User git keeps the GitHub username rule on that alias. A Host gitlab.com block is the same shape with GitLab’s IdentityFile. Omit UseKeychain when the key has no passphrase.
A crowded agent can make the server disconnect after several tries. In the auth.c read for this page, auth_maxtries_exceeded disconnects with the reason Too many authentication failures. sshd_config(5) defaults MaxAuthTries to 6. That is general OpenSSH server behavior. This page did not read the sshd_config on github.com or gitlab.com, and does not know the number they set. The client-side response the manual already names is IdentitiesOnly yes plus the one IdentityFile, so the verbose log offers the key you named.
VS Code, GUI apps, and AI agents
General guidance. A program opened from the Dock or Launchpad does not receive the environment you exported in Terminal. SSH_AUTH_SOCK and the ssh on PATH can both differ. Run ssh -T git@github.com and git fetch in Terminal.app first. Terminal succeeding while VS Code or an agent still prints Permission denied (publickey) is a difference in the ssh and the agent that process uses.
To see the ssh one git invocation actually runs, use GIT_SSH_COMMAND, documented in git:
GIT_SSH_COMMAND="ssh -v" git fetch
It applies to that command only, and it attaches verbose output to that fetch. Read the table above. Another note on this site is about a process that cannot see a command the interactive shell can run: 明明全局安装了 agent-browser,为什么 AI 代理还是说“找不到”?. That one is PATH. This one is keys and the agent. Both are the process that runs git, rather than the interactive shell in front of you.
Your own server, briefly
GitHub and GitLab.com use the SSH user git (a self-managed GitLab may not; see above). On a machine you administer, the user is an account on that machine. Check it from the provider console, or from a session you already have, because the SSH session that is failing cannot read the far side.
The client’s Permission denied (publickey) does not say whether the username exists. On the console, confirm three things: the username is the account you mean to log into; ~/.ssh/authorized_keys exists under that home directory; the directory and the file are owned by that user.
The modes are general guidance, tied to StrictModes in sshd_config(5). The manual says the default is yes: before accepting login, sshd checks modes and ownership of the user’s files and home directory, because people sometimes leave them world-writable. The modes people set so that check can pass are 700 for ~/.ssh and 600 for authorized_keys. That paragraph does not list those two octal values as the only legal ones. chmod 777 points the wrong way, and it is in the next section.
Print the effective config without reloading:
sudo sshd -T | grep -E '^(pubkeyauthentication|authorizedkeysfile|maxauthtries) '
The manual default for PubkeyAuthentication is yes. An effective value of no means public-key authentication is off on this sshd. The manual default for AuthorizedKeysFile is .ssh/authorized_keys .ssh/authorized_keys2, relative to the home directory. A file you edited that is not in that line is a file sshd is not reading.
Read the logs you already have. Use the unit name established in the Connection refused note: ssh is the usual name on Debian and Ubuntu, sshd on the Red Hat family.
sudo journalctl -u ssh -n 50 --no-pager
sudo journalctl -u sshd -n 50 --no-pager
Run the unit this machine has. On Debian-family systems the same records often also land in /var/log/auth.log. The MaxAuthTries paragraph says further failures are logged once the failure count reaches half that value. At the default log level, the earlier public-key failures can have no line. An empty stretch of log is not evidence that the key was accepted. This page does not include a LogLevel change.
When you still have a login that works, such as a password, ssh-copy-id(1) installs the public key:
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host
It writes the public key into authorized_keys through a connection that can already log in. If the only SSH attempt is the one that prints Permission denied (publickey), this command uses that same failing path. The place to edit is the provider console: append one full line from the .pub file, one public key per line.
Whether a new box should expose port 22 at all is a deployment decision, covered in 10 things to confirm before deploying a GitHub project. That article does not clear this error.
What to leave alone
The private key stays on the Mac. The file without .pub is the private key. Do not paste it into a ticket, a chat, or an AI agent’s input. When the other side asked for the public key, send the contents of the .pub file. The troubleshooting page’s instruction for an SSH key on the GitHub account that you do not recognize is to delete it and contact GitHub Support, as quoted above.
Do not chmod 777 ~/.ssh, config, the private key, or authorized_keys. A ~/.ssh/config that is group- or world-writable stops the client with Bad owner or permissions, from the readconf.c check above. On a server, StrictModes yes (the manual’s default) checks modes and ownership of the home directory and the user’s files before login. World-writable is the case that paragraph names. 700 and 600 stay the general guidance from the previous section, not the only octal values the manual lists.
Do not delete the ~/.ssh directory. It holds the private keys, config, and known_hosts together. known_hosts is stored per host, and removing the directory drops entries that have nothing to do with this error. The host-key layer is the host key article and the known_hosts fix command generator.
Where this page stops
- The commands were not re-run on a Mac or a VPS. The manuals, the OpenSSH source linked above, and the GitHub and GitLab docs are the sources. The
manon the machine wins. - This page does not pin a macOS release or Apple’s OpenSSH patch level.
which -ashows which binary this shell will run. - The Windows OpenSSH client and Git Bash have their own steps on GitHub’s two pages. Those steps are not this page.
- GitHub Enterprise hostnames are outside this page. The testing page says to use your enterprise hostname and the username that instance expects. The commands here stay on
git@github.com. - Hardware security keys (
ed25519-sk) are a separate section of GitHub’s generating page. The command here is ordinaryed25519. Too many authentication failureslines up withMaxAuthTrieson an OpenSSH server you run. Whether github.com uses the manual’s default is not something this page read.- A corporate VPN, an HTTP proxy, or an MDM profile. If
ssh -Galready printedproxyjump, finish reading that line. - Search volume. This page does not add an external measurement.
A tunnel whose address and port kept changing is the Chinese incident 一次ssh排错经历. Other paths on the site start at Start Here. The Chinese writeup of this same topic is Mac 终端里 git@github.com: Permission denied (publickey):先读 ssh -vT 递出去的钥匙.
If this post helps, you can sponsor the blog. This is voluntary personal support, not a charitable donation.