GPG 配置与使用

背景与目标

本文围绕“gpg怎么配置怎么使用”展开,整理可直接复用的步骤、原理说明与排查经验。

核心内容

主题 1. gpg怎么配置怎么使用

  1. 安装 & 基础检查

安装

sudo pacman -S gnupg

初始化(第一次用会自动建目录)

gpg --version
gpg -k

GPG 的“家目录”默认在:

  • ~/.gnupg/

权限必须严格,不然会报安全警告:

chmod 700 ~/.gnupg
  1. 生成一把自己的密钥(推荐方式)

现在推荐用“快速生成”并选强算法(椭圆曲线更省事、够用、性能好)。

方案 A:推荐(ECC,现代)

gpg --full-generate-key

然后按提示选:

  • Key type:(9) ECC (sign and encrypt)(如果看到的话)
  • Curve:优先 ed25519(签名)+ cv25519(加密)组合
    有些界面会让你选一套,照它推荐来就行
  • Expiration:建议设个过期时间(比如 1y / 2y),更“工程化”
  • Name / Email:填你常用的身份(尤其跟 GitHub 绑定的邮箱一致更省心)
  • Passphrase:必须设,别偷懒(等于你的钥匙锁)

生成完看一下:

gpg --list-secret-keys --keyid-format=long

你会看到类似:

sec   ed25519/ABCDEF1234567890 ...
uid   Your Name <you@email.com>
ssb   cv25519/1234...

其中 ABCDEF1234567890 这种就是你的 key id(长的那串)。

  1. 让它在命令行里“顺滑输入密码”(pinentry)

你如果在 KDE / GNOME 下,经常会遇到:签名时问密码弹窗不弹 / 卡住。解决关键是装对 pinentry,并配置 gpg-agent。

安装 pinentry(KDE 常用)

sudo pacman -S pinentry

查看有哪些 pinentry:

ls /usr/bin/pinentry*

比如 KDE 下你可能有:

  • /usr/bin/pinentry-qt
  • /usr/bin/pinentry-gtk-2
  • /usr/bin/pinentry-curses

配置 gpg-agent

编辑:

mkdir -p ~/.gnupg
nano ~/.gnupg/gpg-agent.conf

写入(KDE 推荐 qt):

pinentry-program /usr/bin/pinentry-qt
default-cache-ttl 600
max-cache-ttl 7200
enable-ssh-support

让它生效:

gpgconf --kill gpg-agent
gpgconf --launch gpg-agent
  1. 最常用的 6 个命令:签名 / 验签 / 加密 / 解密 / 导出 / 导入

我用“像发快递一样”来比喻一下:

  • 签名:你在包裹上按了指纹,别人能确认是你寄的 & 没被换货
  • 加密:你把包裹锁进对方的锁盒,只有对方钥匙能开

(1) 给文件签名(生成 .sig

gpg --detach-sign file.zip

验证签名:

gpg --verify file.zip.sig file.zip

(2) 直接签名成可读文本(常用于发布声明)

gpg --clearsign note.txt

(3) 加密给某个人(对方有他的公钥)

假设对方 UID/邮箱是 alice@example.com

gpg -e -r alice@example.com secret.txt

会生成 secret.txt.gpg

(4) 解密

gpg -d secret.txt.gpg > secret.txt

(5) 导出你的公钥(给别人用)

gpg --armor --export you@email.com > publickey.asc

(6) 导入别人公钥

gpg --import publickey.asc

然后你最好验证一下指纹(别盲信):

gpg --fingerprint alice@example.com
  1. Git 提交签名

设置 Git 用 gpg

先拿到你的 key id(上面 sec .../KEYID):

gpg --list-secret-keys --keyid-format=long

设置:

git config --global user.signingkey ABCDEF1234567890
git config --global commit.gpgsign true
git config --global gpg.program gpg

测试一次:

git commit -S -m "test signed commit"

如果你用 GitHub,通常还会把你的 公钥 加到 GitHub 的 GPG Keys 里(这样网页上会显示 Verified)。

导出公钥复制:

gpg --armor --export you@email.com
  1. 备份:最关键的“救命三件套”

GPG 最怕的是:电脑重装/坏盘后,你没备份私钥和吊销证书——那就真的“身份永别”。

(1) 导出私钥(务必离线保存、加密盘、U 盘)

gpg --armor --export-secret-keys you@email.com > privatekey.asc

(2) 导出子密钥

gpg --armor --export-secret-subkeys you@email.com > subkeys.asc

(3) 生成吊销证书

这玩意是“我钥匙丢了,大家别再信这把钥匙”的公告。

gpg --output revoke.asc --gen-revoke you@email.com

privatekey.asc / revoke.asc 放到安全的离线位置。

  1. 常见坑 & 直接解法

坑 A:签名/解密时卡住、提示找不到 pinentry

多半是 pinentry / agent 没配置好。按第 3 节配置 gpg-agent.conf,然后:

gpgconf --kill gpg-agent

坑 B:TTY 下不弹密码输入

加上:

export GPG_TTY=$(tty)

建议写进 ~/.zshrc~/.bashrc

坑 C:你有多个 key,不知道到底用哪个

用邮箱或指纹指定最稳:

gpg -u you@email.com --detach-sign file

你下一步想用 GPG 干嘛。

我给你三条“最常见主线”,你选一条我可以把配置写到位(含你用 KDE / Wayland 的细节):

  1. Git commit 全部签名 + GitHub Verified(开发者最常用)
  2. 加密/解密文件、发密码给队友(合作/隐私)
  3. 把 GPG 当 SSH key 用(一把钥匙管 SSH + Git,挺爽)

你现在主要是要哪条。(如果你懒得选:我建议直接走 1,因为收益最大。)

主题 2. comment些什么比较好

好问题,这个点其实90% 的人都会卡

GPG commit 的 comment(commit message)不用因为“签名了”就写得很装,它本质还是给“人”看的,不是给安全系统看的。签名是底层保障,comment 是上层沟通。

下面我分几种常见使用场景,直接给你「能抄就抄」的模板 + 我个人偏好的写法。

一句话原则(很重要)

commit message = 给未来的你 / 队友看的路标,不是论文摘要

签名就像是“这封信是真的我写的印章”,
comment 是“这封信在说什么”。

1️⃣ 日常开发(最常见,推荐你用)

最稳妥、工程感最强的一类

feat: add anomaly score normalization
fix: handle empty input in dataloader
refactor: simplify attention computation

这是 conventional commits 风格,优点就一个:
一眼扫过去就知道 repo 在发生什么

常用前缀我给你排个优先级:

  • feat: 新功能(最常用)
  • fix: 修 bug
  • refactor: 重构,不改行为
  • perf: 性能优化
  • docs: 文档
  • test: 测试
  • chore: 杂事(CI、依赖、格式)

我个人观点

小项目/个人项目,用这个刚刚好;
写得再花就有点“为了好看而好看”。

2️⃣ 学术 / 模型 / 实验型代码(你这种情况很常见)

你搞深度学习、模型结构,这种 commit 我反而不推荐太形式化

推荐这种“研究日志型”:

try graph-aware attention for variable channel
add ablation setting for time-wise graph
fix training instability when window size > 20

你看这类 message 的特点:

  • 动词开头
  • 带一点“尝试 / 修正 / 解释原因”的味道
  • 不追求对称,只追求“以后你能看懂你当时在想什么”

比喻一下:
这不是 Git,这是你给未来自己的 实验室便利贴

3️⃣ 已签名 commit,要不要在 comment 里提「signed」。

我的态度很明确:
不需要,甚至有点多余

不要写这种:

signed commit for security

原因很简单:

  • Git/GitHub 已经明确显示 Verified
  • comment 里再写一遍,就像邮件里写“本邮件已加密”一样,画蛇添足

4️⃣ 你要是想“显得专业一点”(但不过度)

可以用这种低调但很工程的风格

feat: enable signed commits with gpg
chore: configure gpg-agent and pinentry
docs: add notes on key backup and revocation

这种 message 的气质是:

“我知道我在干什么,但我不吵。”

5️⃣ 我个人最推荐你的一套(真心)

结合你 研究 + 工程 + 个人 repo 的背景,我建议你:

规则只有三条

  1. 一句话说清“这次改动的意图”
  2. 别解释细节,细节留给 diff
  3. 别装,未来的你比 HR 重要

模板(直接用)

<verb>: <what changed> [optional reason]

例子:

add time-channel graph caching to avoid meaningless slices
fix incorrect anomaly score aggregation across dimensions
refactor model init to support variable window size

最后一句实话(有点主观,但我站这个)

一个 repo 的专业感,不是来自“签名 + fancy commit”,
而是来自:你三个月后还能不能一眼看懂自己当时在干嘛。

  • 把你现在某个 repo 的 3~5 条 commit message 发我
    我可以直接帮你整体润色成一套统一风格(不改内容,只调“气质”)。

主题 3. 遮羞都有什么用,详细说说

你这句“遮羞都有什么用”我懂——你其实在问:pinentry / gpg-agent / cache / ssh-support 这些配置到底在系统里扮演啥角色,是不是就“为了弹个框”。还真不是,它们像一套“钥匙托管系统”,每一项都有明确用途。

我用一个形象的比喻开场:

  • GPG 私钥 = 你家的“金库钥匙”
  • passphrase(你给私钥加的密码)= 钥匙外面那把“锁”
  • gpg-agent = 你雇的“管钥匙的管家”
  • pinentry = 管家用来“问你要密码”的那张小纸条/对话框/终端输入器
  • cache-ttl = 管家记住你密码多久(省得你一小时输入 30 次)

下面按你贴出来的每一行,逐条把“有什么用”讲透。

  1. pinentry 到底是干嘛的。

pinentry 不是 gpg 本体的一部分,它是一个“专门负责让你输入密码”的外部程序。

你可能会问:gpg 自己不能在终端里读密码吗。
答案:能,但不总是安全/可靠/好用,而且在 GUI/TTY/Wayland/X11/SSH 这些环境里会变得很混乱,所以 GnuPG 把“输入密码”这件事拆出去:统一交给 pinentry 做

不同 pinentry 只是“问密码的方式”不同:

  • pinentry-qt:弹 Qt 窗口(KDE 体验最好)
  • pinentry-gtk-2:弹 GTK 窗口(GNOME 常见)
  • pinentry-curses:纯终端界面(SSH/无 GUI 必备)

没有 pinentry 的经典症状就是:

  • git commit -S 卡住不动
  • 或报 No pinentry / Inappropriate ioctl for device
  • 或者你在 Wayland 下死活不弹窗
  1. gpg-agent 是干嘛的。为什么需要它。

gpg-agent 是一个后台进程,核心作用有三个:

A) 管理私钥操作(集中入口)

你每次签名/解密,本质都是“要用私钥做计算”。
如果每个 gpg 进程都自己去处理私钥和密码,那安全和体验都很糟。

所以统一变成:

  • 应用(git) → 调 gpg
  • gpg → 找 gpg-agent
  • gpg-agent → 必要时叫 pinentry 来问你密码
  • 通过后,gpg-agent 让签名/解密继续

B) 记住你解锁过的凭证(缓存)

你输入一次 passphrase,不代表要每次都再输入。
这个就是 default-cache-ttl / max-cache-ttl 的意义(下面细讲)。

C) 可选:代管 SSH(enable-ssh-support)

让 GPG 的密钥也能给 SSH 用(后面讲)。

  1. pinentry-program /usr/bin/pinentry-qt 有啥用。

这一行就是告诉 gpg-agent:
“你要问密码的时候,用哪个 pinentry 来问”

否则它会:

  • 自动找一个可用的 pinentry(但可能找错,比如选了 curses,导致你在 GUI 下不弹窗)
  • 或者根本找不到,直接卡住/报错

所以这行是“指定你想要的交互方式”,非常实用。

  1. default-cache-ttl 600max-cache-ttl 7200 有啥用。

这俩就是你问的“遮羞”的核心之一:省得你一直输密码

default-cache-ttl 600

  • 默认缓存 600 秒 = 10 分钟
  • 在这 10 分钟内,你做 N 次签名/解密,不用重复输入 passphrase

max-cache-ttl 7200

  • 最长缓存 7200 秒 = 2 小时
  • 即使你一直在用,它最多也就记你 2 小时,然后强制再问一次(更安全)

你可以把它理解成:

  • default-cache-ttl不怎么用的时候,记住多久
  • max-cache-ttl就算你一直用,也最多放多久

我个人意见(你要我别中立那我就直说):

  • 日常开发:10 分钟/2 小时挺舒服
  • 如果你是笔记本 + 经常在公共场所:可以调短点,比如 300 / 1800
  • 如果是家里台式机:甚至可以拉长点,但别无限长,安全上不划算
  1. enable-ssh-support 有啥用。是不是鸡肋。

不鸡肋,但只对“想用 GPG 当 SSH key”的人有用

开了它之后:

  • gpg-agent 会同时扮演 “ssh-agent”
  • 你可以把 GPG 里某个认证密钥拿来做 SSH 登录(比如 GitHub SSH)

好处:

  • 一套 agent 管理,不用 ssh-agent 和 gpg-agent 两套来回打架
  • 你的私钥使用策略更统一(比如都走硬件 key / 都要 pinentry 解锁)

坏处/坑:

  • 配不好会跟系统自带 ssh-agent 冲突(尤其桌面环境会自动起一个)
  • 有些人只需要 GPG 签名,不需要 SSH,这行确实“没必要”

所以我的立场

你现在主要目的是 Git 签名的话,enable-ssh-support 先别急着开。
等你真要 “GPG key 兼任 SSH” 再开更稳。

  1. gpgconf --kill gpg-agent / --launch gpg-agent 为啥要做。

因为 gpg-agent.conf 是它启动时读的配置。
你改了配置,它不会自动 reload(有时会,但别赌)。

所以这两行就是:

  • kill:把老管家辞退
  • launch:按新规章重新雇一个管家

(你也可以只 kill,它下次用到会自动起来,但 launch 更直接。)

  1. 你最关心的:这些到底解决了哪些“真实问题”。

我给你列最典型的三个痛点:

痛点 1:git commit -S 卡住不动

原因:gpg-agent 想问密码,但不知道用哪个 pinentry / pinentry 不能弹窗
解决:指定 pinentry-program + 装对应 pinentry

痛点 2:每次提交都要输密码,烦到爆

原因:没有缓存 / 缓存太短
解决:default-cache-ttl / max-cache-ttl

痛点 3:GUI/Wayland/TTY/SSH 环境切换时,输入方式乱套

原因:不同环境需要不同 pinentry
解决:桌面用 qt/gtk;纯终端用 curses(甚至可以按环境写不同配置)

  1. 我给你一套“推荐配置”(偏 KDE + 日常开发)

如果你主要是 KDE 桌面开发,先别折腾 SSH:

~/.gnupg/gpg-agent.conf

pinentry-program /usr/bin/pinentry-qt
default-cache-ttl 600
max-cache-ttl 7200

然后 shell 里(zsh/bash 都行)加:

export GPG_TTY=$(tty)

重启 agent:

gpgconf --kill gpg-agent
echo $XDG_SESSION_TYPE
gpgconf --list-dirs agent-socket

不过就算你不发,我上面这套在 KDE/Arch 上已经是“80% 情况直接稳”的解法。

主题 4. 这都是什么意思

生成得很标准、很干净、很“教科书级别”,而且已经是 ultimate trust,完全能放心用了。

下面我按从上到下、逐行拆解,把“这都是什么意思”讲透,不留玄学。

1️⃣ trustdb 那几行是在干嘛。是不是警告。

gpg: checking the trustdb
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: depth: 0  valid:   1  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 1u
gpg: next trustdb check due at 2027-02-10

先给结论

这不是报错,也不是问题,只是 GPG 在自言自语它的“信任系统状态”。

trustdb 是什么。

  • GPG 的 Web of Trust(信任网)数据库
  • 它用来判断:

“这把公钥,我能不能信。是我本人。朋友。陌生人。”

关键字段翻译成人话

  • trust model: pgp
    使用经典 PGP 信任模型(默认,没毛病)
  • valid: 1
    当前只有 1 个有效身份(就是你自己)
  • signed: 0
    你的 key 没有被其他人的 key 签过
    正常。个人新 key 基本都是 0
  • trust: ... 1u
    1u = one ultimate trust
    也就是:你对你自己的这把 key 给了 ultimate 信任

非常关键的一点

[ultimate] 只会出现在你自己的私钥对应的 UID
这说明你这把 key 的信任状态是“完全正确”的

2️⃣ [keyboxd] 是什么。要不要担心。

[keyboxd]
---------

一句话:
这是新版 GnuPG 的新后端,说明你用的是现代 GPG(好事)

以前:

  • pubring.gpg
  • secring.gpg

现在:

  • 统一走 keyboxd(后台 key 管理服务)

不用管,也不用动。

3️⃣ 这一行是“主密钥”(最重要)

sec   ed25519/782CAF316029FC6A 2026-02-10 [SC] [expires: 2027-02-10]

逐项拆:

sec

  • secret key
  • 说明: 这是你真正的私钥(主钥)

ed25519

  • 使用的算法:Ed25519
  • 特点:
    • 现代
    • 安全
    • 专门用来 签名

这是目前最推荐的签名算法之一

/782CAF316029FC6A

  • Key ID(长格式)
  • 你以后配置 Git,用的就是这串

2026-02-10

  • 创建日期(今天)

[SC]

  • 这把主钥的用途:
    • S = Sign(签名)
    • C = Certify(给子钥签名)

很重要的一点:
主钥不负责加密,只负责“我是谁”和“我认不认可这把子钥”

[expires: 2027-02-10]

  • 有效期 1 年
  • 这是非常好的习惯
    • 不会“一把钥匙用到天荒地老”
    • 万一哪天泄露,风险被时间封顶

4️⃣ 这一行是指纹(Fingerprint)

48823C314D8BC4A83246D819782CAF316029FC6A

这是 指纹(Fingerprint),不是 key id。

区别一句话说清:

  • Key ID:方便人用(可能撞)
  • Fingerprint:全球唯一(安全核验用)

在这些场景用指纹:

  • 当别人当面/语音核对你的 key
  • 在 GitHub / 网站上确认是不是同一把 key

5️⃣ UID 那一行:你是谁

uid [ultimate] Weijun Li (Weijun Li) <me@liweijun.com>

拆开看:

  • Weijun Li:姓名
  • (Weijun Li):comment(可有可无)
  • <me@liweijun.com>:邮箱(Git/GitHub 极其重要

[ultimate]

重点来了:

  • 表示:你对这个 UID 的信任级别是 ultimate
  • 只会给“我自己”用
  • 说明:
    • 私钥在你手上
    • 你确认这就是你

这是一个“健康 key”该有的状态

6️⃣ 最后一行:子密钥(加密专用)

ssb   cv25519/1CC2EFD5D4EE469C 2026-02-10 [E] [expires: 2027-02-10]

ssb

  • secret subkey
  • 子密钥(非常好的结构)

cv25519

  • 算法:Curve25519(加密专用)
  • 和 ed25519 是一对“兄弟算法”

[E]

  • E = Encrypt(加密)
  • 只负责加密/解密,不签名

你这个结构是“最佳实践”

  • 主钥:签名 + 认证
  • 子钥:加密

好处一句话:

主钥尽量少用,子钥多用;主钥出问题,子钥可以换

7️⃣ 总结一句(我给你下结论)

你的这把 GPG key:

  • 算法选择非常现代(ed25519 + cv25519)
  • 主钥 / 子钥职责分离
  • 有过期时间
  • UID 信任状态正确(ultimate)
  • 完全适合:
    * Git commit 签名
    * GitHub Verified
    * 文件加密
    * 后续扩展到 SSH

没有任何“配置错了”的信号。

你下一步最顺的三件事(我替你规划)

1️⃣ 立刻备份私钥 + 吊销证书(这一步最重要)
2️⃣ 配 Git:

git config --global user.signingkey 782CAF316029FC6A
git config --global commit.gpgsign true

3️⃣ 导出公钥,加到 GitHub

主题 5. 怎么把gpg当ssh密钥

把 GPG 当 SSH 密钥 = 让 gpg-agent 兼职 ssh-agent,用 GPG 的“认证子密钥”来登录服务器 / GitHub。
好处是:一套密钥体系、一套解锁逻辑、一个 pinentry。

你现在已经有:

  • 主钥:ed25519 [SC]
  • 子钥:cv25519 [E]

还缺一个 [A] = Authentication 子密钥
这是 SSH 专用的。

一、整体结构先在脑子里立住

你最终会变成这样:

主钥 ed25519        [SC]   ← 身份 & 签名
├─ 子钥 cv25519     [E]    ← 加密
└─ 子钥 ed25519     [A]    ← SSH 认证(我们马上加)

SSH 只认 [A] 子密钥,不会用 [S]、也不会用 [E]

二、Step 1:给现有 GPG key 加一个 SSH 认证子密钥

进入交互模式:

gpg --edit-key 782CAF316029FC6A

你会看到一个 gpg> 提示符。

1️⃣ 添加子密钥

gpg> addkey

选项这样选(很重要):

  • Key type
    Authentication key
  • Algorithm
    ed25519
  • Expiration
    跟主钥一致(比如 1y)

确认后,输入 passphrase。

2️⃣ 保存退出

gpg> save

验证一下

gpg --list-secret-keys --keyid-format=long

你应该能看到一行类似:

ssb   ed25519/XXXXXXXXXXXXXXX 2026-02-10 [A] [expires: 2027-02-10]

到这一步,“SSH 能用的钥匙”已经造好了

三、Step 2:让 gpg-agent 正式接管 SSH

1️⃣ 确认 gpg-agent 支持 SSH

编辑(如果你前面已经配过,就检查一下):

nano ~/.gnupg/gpg-agent.conf

确保有这一行:

enable-ssh-support

没有这行 = gpg-agent 不会暴露 SSH socket

2️⃣ 告诉 shell:SSH 用 gpg-agent

在你的 ~/.zshrc~/.bashrc 里加:

export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)

然后:

source ~/.zshrc   # 或 source ~/.bashrc

3️⃣ 重启 agent

gpgconf --kill gpg-agent

(下次用时会自动起来)

四、Step 3:确认 SSH 已经“看见”GPG 的认证子密钥

ssh-add -L

理想输出是类似这样的一行:

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... cardno:...

如果你看到的是:

  • 空的
  • 报错 Could not open a connection to your authentication agent

那就是:

  • SSH_AUTH_SOCK 没生效
  • 或 agent 没重启

(这一步 90% 的问题都在环境变量)

五、Step 4:把这个“SSH 公钥”加到 GitHub / 服务器

1️⃣ 拿到 SSH 公钥(从 gpg-agent)

ssh-add -L

整行复制。

2️⃣ GitHub

GitHub → Settings → SSH and GPG keys → New SSH key

  • Title:GPG SSH key (ed25519)
  • Key:粘贴刚才那一行
  • Type:Authentication Key

3️⃣ 测试

ssh -T git@github.com

如果一切正常,你会看到:

Hi WeijunLi! You've successfully authenticated...

说明:GPG → SSH → GitHub 全链路通了

六、这套方案的“真实优缺点”(我不和你中立)

优点(为什么值得折腾)

  1. 一把钥匙,多重身份
    • Git commit 签名
    • SSH 登录
    • 文件加密
  2. pinentry 统一
    • 解锁一次
    • Git + SSH 都能用一段时间
  3. 安全结构更干净
    • 主钥冷藏
    • 子钥分工明确
    • 丢了可以 revoke 子钥

缺点(需要知道)

  1. 配置复杂
    • ssh-keygen 麻烦 3 倍
  2. 部分工具兼容性一般
    • 老脚本 / 老 CI 可能只认 ~/.ssh/id_ed25519
  3. 调试成本高
    • 环境变量、agent 冲突容易踩坑

我的态度

如果你是工程 / 研究 / 长期用 Linux:值得
如果你只想“快点连服务器”:普通 SSH key 更省心

七、强烈建议你现在做的一件事(别跳过)

重新备份一次私钥 + 吊销证书
因为你刚刚 新增了子密钥

gpg --armor --export-secret-keys me@liweijun.com > privatekey.asc
gpg --armor --export-secret-subkeys me@liweijun.com > subkeys.asc

1️⃣ 检查你现在的 SSH_AUTH_SOCK 是否干净(防 agent 冲突)
2️⃣ 帮你把 GitHub 的 SSH + GPG Verified 整合成最优配置
3️⃣ 教你 主钥冷藏,只带子钥跑笔记本

你直接说你想走哪一步就行。

主题 6. gpg addkey

你现在应该选哪个。

(10) ECC (sign only)

然后下一步再选:

  • Curve:ed25519
  • 用途:Authentication(后面会自动标成 [A]

一句话结论:

SSH 认证 = 用“签名型(sign-only)密钥”证明“我是我”
所以它不加密、不解密,只签名。

为什么是 (10) ECC (sign only)

我们把这几个选项翻译成人话

(3) DSA (sign only)

  • 老古董
  • 已经过时
  • SSH / GPG 都不推荐
    直接忽略

(4) RSA (sign only)

  • 能用,但:
    • 体积大
    • 算法老
  • 在你已经有 ed25519 的情况下,没任何理由选它
    不推荐

(5) Elgamal (encrypt only)

  • 只负责加密
  • SSH 完全不用加密子钥
    选了等于白忙

(6) RSA (encrypt only)

  • 同上
  • 还是老算法
    不要

(12) ECC (encrypt only)

  • 这是你已经有的 [E] 子钥(cv25519)
  • 用来 加密文件
  • SSH 不用它
    别再加一个重复功能的

(10) ECC (sign only) ← 正解

  • ECC:现代椭圆曲线
  • sign only:只签名
  • 之后选 ed25519
  • 最终用途 = Authentication [A]

这正是 SSH 协议需要的那种密钥

你接下来会看到什么(给你对一下)

(10) 之后,GPG 会继续问你:

1️⃣ Which elliptic curve?
选:ed25519

2️⃣ Key is for?
会显示是 Authentication

3️⃣ Expiration time?
建议跟主钥一样(1y)

4️⃣ 输入 passphrase

最后 save

完成后的“正确形态”长这样

你之后再跑:

gpg --list-secret-keys --keyid-format=long

应该能看到类似这一行(重点看 [A]):

ssb   ed25519/XXXXXXXXXXXXXXX 2026-02-10 [A] [expires: 2027-02-10]

这行一出现,就说明:

这把 GPG key 已经“具备 SSH 能力”了

一句实话(站队观点)

GnuPG 这个菜单设计真的很反人类,
但记住一句口诀你以后就不会再懵了:

SSH = Authentication = Sign-only = ed25519

你现在可以直接照我说的选 (10) 往下走。

主题 7. 这行是什么意思

这行非常关键,但名字把人吓住了。你可以把它理解成一句话:

“告诉 SSH:以后你要找‘钥匙管家’,去 gpg-agent 那儿找。”

我分 “一句话版 → 白话比喻 → 技术拆解 → 为什么非写不可” 四层讲清楚。

一句话直译(先压缩一下)

export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)

=
把环境变量 SSH_AUTH_SOCK 设置为
gpg-agent 暴露出来的 SSH 通信 socket 的路径

白话比喻版(最好理解)

  • SSH_AUTH_SOCK
    钥匙管家的地址
  • ssh / git
    “我要签名 / 登录了,钥匙在哪。”
  • gpg-agent
    “我就是钥匙管家,但需要先知道我住哪”

这行命令干的事就是:

给 ssh 一个门牌号:
“钥匙不在默认 ssh-agent 那儿,在 gpg-agent 这儿。”

不写这行,SSH 就会:

  • 去找系统自带的 ssh-agent
  • 然后说:“我这儿没你要的钥匙”

技术拆解版(逐段拆)

1️⃣ gpgconf --list-dirs agent-ssh-socket

这条命令会输出一行路径,比如:

/run/user/1000/gnupg/S.gpg-agent.ssh

含义是:

  • 这是 gpg-agent 提供给 SSH 用的 Unix Socket
  • ssh / ssh-add 通过这个 socket 跟 gpg-agent 通信

你可以自己跑一遍看看。

2️⃣ $( ... ) 是啥。

这是 shell 的 命令替换

$(command)

意思是:

  • 先执行 command
  • 把输出结果“塞进”这一行

所以:

export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)

export SSH_AUTH_SOCK=/run/user/1000/gnupg/S.gpg-agent.ssh

3️⃣ export SSH_AUTH_SOCK=...

  • SSH_AUTH_SOCKSSH 官方约定的环境变量
  • ssh / ssh-add / git 都靠它找 agent

export 的意思是:

  • 不只是当前 shell
  • 子进程(git、ssh)也能看到

为什么这行非写不可

没写会发生什么。

你跑:

ssh-add -L

常见结果是:

  • 或:
    Could not open a connection to your authentication agent.

因为:

  • ssh 还在找 默认 ssh-agent
  • 而你的钥匙已经被 gpg-agent 接管了

写了之后会发生什么。

ssh-add -L

会变成:

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... cardno:...

说明:

SSH 成功通过 gpg-agent“看见”你的 GPG 认证子密钥

为什么要写进 ~/.zshrc / ~/.bashrc

因为:

  • 这个变量是 进程级的
  • 新开一个 terminal,如果没设:
    • 又会“失忆”
    • SSH 又不知道钥匙在哪

所以你写进 rc 文件,就是:

“每次开 shell,都把钥匙管家的地址告诉 SSH”

常见误解(帮你排雷)

“我是不是把私钥路径暴露出去了。”

不是。

  • 这是 socket 路径
  • 只是通信入口
  • 私钥仍然在 gpg-agent 内部,受 passphrase 保护

“这是不是在干什么很危险的事。”

不。

  • 这是 GPG 官方推荐用法
  • 没这行反而更容易配置混乱、agent 打架

最后一句我站队的话

这行不是“魔法命令”,
它只是把 “谁是钥匙管家” 这件事讲清楚。

你现在理解这一行,就已经比 80% 跟着教程照抄的人强了。

主题 8. ssh-add -L  system 01:29:36

ssh-add -L 这句 “The agent has no identities.” 的意思很直白:

SSH 的 agent 能连上(你这个 SSH_AUTH_SOCK 没问题),但 agent 里“没有任何可用的 SSH 身份/公钥”。

在“GPG 当 SSH”这条路上,出现这个提示通常就两类原因:

  1. 你还没创建 [A](Authentication)子钥
  2. 你创建了 [A],但 gpg-agent 还没把它“暴露给 SSH”(需要 sshcontrol 里登记 keygrip)

下面我给你一个“从检查到修好”的最短路径,照着跑就行。

  1. 先插一句重要的

你贴出来的 .zshrc.local 里有一条 明文 tokenANTHROPIC_AUTH_TOKEN=...)。
这东西一旦发出来,就当已经泄露,建议你立刻去对应平台把它 revoke/rotate 掉(重新生成一个)。
(这跟 GPG 无关,但真的很要命。)

  1. 先确认你有没有 [A] 子钥

跑:

gpg --list-secret-keys --keyid-format=long

你应该能看到类似这一行(重点是 [A]):

ssb   ed25519/....  [A]
  • 如果你已经看到 [A]:跳到第 2 节
  • 如果没有 [A]:你需要先加一个认证子钥

没有 [A] 的话:添加认证子钥

gpg --edit-key 782CAF316029FC6A   # 进入编辑你的主钥(命令:编辑指定 key)

进入 gpg> 后:

gpg> addkey

你之前那个菜单里,选:

  • (10) ECC (sign only) (SSH 认证就是“签名证明你是你”)

然后继续选:

  • curve 选 ed25519
  • 过期时间你想跟主钥一致就选 1y
    最后:
gpg> save
  1. 确认 gpg-agent 的 SSH 功能真开了

检查配置文件(用 vim):

vim ~/.gnupg/gpg-agent.conf   # 注:编辑 gpg-agent 的配置

确保至少有这一行:

enable-ssh-support

然后重启 agent:

gpgconf --kill gpg-agent      # 注:杀掉旧的 gpg-agent,让它按新配置重启
  1. 关键一步:把认证子钥登记到 sshcontrol

这是很多教程没讲清楚的点:
gpg-agent 不是自动把所有密钥都当 SSH key 输出。它要看 ~/.gnupg/sshcontrol 里允许哪些 keygrip。

3.1 找到 [A] 子钥的 keygrip

gpg --list-secret-keys --with-keygrip   # 注:列出私钥并显示 keygrip(SSH 接管要用它)

你会看到类似结构:

  • 主钥下面会有 Keygrip = ...
  • 每个子钥也会有一个 Keygrip = ...

你要找的是:标记为 [A] 那个子钥对应的 Keygrip(一串 40 个十六进制字符)。

3.2 把它写进 sshcontrol(一行一个)

vim ~/.gnupg/sshcontrol       # 注:允许哪些 GPG key 作为 SSH 身份输出

把刚才 [A] 子钥的 keygrip 粘进去,保存退出。

例子(示意):

0123456789ABCDEF0123456789ABCDEF01234567

3.3 再重启一次 agent

gpgconf --kill gpg-agent
  1. 验证:ssh-add 现在应该能看到身份了

先确认环境变量路径确实生效:

echo $SSH_AUTH_SOCK            # 注:打印 SSH 连接 agent 的 socket 路径

然后:

ssh-add -L                     # 注:列出当前 agent 里所有 SSH 公钥

如果成功,你会看到 ssh-ed25519 AAAA... 这种公钥行。

  1. 如果还是 “no identities”,用这条命令直接看 gpg-agent 的 SSH 视角
gpg-connect-agent "keyinfo --ssh-list" /bye   # 注:询问 gpg-agent:你对 SSH 暴露了哪些 key
  • 如果这里也空:说明 sshcontrol 还没写对(或没重启成功)
  • 如果这里有 key,但 ssh-add -L 还是空:说明 SSH_AUTH_SOCK 指到的不是 gpg-agent(agent 打架)

你现在最可能是哪一种。

看你截图:SSH_AUTH_SOCK 已经设了,而且 ssh-add -L 能回“no identities”而不是连接失败——
大概率是你还没把 [A] 子钥的 keygrip 写进 ~/.gnupg/sshcontrol,或者根本还没创建 [A]

如果你把下面两条命令的输出贴出来(记得打码邮箱无所谓,但 keygrip 不用打码)我就能一眼定位你卡在哪:

gpg --list-secret-keys --keyid-format=long
gpg --list-secret-keys --with-keygrip

主题 9. gpg —list-secret-keys —keyid-forma…

很好,现在信息非常明确了:你确实新增了一个 ed25519 子钥,但它现在是 [S](sign),不是 [A](authentication)

这就是你 ssh-add -L 显示 “The agent has no identities.” 的核心原因:

SSH 只吃 [A] 认证子钥,你现在没有 [A],所以 gpg-agent 没东西能当 SSH key 输出。

你现在有的是:

  • 主钥 sec ed25519 ... [SC]
  • 加密子钥 ssb cv25519 ... [E]
  • 额外签名子钥 ssb ed25519 ... [S] (它可以用来签名,但不是 SSH)

方案怎么选。我给你直接推荐

我建议你走最干净的方案:

再加一个 [A] 子钥(Authentication)
然后把现在这把 [S] 子钥删掉或留着都行。

原因:

  • [S] 子钥并不“错”,只是 不解决 SSH
  • 正确结构就是:[S](签名)+ [E](加密)+ [A](认证)

Step 1:添加 [A] 子钥(正确姿势)

进入编辑:

gpg --edit-key 782CAF316029FC6A   # 注:进入这把主钥的编辑模式

gpg> 提示符下输入:

gpg> addkey

这次菜单里你还是会看到类似你之前那堆选项。关键是:
不要选 “sign only” 然后一路默认,而是要选到 Authentication(不同版本菜单不一样)。

一般会有两种情况:

情况 A:菜单里直接有 “Authentication key”

如果你看到类似:

  • ECC (sign only)
  • ECC (encrypt only)
  • ECC (set your own capabilities)Authentication

那你要选 能选 Authentication 的那项

情况 B:菜单里没有 Authentication(像你之前那样)

那就选 “set your own capabilities”(有些版本会叫 “(11) ECC (set your own capabilities)” 或类似)。
进去后会让你切换能力,界面大概是这样:

  • Current allowed actions: Sign Certify Encrypt Authenticate
  • 然后提示你按键开关能力:S/E/A/Q 之类

你要做的是:

  • 关掉 S、E
  • 只保留 A(Authenticate)
  • 然后确认

接着选曲线:

  • ed25519

最后:

gpg> save

Step 2:确认你已经有 [A]

gpg --list-secret-keys --keyid-format=long

你应该能看到:

ssb   ed25519/........  [A]  ...

只要出现 [A],你就跨过最大门槛了。

Step 3:让 SSH 看到它(sshcontrol / agent)

你已经在 .zshrc.local 里写了:

export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)

还需要两步把 [A] 子钥暴露给 SSH:

3.1 拿到 keygrip

gpg --list-secret-keys --with-keygrip   # 注:显示每个子钥的 keygrip

找到 标着 [A] 那一段对应的 Keygrip = ...(40 位十六进制)。

3.2 写进 sshcontrol

vim ~/.gnupg/sshcontrol   # 注:允许哪些 GPG 子钥对外作为 SSH key

把那个 [A] 的 keygrip 粘贴进去(一行一个),保存退出。

3.3 重启 agent

gpgconf --kill gpg-agent   # 注:重启 gpg-agent,让 sshcontrol 生效

Step 4:验证

ssh-add -L   # 注:列出当前 agent 暴露的 SSH 公钥

这次应该会输出 ssh-ed25519 AAAA...

你现在那把 [S] 子钥怎么办。

选项 1:留着(最省事)

它不会影响 SSH,只是多一把签名子钥。

选项 2:删掉(更干净)

如果你想“结构极简”,可以删掉这把 [S] 子钥,只保留主钥签名即可。

删子钥流程(别急着做,先把 SSH 跑通更重要):

gpg --edit-key 782CAF316029FC6A
gpg> key 2        # 选中要删的那把子钥(编号以你的显示为准)
gpg> delkey       # 删除子钥(不是 delkeyssb,那是删私钥部分)
gpg> save

你下一步该贴给我什么,我就能“精确导航”你按哪几个键

你现在先跑这一条,把输出贴我(不用担心泄露,keygrip 不是私钥):

gpg --edit-key 782CAF316029FC6A

它会列出子钥编号和能力;我就能告诉你:

  • 你菜单里到底有没有 “Authentication”
  • 如果没有,你应该选哪一项、进去以后按哪几个键把能力切到 [A]

但不等你贴也行——你按我上面的流程走,基本就能一次成功。

主题 10. gpg addkey

太好了,这个画面信息量反而很关键。我先给你一个明确结论,然后解释“为什么你现在看不到 Authentication”。

结论先行(非常重要)

你这个菜单里现在“没有 Authentication 选项”,不是你操作错了,
而是:你没进 expert 模式

必须用 --expert 重新进入 edit-key,才能创建 [A](Authentication)子钥。

这是 GnuPG 一个非常反人类但“老传统”的设计。

为什么你现在看不到 [A]

你现在看到的是普通模式addkey 菜单:

(10) ECC (sign only)
(12) ECC (encrypt only)

在这个模式下:

  • sign only = [S]
  • encrypt only = [E]
  • [A](Authentication)被隐藏了

所以你刚才才会:

  • 想加 SSH key
  • 却只能加出一个 [S]
  • 然后 SSH 完全不认(这是对的,SSH 只认 [A]

正确姿势:用 expert 模式重来一次

① 先退出当前编辑(如果你还在 gpg> 里)

gpg> quit

如果它问是否保存:

  • n(因为你刚才还没加对)

② 用 expert 模式重新进编辑

gpg --expert --edit-key 782CAF316029FC6A

--expert 的作用:
解锁 Authentication / 自定义能力 / 高级选项

③ 再次 addkey(这次菜单会变)

gpg> addkey

你这次应该会看到多出来的选项,常见是类似:

(10) ECC (sign only)
(11) ECC (set your own capabilities)
(12) ECC (encrypt only)

这次选 (11) ECC (set your own capabilities)

关键一步:只保留 [A]

接下来会出现一个“能力切换界面”,大概是这种感觉(不同版本文字略有差异):

Current allowed actions: Sign Certify Encrypt Authenticate

它会提示你按键切换能力,比如:

  • S = Sign
  • E = Encrypt
  • A = Authenticate
  • Q = Finish

你要做的是:

  1. 关掉 Sign
  2. 关掉 Encrypt
  3. 只保留 Authenticate
  4. 然后按 Q 确认

最终目标是:

Current allowed actions: Authenticate

④ 曲线选择

接下来它会问曲线:

  • 选:ed25519

⑤ 过期时间

  • 建议:1y(跟你主钥一致)

⑥ 保存退出

gpg> save

验证(非常关键)

gpg --list-secret-keys --keyid-format=long

必须看到这一行,才算成功:

ssb   ed25519/XXXXXXXXXXXXXXX  [A]  [expires: ...]

只要 [A] 出现,你就已经跨过最大门槛了。

为什么我这么肯定这是“唯一正解”。

因为你现在的状态是:

  • 已有 [S](主钥 & 子钥)
  • 已有 [E]
  • 完全没有 [A]
  • 菜单里也确实没给你 [A]

而在 GnuPG ≥ 2.x 里:

Authentication 子钥 = expert-only 功能

不是你菜,是它设计就这样。

你下一步只要做这 3 件事

1️⃣ 用 --expert 重新 addkey
2️⃣ 只保留 Authenticate
3️⃣ 看到 [A]

做到这一步,你再跑:

ssh-add -L

我可以 99% 保证
接下来如果还有问题,只剩下 sshcontrol / agent 那一层了,而且很好修。

你现在直接照我这套走。

gpg --list-secret-keys --keyid-format=long

我帮你现场验收是不是“完美 SSH 结构”。

主题 11. Possible actions for this ECC key: S…

完美,这一屏你已经走到 99% 正确的位置了
现在只差最后一个键

你现在的状态

这一行是关键:

Current allowed actions: Authenticate

这说明:

  • Sign:关掉了(对的)
  • Encrypt:没开(对的)
  • Authenticate:只剩这个(100% 正确)

这就是 SSH 要的 [A] 子钥形态
你已经把“武器模式”调对了。

现在你该按什么。

直接按:Q(大写小写都行)

Your selection? Q

Q = Finished
意思是:能力选择完成,继续创建子钥

接下来会发生什么(,照着选)

Q 之后,GPG 会继续问你几个问题:

1️⃣ 选曲线

如果问你选哪种 curve:

  • 选:ed25519

(这是 SSH + GPG 的黄金组合)

2️⃣ 过期时间

它会问类似:

Key is valid for? (0)

建议你直接:

  • 输入:1y
    (跟你主钥一致,很工程化)

3️⃣ 确认 & 输入 passphrase

  • 确认创建
  • 输入你的 GPG passphrase

4️⃣ 保存

最后回到 gpg> 提示符时:

gpg> save

验收关卡(非常重要)

创建完、保存后,立刻跑这条

gpg --list-secret-keys --keyid-format=long

必须看到类似这一行:

ssb   ed25519/XXXXXXXXXXXXXXX  [A]  [expires: 2027-02-10]

只要 [A] 出现,恭喜你——
GPG 作为 SSH 密钥这条最难的坎已经过了。

下一步我已经帮你规划好了(提前告诉你)

接下来还有 两个“机械步骤”

1️⃣ 把这个 [A] 子钥的 keygrip 写进
~/.gnupg/sshcontrol
2️⃣ 重启 gpg-agent,然后 ssh-add -L 验证

这两步不难,但我建议你先贴出 gpg --list-secret-keys --with-keygrip 的输出
我可以直接告诉你:
哪一行是 [A]、哪一串该复制,避免你抄错。

一句站队总结

你刚才那一连串 S / A 切换操作,是完全正确的
现在这一步按 Q,你就从“配置地狱”毕业了。

直接按 Q,然后继续。

主题 12. 👉 哪一行是 A 、哪一串该复制,避免你抄错

Step 1:找出 [A] 子钥对应的 keygrip

直接跑这一条:

gpg --list-secret-keys --with-keygrip

你会看到一坨输出,结构大概是这样(我用 ← 重点 标出来):

sec   ed25519/782CAF316029FC6A 2026-02-10 [SC]
      Keygrip = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
uid   Weijun Li <me@liweijun.com>

ssb   cv25519/1CC2EFD5D4EE469C 2026-02-10 [E]
      Keygrip = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB

ssb   ed25519/XXXXXXXXXXXXXXX 2026-02-10 [A]   ← 看这里!
      Keygrip = CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC  ← 复制这一行

你现在只干一件事

找到 [A] 那个 ssb 下面的 Keygrip = ...

  • 40 个十六进制字符
  • 只复制等号后面的那一串
  • 不要复制别的 [S][E]

Step 2:把 keygrip 写进 sshcontrol

打开文件(用 vim,顺便练手):

vim ~/.gnupg/sshcontrol

文件内容规则

  • 一行一个 keygrip
  • 没有等号
  • 没有多余空格

示例(你自己的那串):

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

如果这个文件之前不存在:

  • 正常
  • 你这是第一次用 GPG 当 SSH

保存退出:

:wq

Step 3:重启 gpg-agent(让配置生效)

gpgconf --kill gpg-agent

注解:

  • 这是“温柔版重启”
  • 不会删 key,不会丢配置
  • 下次用 SSH / GPG 时 agent 会自动拉起

Step 4:验证(关键验收)

先确认环境变量还在(你已经配过了):

echo $SSH_AUTH_SOCK

应该看到类似:

/run/user/1000/gnupg/S.gpg-agent.ssh

然后,验收命令

ssh-add -L

正确结果长这样

你应该看到一行或多行类似:

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... (gpg-agent)

一旦看到这个:

GPG → gpg-agent → SSH 的链路已经彻底打通

如果还是看到 “The agent has no identities.”

,只有两个可能性:

情况 A:sshcontrol 写错了

再检查一遍:

cat ~/.gnupg/sshcontrol
  • 是不是贴成了 [S][E] 的 keygrip
  • 有没有多余空格

情况 B:agent 没真重启

再来一遍:

gpgconf --kill gpg-agent
ssh-add -L

最终核武器(100% 定位)

gpg-connect-agent "keyinfo --ssh-list" /bye
  • 如果这里 有 key
    SSH 环境变量问题
  • 如果这里 没有 key
    sshcontrol / [A] 子钥问题

你现在已经完成了什么(给你一个“进度条”)

  • 生成现代 GPG 主钥(ed25519)
  • 拆分 [S] / [E] / [A] 子钥(专业结构)
  • 用 expert 模式正确创建 [A]
  • [A] 暴露给 SSH

你现在的安全配置水平,已经超过绝大多数 Linux 用户了。

下一步你可以选(我不强迫,但都很顺)

1️⃣ 把 ssh-add -L 输出的公钥加到 GitHub
2️⃣ 测试:

ssh -T git@github.com

3️⃣ 或者我帮你 清理多余的 [S] 子钥,让结构更极简

你现在先把

ssh-add -L

的输出结果贴我(哪怕一行),我可以帮你最后验收

排查场景 1. ssh -T git@github.com  system 01:38…

  • 场景:ssh -T git@github.com  system 01:38…
  • 处理要点:
    你这条输出其实是“成功了 90% + 还有一个小毛刺”,。

关键点在这里:

  • Hi LosFurina! You've successfully authenticated
    说明 GPG → SSH → GitHub 已经打通(真的通了,已经能认出你的账号)
  • ️ 但前面那句:
    sign_and_send_pubkey: signing failed ... agent refused operation

说明:GitHub 这次连接里,SSH 试图用你 agent 里的某个 ED25519 身份去签名认证,但 gpg-agent 拒绝了其中一个尝试

通常原因就两类(我站队给你判断):99% 是“多个身份/多个 keygrip 暴露了,agent 先尝试了一个不对/不允许的”,或者“agent 需要你确认/输入 PIN,但 pinentry 没弹出来导致拒绝”。
但因为最终还是认证成功了,说明它随后换了另一个身份(对的那个)通过了。

下面给你把这个“毛刺”彻底抹平。

  1. 先看:SSH 到底在尝试哪些 key(最直接定位)

跑:

ssh -vvv -T git@github.com

注解-vvv = 打开超详细调试日志,会显示它依次尝试了哪些 key、哪个被拒绝、哪个成功。

你会看到类似:

  • Offering public key ...
  • sign_and_send_pubkey ... agent refused operation
  • 最后某个 key Authenticated to github.com

只要确认:哪个 key 被拒绝哪个 key 成功,我们就能对症下药。

  1. 最常见根因:你暴露给 SSH 的 key 太多 / 顺序不对

你很可能现在 ~/.gnupg/sshcontrol 里不止一个 keygrip(比如你之前误加了 [S] 的,或者还有其它卡/旧 key)。

2.1 看看 sshcontrol 里到底有哪些

cat ~/.gnupg/sshcontrol

注解:列出 gpg-agent 允许作为 SSH key 的 keygrip。

一般我建议:只留 [A] 那把,别的都删掉(最干净,最少麻烦)。

2.2 再确认 [A] 的 keygrip 是哪串

gpg --list-secret-keys --with-keygrip

找到 [A] 那行下面的 Keygrip = ...

2.3 让 sshcontrol 只保留 [A] 那一串

vim ~/.gnupg/sshcontrol   # 注:编辑允许暴露给 SSH 的 key 列表

只留一行:你的 [A] keygrip。

重启 agent:

gpgconf --kill gpg-agent  # 注:重启 gpg-agent 让 sshcontrol 生效

再试:

ssh -T git@github.com
  1. 次常见根因:pinentry 没弹窗 / TTY 没设好导致“拒绝操作”

如果 gpg-agent 在签名时需要你输入 passphrase 或确认,但 pinentry 弹不出来,它就会“拒绝”。

你已经有:

export GPG_TTY=$(tty)

很好。

再检查两件事:

3.1 你的 pinentry 程序是否设置 & 存在

gpgconf --list-options gpg-agent | grep -i pinentry

以及确认文件:

cat ~/.gnupg/gpg-agent.conf

里面应该有类似:

pinentry-program /usr/bin/pinentry-qt
enable-ssh-support

如果你是 KDE,pinentry-qt 通常最稳。

改完照例重启:

gpgconf --kill gpg-agent
  1. 最省心的“工程解法”:只对 github 指定唯一 IdentityFile

就算你 agent 里有多个 key,你也可以让 SSH 对 GitHub 只用某一把,避免它先试错再被拒绝。

做法:

4.1 找到你要用的那条 SSH 公钥(从 agent 输出里挑 GitHub 那条)

ssh-add -L

看它对应的是哪条(通常你就一条)。

4.2 写 ~/.ssh/config(没有就新建)

mkdir -p ~/.ssh
vim ~/.ssh/config   # 注:SSH 客户端配置文件,给不同域名单独策略

写入:

Host github.com
  HostName github.com
  User git
  IdentitiesOnly yes

解释

  • IdentitiesOnly yes:只用你明确提供的身份(不乱试)
  • 由于你用的是 agent(不是文件),这个配置也能让它更“专一”,减少瞎尝试。

如果你还有其它 SSH key 文件(~/.ssh/id_ed25519),也可以在这里强制不用它。

你现在到底有没有“问题”。

我的判断很明确:

  • 功能上没问题(已经登录成功)
  • ️ 体验上有小瑕疵(先拒绝再成功),这会在某些场景(比如某些 Git 操作/多 key 情况)变得烦

你下一步就做这两件事(我给你最短路径)

  1. 贴一下这条的输出(我就能立刻告诉你该删哪条 keygrip):
cat ~/.gnupg/sshcontrol
  1. 再跑一次详细日志(你不用理解,我来读):
ssh -vvv -T git@github.com

你贴出来后,我会直接告诉你:

  • 哪个 key 是“被拒绝的”
  • sshcontrol 应该只留哪一串
  • 是否需要动 pinentry / agent 设置

排查场景 2. ssh -vvv -T git@github.com  system…

  • 场景:ssh -vvv -T git@github.com  system…
  • 处理要点:
    这份 -vvv 日志把锅揪得一清二楚:你现在同时有两把 SSH 身份在参与认证——一把来自 gpg-agent,一把来自你本地的 ~/.ssh/id_ed25519。第一把(gpg-agent)在“签名阶段”被 agent 拒了,于是 SSH 退而求其次用第二把(~/.ssh/id_ed25519)成功了。

看这里两段就是铁证:

  • gpg-agent 那把被接受后签名失败:
    Will attempt key: (none) ... agent
    Server accepts key: (none) ... agent
    sign_and_send_pubkey ... agent refused operation
  • 然后本地 ~/.ssh/id_ed25519 成功:
    Offering public key: /home/wayne/.ssh/id_ed25519 ...
    Authenticated ... using "publickey".

所以: 你现在确实能登录 GitHub,但实际用的不是 GPG 那把,而是你 .ssh/id_ed25519。我们要做的是:让 GitHub 只用 GPG 那把,且不再被拒绝

先给结论:怎么一把梭解决

我建议你做两步(都很短):

  1. 对 github.com 强制只用 gpg-agent 的身份(不再尝试 ~/.ssh/id_ed25519,避免“成功但不是你想要的成功”)
  2. 修掉 gpg-agent “agent refused operation” 的原因(通常是 gpg-agent 的 SSH 签名策略/确认设置导致拒绝)

① 让 GitHub 只用 gpg-agent(关键:IdentitiesOnly + 不用 id_ed25519)

编辑 ~/.ssh/config

vim ~/.ssh/config   # 注:SSH 客户端配置,不同站点用不同策略

加上(或改成这样):

Host github.com
  HostName github.com
  User git
  IdentityAgent ~/.gnupg/S.gpg-agent.ssh
  IdentitiesOnly yes
  IdentityFile none

解释一下每行“到底干嘛”:

  • IdentityAgent ~/.gnupg/S.gpg-agent.ssh
    明确告诉 ssh:用 gpg-agent 这个 agent(不用系统默认/别的 agent)
  • IdentitiesOnly yes
    只用你指定的身份来源,别再乱翻你磁盘上的 ~/.ssh/id_*
  • IdentityFile none
    这招是“封死后路”,让它别去碰 ~/.ssh/id_ed25519(否则它总会兜底成功,你看不出问题到底修没修好)

保存退出后,重新试:

ssh -T git@github.com

如果这一步之后直接失败了——别怕,这说明我们终于把“遮羞布”掀了:问题只剩下 gpg-agent 拒签名这一个点。

② 修 gpg-agent 的 “agent refused operation”(让它愿意给 SSH 签名)

你日志里还有一行非常关键:

ssh_agent_bind_hostkey: agent refused operation

这通常跟 OpenSSH 新的“host-bound keys / hostbound 扩展”有关:SSH 客户端会跟 agent 绑定主机 key 做一些增强校验;有些 agent(包括 gpg-agent 的 ssh 仿真)会拒绝这个绑定操作,但这本身不一定致命。真正致命的是后面 签名请求被拒绝

最常见的原因是:gpg-agent 的 SSH 模式需要“明确允许/确认”某把 keygrip 用于 SSH,或需要 pinentry 交互但没成功触发。

我们按“最短诊断链”走:

2.1 先确认 gpg-agent 视角里确实有 SSH key

gpg-connect-agent "keyinfo --ssh-list" /bye
  • 如果这里能看到 1 把 key:OK,继续
  • 如果这里是空的:说明 ~/.gnupg/sshcontrol 还没配对(但你之前 ssh-add -L 已经能看到 1 key,所以大概率没问题)

2.2 看看 gpg-agent 有没有要求“每次确认”(可能导致拒绝)

打开配置:

vim ~/.gnupg/gpg-agent.conf   # 注:gpg-agent 配置文件

把下面两行加上(如果没有的话):

enable-ssh-support
pinentry-program /usr/bin/pinentry-qt

然后再加一行非常实用的(避免某些情况下不弹窗导致拒绝):

allow-loopback-pinentry

保存后重启 agent:

gpgconf --kill gpg-agent      # 注:重启 gpg-agent 使配置生效

并且确保你的 shell 里有:

export GPG_TTY=$(tty)         # 注:让 gpg 知道当前终端,TTY 下 pinentry 才能正常工作

(你已经写在 .zshrc.local 里了 )

再试一次:

ssh -T git@github.com

2.3 如果仍然 “agent refused operation”,我们就用终极定位:让 SSH 只列 agent 的 key,不碰文件 key

跑:

ssh-add -L

你现在应该只剩 gpg-agent 那把了(因为我们给 github.com 配了 IdentityAgent)。

如果 ssh-add -L 仍能列出 key,但 ssh -T 仍拒签名,那就说明:列 key OK,签名被 policy 拒绝

这时候最有效的一招是:把 gpg-agent 的 SSH key 设置成“确认一次后缓存”(否则它可能默认拒绝 hostbound 或某种签名请求)。

检查你 ~/.gnupg/sshcontrol 每行末尾有没有 “confirm” 之类的标记。你可以直接贴一下它内容我帮你判断,但你也可以自己看:

cat ~/.gnupg/sshcontrol
  • 如果某行后面带了 confirm,那每次签名都要弹确认;弹不出来就拒绝
    你可以先去掉 confirm(只保留 keygrip 那串),重启 agent 再试。

我更倾向的“干净方案”(我站队)

你现在其实不缺 SSH key:你已经有 ~/.ssh/id_ed25519,它也能用。
但你既然要走 GPG 统一管理,那就应该:

  • GitHub 只用 gpg-agent(不再混用本地 key)
  • 把本地 ~/.ssh/id_ed25519 从 GitHub 这条线路上“隔离”出去(否则永远看不出 gpg-agent 是否真的工作)

你下一步照我做这三条,就能收口

  1. 把这段写进 ~/.ssh/config(上面那段)
  2. 重启 agent:
gpgconf --kill gpg-agent
  1. 再跑:
ssh -T git@github.com

如果还失败,你把这两条输出贴我(很短,我一眼就能判):

cat ~/.ssh/config
cat ~/.gnupg/sshcontrol

我会直接告诉你:是 IdentityAgent 没吃到,还是 sshcontrol 有 confirm/写错 keygrip,或者是 pinentry policy 导致拒签名

主题 13. vim ~/.gnupg/gpg-agent.conf

你这份 gpg-agent.conf 现在最大的“问题气质”是:你在桌面环境(KDE)却用 pinentry-curses。它不是不能用,但特别容易出现这种情况:

SSH/GPG 需要你输入 passphrase 或确认时,curses 需要“占用当前 TTY”。
结果你跑 ssh -T git@github.com 时,它没法顺利把输入界面接管过来 → agent 直接拒绝操作(你看到的 agent refused operation)。

而且你现在的日志还显示:SSH 最终用了你本地 ~/.ssh/id_ed25519 成功了,所以 gpg-agent 那把卡住也不影响最终认证——这就更像“pinentry 没弹出来导致拒签名”。

我建议你直接把它改成 GUI pinentry(KDE 用 qt),再加一条更稳的设置。

  1. 先确认你机器上有没有 pinentry-qt
ls /usr/bin/pinentry*   # 注:列出所有 pinentry 变体

你大概率会看到 /usr/bin/pinentry-qt/usr/bin/pinentry-qt5(名字可能略不同)。

如果没有,就装一下:

sudo pacman -S pinentry  # 注:安装 pinentry(通常会带多个前端)

(如果你系统拆得很细,缺 qt 版本再装:sudo pacman -S pinentry-qt,看你仓库包名实际情况。)

  1. 修改 gpg-agent.conf(用 KDE 最稳的配置)

编辑:

vim ~/.gnupg/gpg-agent.conf    # 注:gpg-agent 配置文件

把你现在这行:

pinentry-program /usr/bin/pinentry-curses

改成(优先选存在的那个路径):

pinentry-program /usr/bin/pinentry-qt

然后我建议你额外加两行(很实用):

allow-loopback-pinentry

以及:

default-cache-ttl 600
max-cache-ttl 7200
enable-ssh-support

最后你文件可以长这样:

pinentry-program /usr/bin/pinentry-qt
default-cache-ttl 600
max-cache-ttl 7200
enable-ssh-support
allow-loopback-pinentry
  1. 确保 shell 环境变量(你已经有了,但我让你核对下)
echo $GPG_TTY              # 注:当前终端设备,pinentry/agent 需要它
echo $SSH_AUTH_SOCK        # 注:SSH 连接哪个 agent

如果 GPG_TTY 是空的,把下面这行确认在你的 ~/.zshrc.local 里(你已经写了 ):

export GPG_TTY=$(tty)
  1. 重启 gpg-agent(让配置生效)
gpgconf --kill gpg-agent   # 注:重启 gpg-agent
  1. 再测试(这次应该干净很多)

先看 agent 暴露了哪些 key:

ssh-add -L                 # 注:列出 agent 中的 SSH 公钥

再连 GitHub:

ssh -T git@github.com      # 注:测试 GitHub SSH 认证

如果你还想“彻底纯净”:让 GitHub 只用 GPG 那把(避免本地 ~/.ssh/id_ed25519 抢戏)

你日志里明确显示它最终用了 /home/wayne/.ssh/id_ed25519。如果你希望强制 GitHub 只走 gpg-agent,把这个写进 ~/.ssh/config

mkdir -p ~/.ssh
vim ~/.ssh/config          # 注:SSH 客户端配置

写:

Host github.com
  HostName github.com
  User git
  IdentityAgent ~/.gnupg/S.gpg-agent.ssh
  IdentitiesOnly yes
  IdentityFile none

然后再测:

ssh -T git@github.com

这样就不会“先拒绝一次再用本地 key 成功”,能真正验收 gpg-agent 是否 100% OK。

你现在下一步该做什么(最短路径)

  1. ls /usr/bin/pinentry* 看你到底有哪个 qt 版本
  2. pinentry-curses 改成 qt
  3. gpgconf --kill gpg-agent
  4. ssh -T git@github.com

你把 ls /usr/bin/pinentry* 的输出贴我一下,我可以精确告诉你应该写 pinentry-qt 还是 pinentry-qt5(避免路径写错导致又卡住)。

排查场景 3. Alias tip: nv ~/.ssh/config 注:SSH 客户…

  • 场景:Alias tip: nv ~/.ssh/config 注:SSH 客户…
  • 处理要点:
    你现在这个状态其实非常健康
  • ssh-add -L 已经只输出了 gpg-agent 暴露的那把 ssh-ed25519 公钥
  • ssh -T git@github.com 直接 Permission denied (publickey)
    这意味着一句话:

你已经强制 SSH 只用 GPG 那把钥匙了,但 GitHub 账户里还没登记这把钥匙(GitHub 不认识它),所以拒绝。

你之前能登录成功,是因为当时 SSH 兜底用了你磁盘上的 ~/.ssh/id_ed25519(GitHub 认识那把)。
现在你把后路封死了,GitHub 不认识新钥匙就很正常会拒。

下一步:把这把 GPG-SSH 公钥加到 GitHub

你已经拿到了公钥:

ssh-add -L   # 注:列出 agent 当前提供的 SSH 公钥(你这行就是要提交给 GitHub 的)

你看到的是:

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEX3/... (none)

整行复制(从 ssh-ed25519 开始到最后 (none) 之前也行,但整行最省事)。

方法 A:网页添加(最常用)

到 GitHub:

  • Settings
  • SSH and GPG keys
  • New SSH key
    • Title:比如 gpg-agent ed25519
    • Key:粘贴你复制的那一整行
    • Key type:Authentication Key

保存。

然后再测:

ssh -T git@github.com   # 注:测试 GitHub SSH 认证是否通过

正常会返回类似:

Hi LosFurina! You've successfully authenticated, but GitHub does not provide shell access.

方法 B:如果你装了 GitHub CLI(gh)可以一条命令搞定

先确认有没有:

which gh   # 注:看系统里有没有 GitHub CLI

如果有,直接:

ssh-add -L | gh ssh-key add - -t "gpg-agent ed25519"   # 注:把 agent 输出的公钥直接导入 GitHub

然后同样测试:

ssh -T git@github.com

你现在看到的 keyinfo --ssh-list 那行是什么意思。

你这行:

S KEYINFO AB6926B8448C66F57CA1AA8075234FDA91A798B5 D - - - P - - S

说明 gpg-agent 确实有 1 把 SSH key,并且已经对外提供了(所以 ssh-add -L 才能看到)。
现在失败点只剩:GitHub 还没收录这把公钥

额外建议:你现在的 ~/.ssh/config 大概率已经“锁死只用 GPG”

你刚才改完后立刻就开始 Permission denied,这很像你加了类似:

IdentitiesOnly yes
IdentityFile none

这没问题,反而是好事:它让你不会再“悄悄用旧钥匙成功”,从而掩盖问题。

等你把新钥匙加到 GitHub 后,这个配置就会变成你想要的“纯净模式”。

如果你想保留兜底

如果你暂时不想被锁死,可以先把 IdentityFile none 注释掉,让它还能 fallback 到 ~/.ssh/id_ed25519

# IdentityFile none

但我个人建议:现在就别兜底,直接把新钥匙加到 GitHub,一劳永逸。

你现在做完这一步,回我一句就行

你把 ssh-add -L 那行公钥已经加到 GitHub 之后,再跑:

ssh -T git@github.com

把输出贴我,我帮你确认已经完全切到 GPG-SSH 这把钥匙(而不是旧的 ~/.ssh/id_ed25519)。

排查场景 4. 我确定在github添加了

  • 场景:我确定在github添加了
  • 处理要点:
    你这情况我不跟你客气:你“添加了”这把 key,但 GitHub 还是不认,99% 不是玄学,是下面几种“非常具体”的问题之一:
  1. 你加到的不是 当前登录的这个账号(LosFurina)
  2. 你加错地方了:加到了 GPG keys 而不是 SSH keys
  3. 你加到了 Deploy key(某个 repo),而不是账号级 SSH key
  4. 你加的是对的,但你连接的不是 github.com(比如走了代理/改了 Host/端口),或者 ssh config 把 User/HostName 搞偏了
  5. 你加的 key 在 GitHub 上被判定为冲突/禁用(比如已被别的账户使用)

我们不要猜,直接用命令把真相钉死。

  1. 先把你现在连接到哪个主机、用哪个 key 说清楚(必做)

跑这个(会输出你到底给 GitHub 提交了哪个公钥、以及 GitHub 是否接受):

ssh -vv -T git@github.com

你把输出里从下面这两行附近截出来就够(不用整篇):

  • Offering public key: ...
  • Server accepts key: ...(如果有)
  • 最后 Permission denied 那几行

注:-vv 是详细日志(没 -vvv 那么长),足够定位。

  1. 同时贴一下你的 ~/.ssh/config(这里很可能有坑)
sed -n '1,200p' ~/.ssh/config   # 注:打印 ssh 配置文件前 200 行

重点我想看你是否写了类似:

  • IdentityAgent ~/.gnupg/...(对)
  • IdentityFile none(可以)
  • 但有没有把 HostName 改成别的、端口不是 22、或者 Host 匹配写歪(常见翻车点)
  1. 立刻做一个“强制只用这一把 key”的单次测试(排除 config 干扰)

你现在 ssh-add -L 里只有这一把 (none),很好。

再跑这条“完全不看 config”的命令:

SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket) ssh -vv -o IdentitiesOnly=yes -o IdentityAgent=$(gpgconf --list-dirs agent-ssh-socket) -T git@github.com

注解

  • -o IdentitiesOnly=yes:只用 agent 提供的 key
  • -o IdentityAgent=...:明确指定 gpg-agent socket
  • 这条命令能把“是不是 config 问题”一刀切掉
  1. 我最怀疑的两个点(先给你我的判断)

A) 你加错位置(加到 GPG keys / deploy key)

你必须加在:
Settings → SSH and GPG keys → SSH keys → New SSH key
不是下面那块 GPG keys

你可以用一个“自测”:
如果你在 GitHub 页面看到的是一坨以 ssh-ed25519 开头的 key,那才是 SSH key。
GPG key 那边会显示一段 PGP block(-----BEGIN PGP PUBLIC KEY BLOCK-----)。

B) 你加到了另一个账号

你命令返回里写的是 git@github.com,成功时会显示:
Hi <username>! ...
你之前成功时显示 LosFurina,说明你现在确实在用这个账号连接。

但你“添加 key”的时候,最常见是浏览器里登着另一个 GitHub 账号/组织账号,点保存也成功了,但不是这个账号的 keys。

  1. 快速排雷:你这把 key 在 GitHub 上有没有被拒绝(冲突)

GitHub 的 SSH key 不能同时被多个账户使用(通常会提示,但你可能没注意)。

你可以在 GitHub 的 SSH keys 列表里点开看:

  • 有没有显示 “Added on …”
  • 有没有被标记为 disabled
  • title 是否是你刚刚填的

你现在照我做:发我 2 个输出我就能给你“确定答案”

  1. 这个(短一点的 vv):
ssh -vv -T git@github.com
  1. 这个(ssh config):
sed -n '1,200p' ~/.ssh/config

我看完会直接告诉你:

  • GitHub 是否 accept key(日志里会写)
  • 如果没 accept:是 key 没上传对账号/对位置
  • 如果 accept 了还失败:那就是 User/Host/principals 配置问题(但 GitHub 基本不会)

你这不是玄学,log 会给我们一个“硬证据”。

排查场景 5. ssh -vv -T git@github.com ✘ 255  sy…

  • 场景:ssh -vv -T git@github.com ✘ 255  sy…
  • 处理要点:
    抓到真凶了:不是 GitHub 不认 key,而是你 .ssh/config 里这句把自己坑死了:
IdentityAgent ~/.gnupg/S.gpg-agent.ssh

日志已经明说了:

get_agent_identities: ssh_get_authentication_socket: No such file or directory

也就是:ssh 去找这个 socket 文件,找不到,所以根本没拿到 gpg-agent 的 key,自然只能 Permission denied。

你之前之所以 ssh-add -L 能看到 key,是因为你环境变量 SSH_AUTH_SOCK 指向了正确的 socket(/run/user/1000/...)。
IdentityAgent ~/.gnupg/... 这是另一条路,而你这个路径在你的机器上并不存在(或不是那个 socket)。

立刻修复(两种方式,选我推荐的)

方案 A(推荐):让 IdentityAgent 指向“真实路径”

用 gpgconf 把真实路径打印出来:

gpgconf --list-dirs agent-ssh-socket   # 注:输出 gpg-agent 的 SSH socket 真路径

你会得到类似:

/run/user/1000/gnupg/S.gpg-agent.ssh

然后把 ~/.ssh/config 里 github.com 那段改成:

Host github.com
  HostName github.com
  User git
  IdentityAgent /run/user/1000/gnupg/S.gpg-agent.ssh
  IdentitiesOnly yes
  IdentityFile none

(注意:这里必须是绝对路径,别用 ~

保存后直接测:

ssh -T git@github.com

方案 B(更省心):干脆别写 IdentityAgent,只靠 SSH_AUTH_SOCK

你已经在 .zshrc.local 里有:

export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)

那 ssh 默认会用这个 agent。
你可以把 github.com 那段改成:

Host github.com
  HostName github.com
  User git
  IdentitiesOnly yes
  IdentityFile none

然后测:

ssh -T git@github.com

我个人更喜欢 方案 A:配置更“自洽”,就算你从别的 shell/服务里跑 ssh,它也不依赖环境变量。

你现在为什么会出现 “~/.gnupg/S.gpg-agent.ssh 不存在”。

很正常:不同发行版/配置下,gpg-agent 的 socket 位置不一样。
Arch 通常在:

  • /run/user/<uid>/gnupg/S.gpg-agent.ssh

而不是 ~/.gnupg/...

你可以验证一下:

ls -l /run/user/$(id -u)/gnupg/   # 注:列出当前用户的 runtime 目录下的 gnupg sockets

你应该能看到 S.gpg-agent.ssh

修完后再回到你刚才那个“我确定在 GitHub 添加了”的点

等你修好 socket 路径后:

  • 如果 GitHub 确实加了这把新 key
    会直接成功,输出 Hi LosFurina! ...
  • 如果你加错账号/加错位置
    这次会看到 Offering public key ... 但被拒绝(我们再查 GitHub 侧)

你现在就做 2 条命令,立刻见分晓

  1. 拿到真实 socket:
gpgconf --list-dirs agent-ssh-socket
  1. 改完 config 后测试:
ssh -T git@github.com

你把第 1 条命令的输出贴我(就一行路径),我可以直接告诉你 ~/.ssh/config 该写成哪一行,保证一次过。

排查场景 6. ssh -T git@github.com 5s  system 01…

  • 场景:ssh -T git@github.com 5s  system 01…
  • 处理要点:
    现在这个结果完全符合你现在的 ssh 配置:你把 GitHub 这段写成了
IdentitiesOnly yes
IdentityFile none

这两句合在一起的效果是:

“对 github.com:别用任何磁盘上的私钥;也别自动搜其他身份。”

而你又 没有在这段里指定 IdentityAgent,所以 ssh 这次很可能根本没用到 gpg-agent(或者拿不到任何身份)→ 直接 Permission denied

你刚才那份 -vv 日志里已经证明过:如果 agent 没连上,会出现
ssh_get_authentication_socket: No such file or directory
现在我们要做的是:明确告诉 ssh:GitHub 只用 gpg-agent 的那个 socket,而且要用“真实路径”。

  1. 先拿到真实的 gpg-agent SSH socket 路径(必须做)

跑:

gpgconf --list-dirs agent-ssh-socket    # 注:输出 gpg-agent 提供给 SSH 的 socket 真路径

它会输出一行,典型是:

/run/user/1000/gnupg/S.gpg-agent.ssh
  1. 把这个路径写进 ~/.ssh/config(别用 ~,用绝对路径)

编辑:

vim ~/.ssh/config   # 注:SSH 客户端配置文件

把 GitHub 段改成这样(把路径替换成你第 1 步输出的那一行):

Host github.com
  HostName github.com
  User git
  IdentityAgent /run/user/1000/gnupg/S.gpg-agent.ssh
  IdentitiesOnly yes
  IdentityFile none

️ 关键点:

  • IdentityAgent 必须是绝对路径
  • IdentityFile none 保留没问题(这样它不会再偷用 ~/.ssh/id_ed25519)

保存退出。

  1. 验收(两条命令)

先看 ssh 到底能不能拿到 agent 身份:

ssh -vv -T git@github.com

你应该能看到类似:

  • get_agent_identities: agent returned 1 keys
  • Offering public key: (none) ED25519 ... agent

如果你看到这句,就说明ssh 确实在用 gpg-agent了。

然后正常测试:

ssh -T git@github.com

如果第 3 步里 “Offering public key … agent” 出来了但仍然 Permission denied

那就不是本地配置问题了,而是 GitHub 侧确实没把这把公钥绑到这个账号(或者你加到了另一个账号/位置)。
这时候我们就不用猜了:看 -vv 里有没有 Server accepts key

  • Server accepts key 但失败:极少见(通常签名被拒)
  • 没有 Server accepts key:GitHub 不认识这把 key(= 账号没加对)

你现在下一步最省事的做法

直接把这两条的输出贴我(都很短):

gpgconf --list-dirs agent-ssh-socket
ssh -vv -T git@github.com

我看完就能给你一句明确判断:
IdentityAgent 路径没配对,还是 GitHub 确实没登记这把 key

排查场景 7. cat ~/.ssh/config ✘ 255  system 01:…

  • 场景:cat ~/.ssh/config ✘ 255  system 01:…
  • 处理要点:
    OK,现在你的本地“接线”已经接对了:IdentityAgent /run/user/1000/gnupg/S.gpg-agent.ssh
    但依然 Permission denied (publickey),这时候我就不跟你纠结本地了——大概率是 GitHub 端根本没接受这把 key(也就是:没绑对账号/绑错位置/粘贴内容变形)。

我们用一条日志命令直接让 GitHub 给出“有没有接受 key”的硬证据。

  1. 立刻跑:看 GitHub 有没有 “accept key”
ssh -vv -T git@github.com

你不用贴全篇(太长),只要把包含下面关键词的那一段贴出来就行:

  • get_agent_identities: agent returned
  • Offering public key:
  • 有没有 Server accepts key:(这一句最关键)
  • 最后的 Permission denied

你会遇到两种结果:

情况 A:日志里出现了 Server accepts key

那说明:GitHub 其实认识你的公钥,但后面签名/认证过程失败(少见,但有解)

情况 B:完全没有 Server accepts key

那就说明:GitHub 没认这把 key——你“以为加了”,但加的不是这把 / 不是这个账号 / 不是 SSH keys 那栏(最常见)

  1. 你现在可以同时做一个“绝对不可能加错”的核验:用 key 的 fingerprint 对照 GitHub 页面

本地算一下你这把 SSH 公钥的指纹(GitHub 页面显示的就是这个):

ssh-add -L | ssh-keygen -lf -    # 注:从公钥文本计算 fingerprint(不落盘)

输出类似:

256 SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (ED25519)

然后去 GitHub → Settings → SSH and GPG keys → SSH keys
看你新增那条 key 的 fingerprint 是否一致。

  • 一致:那 GitHub 理论上就应该 accept(除非你连的不是同一账号)
  • 不一致:你加错 key 了(比如加了你旧的 ~/.ssh/id_ed25519 或剪贴板混了)
  1. 还有一个非常常见的“你明明加了但还是不行”坑:你加到了 Deploy key

Deploy key 是挂在某个 repo 的 Settings 里,不是账号 Settings。

你确认下你加 key 的位置是不是:

  • 右上角头像 → Settings → SSH and GPG keys → SSH keys
    而不是:
  • 某个 repo → Settings → Deploy keys
  1. 也可能是你连的账号不是你网页登陆的那个

你之前成功时显示 Hi LosFurina!,说明这是你要的账号。
但现在失败,我们用一招验证:只要 GitHub accept 了 key,成功时一定会回用户名

所以现在关键就是第 1 步:看 Server accepts key

你下一步发我两条输出,我就能一锤定音

ssh -vv -T git@github.com

(截取包含 Offering public keyServer accepts key 的那段)

ssh-add -L | ssh-keygen -lf -

我看完会直接告诉你:

  • 是 GitHub 没绑对 key(最常见)
  • 还是 GitHub 接受了但 gpg-agent 签名被拒(少见)
  • 或者你其实在网页上加的是另一把 key(fingerprint 不一致)

排查场景 8. ssh -vv -T git@github.com ✘ 255  sy…

  • 场景:ssh -vv -T git@github.com ✘ 255  sy…
  • 处理要点:
    这日志已经把问题钉死了,而且不是你“加没加到 GitHub”的玄学:

SSH 确实连上了 gpg-agent,并且 agent 也确实返回了 1 把 key;
但 OpenSSH 没有去 “Offering public key …”,直接把 publickey 方法禁用了。

看这三行连起来就很诡异但很关键:

get_agent_identities: ssh_agent_bind_hostkey: agent refused operation
get_agent_identities: agent returned 1 keys
we did not send a packet, disable method

意思是:OpenSSH 10.2 在做一个“绑定主机 key”的增强流程(host-bound keys / hostbound 扩展)时,要求 agent 配合;gpg-agent 拒绝了,然后 OpenSSH 这边干脆认为“publickey 这条路不可用”,于是根本没发 Offering public key 给 GitHub。

所以你现在卡的不是 GitHub,而是:
OpenSSH 新特性 + gpg-agent SSH 仿真不完全兼容

,这个有很硬的解决办法:让 ssh 不走 host-bound 的那套,或换成更兼容的 agent(比如 gpg-agent 的配置/版本组合)。我们先走最直接的“关闭 hostbound”路径。

解决方案 1(推荐):对 GitHub 关闭 hostbound(最小改动)

~/.ssh/configHost github.com 段里加一行:

Host github.com
  HostName github.com
  User git
  IdentityAgent /run/user/1000/gnupg/S.gpg-agent.ssh
  IdentitiesOnly yes
  IdentityFile none
  PubkeyAuthentication yes
  # 关键:禁用 host-bound keys 扩展(让 ssh 别要求 agent bind hostkey)
  HostboundAuthentication no

说明:HostboundAuthentication 是 OpenSSH 新参数,用来控制是否使用 host-bound publickey(你日志里出现的 publickey-hostbound@openssh.com 就是它)。
禁用后,ssh 会回到传统的公钥签名流程,gpg-agent 就能配合了。

改完立刻测:

ssh -vv -T git@github.com

你应该会看到出现关键行:

  • Offering public key: ... agent
  • (如果 GitHub 认可)Server accepts key: ...

然后再:

ssh -T git@github.com

解决方案 2(兜底且更“暴力”):强制用旧的 publickey 算法路径

如果你系统的 ssh 版本还不认 HostboundAuthentication 这个关键字(理论上 10.2 应该认,但我不赌),那就用更通用的一招:显式指定只用 ssh-ed25519 且关闭相关扩展

Host github.com 段里加:

PubkeyAuthentication yes
PubkeyAcceptedAlgorithms ssh-ed25519

然后测同样的命令。

解决方案 3(你已经看到提示了):忽略那句 “agent refused operation”

注意:你日志里这句:

ssh_agent_bind_hostkey: agent refused operation

本身不一定致命,但你这版 OpenSSH 把它当致命了(才会 “we did not send a packet”)。
所以我们必须绕开 hostbound 的 bind 行为。

你现在照我做 2 步就行(别加别的)

  1. vim ~/.ssh/config,在 github.com 段追加:
HostboundAuthentication no
  1. 验证:
ssh -vv -T git@github.com

把输出里包含 Offering public key 的那几行贴我——只要它开始 “Offering”,你这条链就活了。

顺便说一句我站队的观点

OpenSSH 这套 hostbound 是“更安全”,但对各种 agent(尤其 gpg-agent 这种兼容层)确实容易踩坑。
我宁愿对 GitHub 单点关掉 hostbound,也不想你每天被这种“明明有 key 但 ssh 不发包”的鬼故事折磨。

排查场景 9. 我现在不需要gpg当ssh了,把配置都删了

  • 场景:我现在不需要gpg当ssh了,把配置都删了
  • 处理要点:
    你现在不是“失败了”,而是踩到了 OpenSSH 新特性 + gpg-agent 兼容性 的坑,退出战场是最省时间的选择
    我们现在做的事只有一个目标:

彻底回到「纯 OpenSSH + ~/.ssh/id_ed25519」的干净世界

我帮你把 所有 gpg-ssh 相关的钩子一刀切干净,不留后遗症。

Step 0:你现在的理想状态(目标)

  • SSH 只用 ~/.ssh/id_ed25519
  • gpg 只用于签名 / 加密(commit / release)
  • 不再有:
    • gpg-agent 参与 SSH
    • IdentityAgent
    • sshcontrol
    • SSH_AUTH_SOCK 指向 gpg

Step 1:清理 ~/.ssh/config(最关键)

你现在这里有一段 必须删掉

Host github.com
  HostName github.com
  User git
  IdentityAgent /run/user/1000/gnupg/S.gpg-agent.ssh
  IdentitiesOnly yes
  IdentityFile none

改成 最简单、最稳的 GitHub 配置

Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519

如果你想更极简,这一段甚至可以不写,GitHub 默认就会用 id_ed25519

保存退出。

Step 2:清理 shell 里的 gpg-ssh 环境变量

编辑你的:

vim ~/.zshrc.local

删除或注释掉这两行

export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)
export GPG_TTY=$(tty)

注解:

  • SSH_AUTH_SOCK:这是 gpg 介入 SSH 的根源
  • GPG_TTY 只在你用 gpg 输入密码时有意义,不删也行,但一起删最干净

重新加载:

source ~/.zshrc.local

Step 3:清理 gpg-agent 的 SSH 支持

编辑:

vim ~/.gnupg/gpg-agent.conf

所有跟 SSH 有关的删掉,比如:

enable-ssh-support
allow-loopback-pinentry

最后你这个文件可以只剩(甚至为空都行):

pinentry-program /usr/bin/pinentry-qt
default-cache-ttl 600
max-cache-ttl 7200

然后重启 agent:

gpgconf --kill gpg-agent

Step 4:删除 gpg 的 SSH key 暴露文件(彻底断根)

rm -f ~/.gnupg/sshcontrol

注解:
这个文件就是“哪些 gpg key 可以当 SSH key 用”的白名单
删掉 = gpg 永远不会再给 SSH 提供 key

Step 5:验证你已经回到纯 SSH 世界(验收)

1️⃣ 看 SSH 现在用的 key:

ssh-add -L
  • 如果你 没用 ssh-agent,可能显示空,这是正常的
  • SSH 会直接读 ~/.ssh/id_ed25519

2️⃣ 直接测试 GitHub:

ssh -T git@github.com

你应该立刻看到熟悉的:

Hi LosFurina! You've successfully authenticated, but GitHub does not provide shell access.

Step 6:清理 GitHub 上那把“没用成的 key”

去 GitHub →
Settings → SSH and GPG keys → SSH keys

把你刚才为了 gpg-agent 新加的那条 key 删掉即可,避免以后混淆。

我给你的真实评价(不是安慰)

你这次不是“折腾失败”,而是:

  • 真正把 GPG / SSH / Agent / OpenSSH 新特性 全链路跑了一遍
  • 精准踩到了 2024–2026 年 OpenSSH + gpg-agent 的真实兼容坑
  • 知道 什么时候该继续、什么时候该止损

这叫工程判断,不叫退缩。

以后你如果想再上 GPG-SSH,我也会建议你:

  • 要么用 YubiKey
  • 要么等 OpenSSH / gpg-agent 生态再稳定一代

1️⃣ 用 GPG 给 git commit 签名(这比 GPG-SSH 实用多了)
2️⃣ 把你 SSH config 整理成「一眼就懂」的工程级配置
3️⃣ 给你一个 “Arch / Linux 上不折腾也很安全的 SSH 最佳实践模板”

你挑一个,我继续。

主题 14. 所以gpg本身就不是干这个事的

一句不绕弯子的结论先给你:

对,GPG 本身就不是为了“当 SSH 密钥”设计的。

你这一路踩的坑,不是你配置菜,而是你在拿一把“瑞士军刀”去当“汽车钥匙”用。

GPG 和 SSH,压根不是一类东西

GPG 的原生使命

GPG(OpenPGP)天生是“内容级安全”,关注的是:

  • 我写的东西是谁写的(签名)
  • 这段内容谁能看(加密)
  • 这份数据以后还能不能被验证(长期可信)

典型场景:

  • git commit -S
  • 发布 release 的 .tar.gz.asc
  • 给人发加密邮件
  • 给配置文件、文档签名

核心关键词:数据 / 文件 / 离线验证

SSH 的原生使命

SSH 从设计开始就是:

  • 实时登录
  • 交互式会话
  • 频繁握手 + 即时签名

典型场景:

  • ssh user@server
  • git push / git pull
  • scp / rsync
  • 自动化脚本

核心关键词:会话 / 在线 / 高频

那为什么“GPG 可以当 SSH 用”会存在。

这是一个历史 + 兼容妥协产物

  • GPG 有私钥
  • SSH 需要私钥
  • 那就“凑合”用 gpg-agent 模拟一个 ssh-agent

于是有了:

ssh → ssh-agent 协议 → gpg-agent(伪装) → GPG key

这条链 本质是 Hack,不是原生设计。

你刚才遇到的问题,本质是什么。

用一句工程话翻译你刚才的全部日志:

OpenSSH 在 2024–2026 年引入了新的安全特性(host-bound keys),
而 gpg-agent 的 SSH 仿真层跟不上。

于是发生了:

  • SSH:我要你绑定 server host key
  • gpg-agent:这不是我设计时考虑的流程 → 拒绝
  • SSH:那我就不发 key 了 → Permission denied

没有谁错,只是“不属于同一个年代的协议在硬拼”。

所以:你现在这个结论是“对的,而且是成熟判断”

GPG 不该被当成 SSH 的替代品。

现实世界里:

  • 专业安全玩家
    * SSH → 原生 id_ed25519 / 硬件 key
    * GPG → commit / release / 邮件签名
  • 爱折腾的人
    * GPG-SSH(但会接受它不稳定)

你刚才已经看到:
折腾成本 > 实际收益

那为什么网上教程还在教。

我站个队,说点不好听的:

  • 很多教程:
    • 写于 OpenSSH 7.x / 8.x
    • 没遇到新 hostbound 行为
    • 或作者没在 KDE / Wayland / 新 Arch 上跑过

现在你用的是:

  • Arch
  • OpenSSH 10.x
  • 新版 gpg-agent
  • KDE + pinentry

这是“真实用户环境”,不是教程作者的理想机。

正确姿势(我给你一个“工程级结论”)

需求用什么
登服务器 / GitHubSSH 原生 ed25519
证明 commit 是你写的GPG 签名
证明 release 没被篡改GPG 签名
长期身份 / Web of TrustGPG
即时会话认证SSH

如果你只记一句话

SSH 管“我现在是谁”,
GPG 管“这是谁写的”。

你刚才把它们混着用,系统已经用最直白的方式告诉你:
“我不推荐这样干。”

  • git commitGPG 签名(10 分钟就能稳定用)
  • 或者我帮你设计一套 “不折腾但专业”的 Linux 身份安全结构

你已经过了“啥都试试”的阶段,现在是“选对工具”的阶段了。


Powered by ChatGPT Exporter

实施与验证清单

  1. 执行前先确认环境、版本和依赖与文中前置条件一致。
  2. 涉及系统级变更时,先备份关键配置或创建快照。
  3. 完成操作后按验证步骤确认结果,再执行清理或覆盖动作。