Mac 终端里 git@github.com: Permission denied (publickey):先读 ssh -vT 递出去的钥匙
Frank Zhang
- 15 分钟阅读 - 7480 字TL;DR
连接和主机密钥这两层已经过了。先用只读命令看 remote、用户名 git,以及 verbose 里有没有 Offering public key;钥匙、Keychain 和配置放在后面。
要解决 Mac 上的 permission denied (publickey),先对上这一句。它出现在 Mac 终端、MacBook 上的 Terminal,或 VS Code、git 替你去连 GitHub 的时候:
git@github.com: Permission denied (publickey).
网络那一层和主机密钥那一层已经走完了。服务器拒绝的是客户端递出去的每一把用户钥匙。GitHub 把这句的含义写在 Error: Permission denied (publickey):服务器拒绝了这次连接。他们那份 verbose 例子里,还能继续的方法只有 publickey。
写这篇时没有在 Mac 或 VPS 上重跑这些命令。旗标含义来自手册,调试原文来自写作时读到的 OpenSSH ssh.c、sshconnect2.c、ssh-add.c 和 auth.c。链接指向当时的 master 树。Mac 自带的是它自己的那一份 OpenSSH。你机器上的 man 和这里不一致时,以你的手册为准。GitHub 文档再改文案时,以文档页为准。
三十秒:这三句停在三层
| 屏幕上的原文 | 停在哪一层 | 第一条只读命令 |
|---|---|---|
ssh: connect to host … port 22: Connection refused | TCP。账号和钥匙都还没开始。 | 顺序在 SSH 报 connect to host port 22: Connection refused:从 Mac 连出去,还是连进这台 Mac。 |
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED,或 Host key verification failed | 主机密钥。known_hosts 里这一台和服务器刚才递来的钥匙对不上。 | 先核对指纹,再只删这一条:Mac 上出现 WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED:先核对指纹,再删 known_hosts 里的那一条。命令可以用 known_hosts 修复命令生成器。 |
Permission denied (publickey) | 用户认证。服务器把客户端递出的公钥都拒绝了。 | ssh -vT git@github.com |
能看到第三句,就说明前两层已经过去。还停在 Connection refused 时,不去改 ~/.ssh 里的用户钥匙。还停在主机密钥警告时,不去删公钥,也不去 GitHub 的 SSH keys 页面上换一把。
只读:remote、用户名,以及 ssh -T
这一节不生成钥匙,不改 ~/.ssh/config,也不往 GitHub 上贴公钥。
git remote -v:git 这次用的是哪一个 URL
git remote -v
每个 remote 有 fetch 和 push 两行。SSH 的样子是 git@github.com:OWNER/REPO.git,或 ssh://git@github.com/OWNER/REPO.git。https://github.com/OWNER/REPO.git 走的是 HTTPS,不会打出 git@github.com: Permission denied (publickey)。你已经看到这句时,正在用的那个 remote 就是 SSH。fetch 和 push 的 URL 不一样时,读你刚跑的那一条命令对应的行。
GitLab.com 是同一个形状:git@gitlab.com:GROUP/PROJECT.git。下面 GitHub 的步骤换成主机名 gitlab.com 即可。自建 GitLab 的主机名以那台实例为准。
用户名是 git
GitHub 的同一页写明:所有连接,包括 remote URL,都必须用用户 git。用你的 GitHub 用户名去连,会失败,他们给的例子是:
ssh -T GITHUB-USERNAME@github.com
Permission denied (publickey).
核对用这一条:
ssh -T git@github.com
ssh(1) 的 -T 是不分配伪终端。GitHub 用它,是因为对面不提供 shell。Testing your SSH connection 写的成功原文是:
Hi USERNAME! You've successfully authenticated, but GitHub does not
provide shell access.
USERNAME 是占位,应换成你的 GitHub 登录名。排错页把同一句收成 Hi USERNAME! You've successfully authenticated...。测试页写明:这条远程命令的退出码是 1。成功看的是 Hi USERNAME! 这一句,退出码 1 是测试页写明的结果。
GitLab 的核对命令是 ssh -T git@gitlab.com。文档 Use SSH keys with GitLab 写的欢迎句是 Welcome to GitLab, <username>!。尖括号里是 GitLab 账号名。SSH 用户名仍然是 git。同一页写明,自建实例的管理员可以改掉这个 SSH 用户名;GitLab.com 按文档用 git。这篇没有逐台核对自建实例。
第一次连接如果先问 The authenticity of host 'github.com' can't be established,你还在主机密钥那一层。指纹去对 GitHub 公布的那一页,步骤在上面那篇 host key 文章里。对上并接受之后,才会进入这一篇的成功句或 Permission denied (publickey)。
不要用 sudo 跑 git
GitHub 排错页有一节问要不要用 sudo 或管理员权限跑 Git。正文的回答是不该用。他们写的原因是:钥匙不用 sudo 生成,接着跑 sudo git push,用到的就不是刚才那把钥匙。sudo 把命令跑成 root,读的是 root 的 ~/.ssh。sudo ssh -T git@github.com 是同一件事。这一节的只读命令都不加 sudo。
配置把主机名改写成什么,仍然可以只展开、不连接。系列里前两篇用的是 ssh(1) 的 -G:
ssh -G github.com | grep -iE '^(user|hostname|identityfile|identitiesonly) '
看 identityfile 指向哪几个文件,以及 identitiesonly 是不是 yes。你在命令行写了 git@github.com 时,送出去的用户名是 URL 里的 git。
ssh -vT 里这几行怎么读
GitHub 排错页用这一条看钥匙有没有被用上。钥匙文件名不是默认的 id_ed25519 时,同一页给出 -i:
ssh -vT git@github.com
ssh -i ~/.ssh/KEY -vT git@github.com
KEY 换成你的私钥文件名,不带 .pub。-i 指定这一次读哪把身份。带 -i 的那条出现 Hi USERNAME!,不带 -i 的那条仍是 Permission denied (publickey),说明这把钥匙能用,默认路径没有选中它。选中它是后面配置里的 IdentityFile。
先看这四类行。type 后面的整数以你这份 ssh 为准。
| verbose 里的一行 | 写作时读到的来源 | 怎么读 |
|---|---|---|
identity file … type -1 | ssh.c 的 load_public_identity_files:debug("identity file %s type %d", filename, public ? public->type : -1)。公钥没载入时打印 -1。 | GitHub 排错页对他们那份例子的读法是:行尾 -1 表示 SSH 没有找到可用的文件。默认名单里你从来没生成过的 id_rsa、id_dsa 打出 -1,只说明那一个路径没有公钥。后面若另有一行 Offering public key,那一把已经载入了。 |
Offering public key: … | sshconnect2.c:id->key != NULL 时打印 Offering public key:。 | 公钥已经递出去了。服务器若仍以 Permission denied (publickey) 结束,拒绝的是递出去的这一把,不是「客户端根本没有钥匙」。 |
Trying private key: … | 同一文件的另一支:还没有公钥时打印 Trying private key:,然后去读私钥文件。 | GitHub 排错页在「全部是 type -1、随后认证结束」的例子里,把这行读成没有找到文件。源码里它的含义更窄:当时没有公钥可递,客户端改去打开私钥。接着出现口令提示,就是这个文件存在,并设了 passphrase。没有 Offering public key、直接到下一行,就是这把没有递出去。 |
No more authentication methods to try. | 同一文件:服务器还接受的方法都对不上时打印这句。 | 下一句就是 Permission denied (publickey).。GitHub 的例子把这两行连在一起。 |
GitHub 例子里,RSA 文件成功载入时打印 identity file … type 1,然后是 Offering RSA public key。那个 1 是他们那份 RSA 例子里的类型号。Ed25519 载入之后,类型号是你这份 OpenSSH 里 Ed25519 的编号,不必等于 1。要看的是 -1 还是一个非负类型号,以及后面是 Offering public key 还是 Trying private key。
ssh -i 已经成功、账号上也有这把钥匙,而 git 仍然失败时,失败发生在 git 调用的那一个 ssh 上。看那一次调用的写法在后面「VS Code」一节,用 GIT_SSH_COMMAND。
指纹,和 GitHub 账号上的那一列
GitHub 排错页用这一条列出 agent 里的钥匙,并要求和网站上的列表对照:
ssh-add -l -E sha256
ssh-add(1) 的 -l 列出指纹,-E sha256 指定指纹算法。写作时读到的 ssh-add.c 在列表为空时打印:
The agent has no identities.
这是 agent 在,里面没有钥匙。同一文件在连不上 agent 时打印 Could not open a connection to your authentication agent.。后一句是这个进程没有连上 agent。Mac 上从 Dock 打开的程序,和 Terminal 里的 shell,可以一个连得上、一个连不上。这是一般判断:这篇没有对某一个 VS Code 版本测量过环境变量。
agent 里没有身份时,磁盘上的公钥仍然可以单独算指纹。ssh-keygen(1) 的 -l 打印公钥指纹,-f 指定文件,-E 指定算法:
ssh-keygen -lf ~/.ssh/id_ed25519.pub -E sha256
文件名换成你实际的 .pub。命令报找不到文件,就和 verbose 里这个路径的 type -1 是同一件事。把打出来的 SHA256: 和 GitHub 上的列表对照。排错页的路径是:右上角头像 → Settings → 侧栏 Access 里的 SSH and GPG keys。列表里要有同一串指纹。没有,就把这个 .pub 文件的内容加到账号上。加钥匙的表单在 Adding a new SSH key to your GitHub account。贴进去的是 .pub 这一份。
GitLab.com 的列表在头像 → Edit profile → Access → SSH keys。同一篇 GitLab 文档写明:上传公钥不会泄露私钥;要贴的是公钥,不要把私钥文件贴进去。
指纹对得上、verbose 里也有 Offering public key,服务器仍拒绝,这把钥匙被递出之后没有被接受。组织开了 SAML SSO 时,钥匙要单独授权,见 Authorizing an SSH key for use with single sign-on:SSH and GPG keys 页上这把钥匙旁边的 Configure SSO,再对那个组织点 Authorize。Deploy key 是另一套配置,这篇不写它的步骤。
排错页的警告原文是:在 GitHub 上看到不认识的 SSH key,立刻删掉它,并通过 GitHub Support 门户联系他们。来路不明的公钥可能是安全问题。同一段指向 Reviewing your SSH keys。
只读对上了,才改 Mac 上的钥匙和 Keychain
下面几步会改 ~/.ssh/config,或生成新钥匙,或把私钥放进 agent。只读结果已经说明钥匙没被选中、agent 是空的,或账号上没有这枚指纹,再做。
用 Apple 自带的 ssh-add
Generating a new SSH key and adding it to the ssh-agent 写明:把钥匙加进 agent 时,用 macOS 自带的 ssh-add,不用 MacPorts、Homebrew 或其他外部来源装的那一个。先看现在哪一个在 PATH 前面:
which -a ssh ssh-add
要出现的是 /usr/bin/ssh 和 /usr/bin/ssh-add。/opt/homebrew/bin 或 /opt/local/bin 排在前面时,后面的命令写成绝对路径 /usr/bin/ssh-add。外部那一份不认识 --apple-use-keychain,GitHub 把这个报错单独写了一页:Error: ssh-add: illegal option – apple-use-keychain。
同一篇生成文档给 macOS Sierra 10.12.2 及以后的 ~/.ssh/config 是:
Host github.com
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_ed25519
IdentityFile 换成你的私钥路径。文件不存在时,GitHub 的例子是先 touch ~/.ssh/config。三行的含义:
- ssh_config(5) 的
AddKeysToAgent yes:从文件载入钥匙时,把它加进正在运行的 ssh-agent。 UseKeychain yes是 Apple 的选项,写在 GitHub 这篇里:把 passphrase 放进钥匙串。同一篇写明,钥匙没有 passphrase 时,删掉UseKeychain这一行。IdentityFile是要读的身份文件。手册里的默认名单包含~/.ssh/id_ed25519,也包含若干你可能没生成过的文件名。
把私钥加进 agent,并让钥匙串记住 passphrase。GitHub 写的是 macOS Monterey(12.0)及以后:
ssh-add --apple-use-keychain ~/.ssh/id_ed25519
没有 passphrase 时,同一篇说去掉 --apple-use-keychain。Monterey 之前,这个旗标写成 -K。文档的原句是:12.0 之前,--apple-use-keychain 和 --apple-load-keychain 分别是 -K 和 -A。
配置报 Bad configuration option: usekeychain 时,GitHub 的写法是在这个 Host github.com 块里加上 IgnoreUnknown UseKeychain。手册对 IgnoreUnknown 的说明是:它用来忽略 ssh 不认识的选项,并且建议写在文件靠前的位置,因为它不管出现在它前面的未知选项。所以这一行要排在 UseKeychain 之前:
Host github.com
IgnoreUnknown UseKeychain
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_ed25519
这行报错也说明,正在读配置的 ssh 可能不是 /usr/bin/ssh。回到上面的 which -a。
readconf.c 在配置文件的所有者既不是 root 也不是你,或模式带有组写、其他用户写(mode & 022)时,直接 fatal("Bad owner or permissions on %s", filename)。对 ~/.ssh/config 做 chmod 777 会撞上这个检查。chmod 777 ~/.ssh 是下一节要避开的写法,服务器上则对上 StrictModes。
GitHub 排错页还写了 eval "$(ssh-agent -s)",并注明:agent 已经作为系统服务在跑时,这条可能失败。Mac 上先跑 ssh-add -l。它能列出指纹,或打出 The agent has no identities.,说明 agent 已经在。再执行上面的 ssh-add --apple-use-keychain,钥匙进的是这一个 agent。另起一个 agent 只属于当前这条命令所在的 shell。这是一般判断。
改完再跑 ssh -T git@github.com。成功句出现之后,回到仓库里跑你原来的 git fetch 或 git push。
新钥匙:不要覆盖已经有的那把
账号上没有这枚指纹,磁盘上也没有你还想继续用的私钥时,才生成新的。GitHub 生成文档的命令是:
ssh-keygen -t ed25519 -C "your_email@example.com"
-C 后面的邮箱是公钥末尾的注释,用来辨认这把钥匙。它不是 SSH 用户名,用户名仍然是 git。
提示 Enter a file in which to save the key 时,已有钥匙会让 ssh-keygen 询问要不要覆盖。GitHub 的原句是:如果以前生成过钥匙,它可能要求改写另一把,这时建议改用自定义文件名,不要覆盖。覆盖会换掉那把私钥对应的登录。自定义文件名要写进上面的 IdentityFile。passphrase 按那一页的提示输入。
同一页写明:2022-03-15,GitHub 停用了更老的钥匙类型。从那天起,DSA(ssh-dss)不再被支持,也不能把新的 DSA 钥匙加到个人账号上。变更记录在 Removed unencrypted Git protocol and certain SSH keys(2022-03-15):永久移除对 DSA 钥匙的支持。磁盘上如果还是 id_dsa,按上面的 ed25519 命令另生成一把,并只把新的 .pub 加到账号上。旧的私钥文件留在原地,直到你确认没有别的主机会用它。
GitLab 的文档把 ED25519 列为首选,DSA 标成 deprecated。生成命令同样是 ssh-keygen -t ed25519 -C "<comment>"。
一把以上的钥匙,以及不在 Terminal 里的程序
两台 GitHub 账号,或 GitHub 和 GitLab 各一把钥匙时,用 Host 别名把 IdentityFile 绑到这个名字上。ssh_config(5) 写明 IdentitiesOnly yes 的用途:即使 agent 里还有很多身份,也只使用配置或命令行指定的身份文件。默认是 no。
Host github.com-work
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
AddKeysToAgent yes
UseKeychain yes
remote 写成 git@github.com-work:ORG/REPO.git。github.com-work 是客户端的别名,HostName 才是真正要连的 github.com。User git 把「必须用 git」写进这个别名。GitLab 再写一块 Host gitlab.com,换成它自己的 IdentityFile。没有 passphrase 时,同样删掉 UseKeychain。
agent 里钥匙很多时,服务器可以在试完若干把之后断开。写作时读到的 auth.c 里,auth_maxtries_exceeded 发送的断开原因是 Too many authentication failures。sshd_config(5) 的 MaxAuthTries 默认是 6。这是 OpenSSH 服务器侧的一般说明。这篇没有读过 github.com 或 gitlab.com 的 sshd_config,不知道他们把这个数设成多少。客户端上对得上的做法,是手册已经写明的那一件:agent 身份很多时,用 IdentitiesOnly yes 配上这一把 IdentityFile,让 verbose 里只 Offering 你点名的那一把。
VS Code、图形界面,以及 AI 代理
这是一般判断。从 Dock 或启动台打开的程序,拿不到你在 Terminal 里 export 的环境。SSH_AUTH_SOCK 指向的 agent、PATH 里的 ssh,都可以和 Terminal 不一致。先在 Terminal.app 里跑 ssh -T git@github.com 和仓库里的 git fetch。Terminal 成功、VS Code 或代理仍报 Permission denied (publickey),差别在那个进程用的 ssh 和 agent。
看某一次 git 实际调用了什么,用 git 文档里的 GIT_SSH_COMMAND(git):
GIT_SSH_COMMAND="ssh -v" git fetch
它只影响这一次命令,把 verbose 打到这次 fetch 上。读上面那张表。本站另一篇写的是另一个进程找不到交互 shell 里已经装好的命令:明明全局安装了 agent-browser,为什么 AI 代理还是说“找不到”?。那篇是 PATH。这一篇是钥匙和 agent。两边都是「跑 git 的那个进程,不是你眼前这个交互 shell」。
自己的服务器或 VPS
这一节短。GitHub 和 GitLab 的用户名固定是 git(自建 GitLab 除外,见上)。你自己的机器上,用户名是那台机器上的账号。从厂商提供的控制台、或一条已经登着的会话里核对,因为正失败的这条 SSH 读不到对面的文件。
客户端上的 Permission denied (publickey) 不会告诉你用户名是否存在。在控制台里确认三件事:用户名是你打算登录的那个;家目录下有 ~/.ssh/authorized_keys;这个目录和这个文件的所有者是这个用户。
权限是一般指导,对应 sshd_config(5) 的 StrictModes。手册写明默认 yes:接受登录前检查用户文件和家目录的模式与所有者,因为人有时会把目录或文件做成任何人可写。人们为了通过这个检查而设置的模式是 ~/.ssh 为 700,authorized_keys 为 600。手册那段没有把这两个八进制写成唯一合法值。chmod 777 是反方向,写在下一节。
生效配置只打印、不重载:
sudo sshd -T | grep -E '^(pubkeyauthentication|authorizedkeysfile|maxauthtries) '
PubkeyAuthentication 的手册默认是 yes。打出来是 no 时,公钥认证在这台 sshd 上是关的。AuthorizedKeysFile 的手册默认是 .ssh/authorized_keys .ssh/authorized_keys2,路径相对家目录。你改的文件不在这行里,sshd 不会读它。
日志先看已经有的。单元名沿用 Connection refused 那篇 里已经确认的那个:Debian 和 Ubuntu 上常见 ssh,Red Hat 系列常见 sshd。
sudo journalctl -u ssh -n 50 --no-pager
sudo journalctl -u sshd -n 50 --no-pager
只跑这台机器上存在的单元。Debian 系上,同一类记录也常在 /var/log/auth.log。MaxAuthTries 那一段写明:失败次数达到这个值的一半之后,后续失败才会被记日志。默认日志级别下,前面几次公钥失败可以没有对应行。日志里暂时没有句子,不能当成钥匙已经被接受。这篇不写如何把 LogLevel 调高。
把公钥装上去、而且你还有一条走得通的登录(例如密码)时,用 ssh-copy-id(1):
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host
它通过一次已经能登录的连接,把公钥写进 authorized_keys。唯一的一条 SSH 也是 Permission denied (publickey) 时,这条命令走的是同一条失败的路。改文件的地方是厂商控制台:把 .pub 里的一整行追加进去,一行一把公钥。
要不要把一台新机器的 22 端口露到公网,写在 部署一个 GitHub 项目前,最应该先确认的 10 件事。那篇不负责修这一句。
不要做的事
私钥留在这台 Mac 上。没有 .pub 后缀的那个文件是私钥。不要把它贴进工单、聊天,或一个 AI 代理的输入框。对方要的是公钥时,给 .pub 的内容。GitHub 排错页对「账号上有一把你不认识的钥匙」的处理是删掉它并联系 GitHub Support,见上一节。
不要对 ~/.ssh、config、私钥或 authorized_keys 做 chmod 777。~/.ssh/config 若带组写或其他用户写,客户端按上面的 readconf.c 停在 Bad owner or permissions。服务器在 StrictModes yes(手册默认)下会在登录前检查家目录和用户文件的模式与所有者;任何人可写就是手册点名的那种情况。700 和 600 仍是上一节的一般指导,不是手册里的唯一合法八进制。
不要把 ~/.ssh 整个目录删掉。里面同时有私钥、config 和 known_hosts。known_hosts 按主机保存,整目录删掉会连同与这次报错无关的主机记录一起消失。主机密钥那一层的做法在 那篇 host key 文章 和 known_hosts 修复命令生成器。
这篇停在这里
- 这些命令没有为这篇在 Mac 或 VPS 上重跑。依据是上面链接的手册、OpenSSH 源码和 GitHub、GitLab 文档。机器上的
man优先。 - 这篇不锁定某一个 macOS 版本,也不锁定 Apple 对 OpenSSH 的补丁级别。
which -a用来看这一次实际叫到哪一个二进制。 - Windows 的 OpenSSH 和 Git Bash 在 GitHub 那两篇文档里有单独步骤。这篇不按那些步骤写。
- GitHub Enterprise 的主机名不在这里展开。测试页写明:换成你的企业域名,并使用那个实例要求的用户名。这篇的命令停留在
git@github.com。 - 硬件安全钥匙(
ed25519-sk)是 GitHub 生成文档里的另一节。这篇的命令是普通的ed25519。 Too many authentication failures在你自己的 OpenSSH 服务器上对得上MaxAuthTries。github.com 是否使用手册默认值,这篇没有读到。- 公司 VPN、HTTP 代理、设备管理配置文件。
ssh -G里如果已经有proxyjump,先把那一行读完。 - 搜索量。这篇没有另做外部数据。
隧道和端口一直在变的那次经过,在 一次ssh排错经历。其他阅读路线见 自部署阅读路线。英文稿是同一题的另一份写法:Permission denied (publickey) for git@github.com on a Mac: the server refused every offered key。
这篇如果有用,欢迎自愿赞助,用来继续更新博客和自部署笔记。这不是慈善捐赠。