在 macOS 上执行一次 ssh-keygen,并不等于私钥已经进入 Secure Enclave。普通 ssh-keygen 生成的是磁盘上的私钥文件;ssh-add 本身也不直接调用安全芯片,它只是连接 SSH agent,让 agent 提供身份和签名能力。
最短的正确链路是:使用明确支持 Secure Enclave 的实现创建或导入密钥,让该实现提供兼容 OpenSSH 的 agent,把 ssh 和 ssh-add 指向同一个 socket,将对应公钥安装到服务器,最后核对客户端、服务器端和 agent 中的密钥指纹。本文以 Secretive 提供的 SSH agent 为主线;导入入口、socket 路径和版本限制应以 Secretive 当前官方文档为准,不能把其他工具的命令混进来。
第一次配置前怎么确认 OpenSSH 真正会走哪条路径
先记录系统、OpenSSH 和硬件信息。ssh -V 用于确认当前 OpenSSH 客户端版本,硬件型号则要对照 Apple 当前支持列表,不能只凭 macOS 版本判断设备是否具备 Secure Enclave。
sw_vers
ssh -V
system_profiler SPHardwareDataType
system_profiler 的输出可能包含设备序列号,不适合原样贴到公开 issue。接着收集最关键的运行时信息:
printf '%s\n' "${SSH_AUTH_SOCK:-<unset>}"
ssh -G <host> | grep -E '^(hostname|user|port|identityagent|identityfile) '
ssh-add -l
ssh -v <user>@<host>
这四条命令分别回答:
- 当前 shell 的
ssh-add会连接哪个 socket; - OpenSSH 对目标主机最终采用了哪个
IdentityAgent、用户、端口和候选身份文件; - 默认 agent 中有哪些密钥;
- 客户端实际提供了哪把公钥,以及失败发生在哪个认证阶段。
这里有一个很容易造成误判的差异:ssh-add 读取 SSH_AUTH_SOCK,而 ssh 还可以通过 IdentityAgent 选择另一个 agent。如果两者没有指向同一个 socket,ssh-add -l 看到的密钥可能根本不是 ssh 准备使用的密钥。排查时应以 ssh -G 和 ssh -v 显示的实际路径为准。
如果某个配置项不能识别,先查看当前系统随附的手册,不要根据其他 macOS 版本猜选项:
man ssh_config
man ssh-add
man ssh-keygen
普通 ssh-keygen 生成的私钥为什么不在 Secure Enclave 里
普通流程生成的是文件密钥:
ssh-keygen -t ed25519
私钥和公钥保存在 ~/.ssh 下,后续的 ssh-add ~/.ssh/id_ed25519 即使成功,也只是把该文件密钥交给普通 agent,并不会因此创建 Secure Enclave 密钥。
Secretive 是专门面向 macOS 的工具,现有资料将其描述为利用 Secure Enclave 保护和管理 SSH 密钥,并通过阻止按常规 OpenSSH 私钥导出的方式降低泄露风险。 SeKey 则是另一套将 Touch ID 与 Secure Enclave 用于 SSH 身份验证的 Rust 实现,它的 socket、交互方式和配置不能与 Secretive 混用。
如果当前 Apple ssh-keygen(1) 明确提供了某种 Secure Enclave 或 SK 类型,应以本机 man ssh-keygen 和对应系统版本文档为准。不能仅凭命令里出现 sk 就断言它使用的是 Apple Secure Enclave,因为外部 FIDO 密钥的 SK 类型与 macOS 内置 Secure Enclave 并不是同一条实现路径。
导入已有明文私钥时还要注意:导入成功不代表磁盘上的旧文件会自动消失。完成迁移后,旧私钥仍可能位于 ~/.ssh、备份目录、终端历史或同步盘里。应保留恢复副本直到新连接从独立终端验证成功,再按组织策略清理副本。
怎样让 ssh 和 ssh-add 指向同一个 Secure Enclave agent
先按所选工具的当前官方文档完成三件事:确认具体 Mac 型号受支持,启动工具,并创建或导入密钥。不要在配置中固定某个 group container 路径,也不要使用未经确认的命令行参数,因为这类路径和入口可能随版本变化。
从工具当前文档中取得正在使用的 agent socket 后,可以先在当前终端临时验证:
export SSH_AUTH_SOCK="<current-agent-socket>"
ssh-add -l
ssh-add -L
ssh-add -l 用来列出 agent 中的密钥指纹,ssh-add -L 只输出公钥。能够列出指纹说明 agent 已经暴露了身份,但不代表当前密钥一定处于可签名状态,也不代表背后一定是 Secure Enclave 实现。
如果 agent 中只有目标密钥,可以把公钥保存出来,供部署和固定身份使用:
ssh-add -L > <public-key-file>
agent 中有多把密钥时,不要把全部输出不加区分地当作目标身份;应使用工具当前提供的单项公钥导出方式,或只保留目标公钥。公钥可以公开,私钥仍然留在受保护环境中。
OpenSSH 的持久配置可以写成:
Host enclave-alias
HostName <host>
User <user>
Port <port>
IdentityAgent "<current-agent-socket>"
IdentityAgent 应使用当前真实路径,而不是从旧文章复制一个可能已经失效的固定地址。修改后先检查最终配置:
ssh -G enclave-alias | grep -E '^(hostname|user|port|identityagent|identityfile) '
如果本机 man ssh_config 明确支持用公钥文件标识由 agent 持有的密钥,还可以把身份固定到选定公钥:
Host enclave-alias
HostName <host>
User <user>
Port <port>
IdentityAgent "<current-agent-socket>"
IdentityFile "<selected-public-key-file>"
IdentitiesOnly yes
这里的 IdentityFile 应当只指向选定公钥文件,而不是普通私钥文件。这样 OpenSSH 可以据此向 agent 请求对应签名,同时减少默认文件密钥抢先命中的机会。若当前版本不支持这种用法,可以先验证 IdentityFile none:
Host enclave-alias
HostName <host>
User <user>
Port <port>
IdentityAgent "<current-agent-socket>"
IdentityFile none
IdentityFile none 的用途是排除默认文件身份,不等于禁止 agent 提供身份。是否按预期解析,同样要用 ssh -G enclave-alias 确认。
长期使用时,优先采用所选工具官方提供的登录项或启动集成,让 socket 路径在应用启动后保持一致。手工写入 SSH_AUTH_SOCK 更适合作为临时排查手段,因为工具重启后路径可能变化。
agent 找不到密钥时先查 SSH_AUTH_SOCK 和 IdentityAgent
下面两条报错通常出现在 agent 连接层:
Could not open a connection to your authentication agent
The agent has no identities
先比较环境变量与 OpenSSH 最终配置中的 socket:
printf '%s\n' "${SSH_AUTH_SOCK:-<unset>}"
ssh -G <host> | grep -E '^(identityagent|identityfile) '
然后用明确的 socket 临时执行一次:
SSH_AUTH_SOCK="<current-agent-socket>" ssh-add -l
如果这样能列出密钥,而普通 ssh-add -l 失败,说明是环境变量没有指向当前 agent。反过来,如果 ssh-add -l 正常,但 ssh -G 显示的 IdentityAgent 指向别处,则 ssh 会使用另一套身份。
如果 socket 路径不存在、工具尚未启动,或工具重启后路径发生变化,应从当前官方文档或工具输出重新取得路径。不要为了绕过报错,手工创建一个指向未知位置的软链接。
多套 agent 共存时,还要检查 ~/.ssh/config 中的 Host *。OpenSSH 对很多配置项采用“最先取得的值”,因此不能默认后面的具体 Host 块一定能覆盖前面的 Host *。更稳妥的写法是按具体主机或别名配置,并始终用 ssh -G <alias> 验证最终结果。
如果 ssh -G 不认识 IdentityAgent,先检查 ssh -V 和当前 ssh_config(5),不要假设所有 macOS 版本都支持同一个选项。
公钥已经安装为什么还会 Permission denied (publickey)
Permission denied (publickey) 至少涉及四个需要区分的阶段:agent 中存在身份、客户端提供了正确公钥、服务器接受该公钥、客户端成功完成签名。逐层验证比反复重新部署同一把公钥更有效。
先确认 agent 暴露的指纹和公钥:
ssh-add -l
ssh-add -L
macOS 不一定自带 ssh-copy-id,先检查:
command -v ssh-copy-id
命令存在时再部署选定的公钥文件:
ssh-copy-id -i <public-key-file> <user>@<host>
没有该命令,或者密钥只存在于 agent 中,可以通过仍然可用的密码登录、恢复密钥或管理员通道追加公钥。下面的命令会追加 agent 暴露的全部公钥,只适用于已经确认其中每一把都应该部署的环境:
SSH_AUTH_SOCK="<current-agent-socket>" ssh-add -L | ssh <user>@<host> 'umask 077; mkdir -p ~/.ssh; cat >> ~/.ssh/authorized_keys'
部署后不要立即关闭当前会话。保留现有连接,在另一个终端测试新密钥,确认成功后再考虑关闭密码登录或撤销旧公钥,这是切换 SSH 密钥时避免把自己锁在服务器外的必要顺序。
随后在服务器上核对目标公钥:
ssh-keygen -lf ~/.ssh/authorized_keys
客户端也可以计算本地公钥指纹:
ssh-keygen -lf <public-key-file>
两边显示的同一把密钥指纹必须一致。如果服务器拒绝公钥,再检查目标用户名、home 目录、authorized_keys 的实际位置、文件所有者和权限是否符合服务器端 OpenSSH 的要求:
ls -ld ~ ~/.ssh ~/.ssh/authorized_keys
如果客户端提供了公钥,服务器也接受了公钥,但签名随后失败,才需要进入签名阶段排查。服务器是否支持当前 SK 或 Secure Enclave 密钥使用的签名类型,要依据服务器端 OpenSSH 文档判断;SSH 是协议,而 OpenSSH 是具体实现,不能假设所有旧服务器都支持同一种公钥和签名算法。
SSH 密钥指纹和服务器主机指纹是两回事。~/.ssh/known_hosts 记录的是服务器身份,authorized_keys 记录的是允许登录的用户公钥:
ssh-keygen -lf ~/.ssh/known_hosts
不要通过关闭主机密钥检查来掩盖公钥、用户或签名阶段的问题。
sign_and_send_pubkey 失败时该查密钥还是服务器
如果错误是:
sign_and_send_pubkey: signing failed: agent refused operation
这通常说明客户端已经找到公钥,但 agent 没有完成这次签名。继续向服务器重发同一把公钥不会解决密钥访问状态或交互授权问题。
先确认 ssh 与 ssh-add 仍在使用同一个 agent:
SSH_AUTH_SOCK="<current-agent-socket>" ssh-add -l
然后检查所选工具是否正在运行、密钥是否处于可访问状态,以及当前签名是否需要 Touch ID、钥匙串授权、应用确认或其他交互。SeKey 明确把 Touch ID 与 Secure Enclave 用于 SSH 身份验证,但其他实现是否采用相同交互必须按各自官方文档判断。
ssh-add -l 能列出指纹,不代表 agent 此刻一定能签名;列表状态和签名能力需要通过实际 SSH 请求分别验证。若客户端使用 -vvv,日志能够显示公钥提供、服务器接受和签名尝试的阶段,但不要把完整日志直接贴到公开 issue。日志通常不会显示私钥,却可能暴露用户名、内部主机名、路径和网络拓扑。
Secretive 的调试同样应遵循最小披露原则,只收集与签名失败有关的部分,并隐藏用户名、主机名、路径、密钥注释及其他环境信息。 不应把导出私钥、关闭访问控制或复制应用数据当作排障捷径。
怎么证明连接用的确实是 Secure Enclave 里的那把密钥
连接成功本身不能证明使用了预期的 enclave 密钥,因为 OpenSSH 可能先使用了 ~/.ssh 中的普通私钥。验证时需要同时检查生效 agent、客户端指纹、服务器端指纹和认证阶段:
ssh -G <host> | grep -E '^(identityagent|identityfile) '
SSH_AUTH_SOCK="<current-agent-socket>" ssh-add -l
ssh -v <user>@<host>
详细日志中要确认客户端提供的是哪把公钥、服务器是否接受该公钥,以及签名是否完成。具体日志文字会随 OpenSSH 版本变化,不应依赖一段固定输出判断。
客户端公钥指纹应与服务器 authorized_keys 中的目标指纹对应:
ssh-keygen -lf <public-key-file>
ssh-keygen -lf ~/.ssh/authorized_keys
如果 ~/.ssh 中仍有同一公钥对应的普通私钥,ssh 可能优先使用文件密钥。仅凭连接成功无法证明使用了 Secure Enclave;此时应通过 IdentityAgent、候选 IdentityFile 和详细认证日志共同确认。
ssh-add -L 只能证明 agent 暴露了哪些公钥,不能证明这些公钥背后采用了哪一种密钥保护实现。多密钥环境最好记录“主机—用户—指纹—agent”的对应关系,避免同名密钥、重复注释或错误默认值造成误判。
CI 和无交互任务能不能继续使用这类密钥
如果当前 Secure Enclave 实现依赖用户在场完成授权,就不适合直接用于 CI、批处理、自动化部署或无法弹出交互窗口的堡垒机任务。任务偶尔在某台个人机器上成功,并不能证明它支持无人值守。
这类场景应改用组织批准的普通 SSH 密钥、集中托管的机器身份,或者明确支持机器模式的硬件方案。不要为了让任务继续运行而关闭 Touch ID、钥匙串、应用授权或其他系统安全策略。
多人环境也不应共享同一个 enclave 身份。服务器日志中的公钥指纹相同,就无法区分具体操作者,也会让单独撤销某一人的访问变得困难。人员账号、机器人账号和恢复密钥应分别管理。
多台服务器需要不同 agent 时,按具体 Host 精确配置 IdentityAgent,不要让宽泛的 Host * 意外改变全部连接行为。
Secure Enclave 私钥到底能备份什么
公钥、指纹和服务器清单都应该备份,但它们不能替代不可导出的 enclave 私钥。
| 对象 | 能否作为可迁移私钥备份 | 能解决什么问题 | 不能解决什么问题 |
|---|---|---|---|
| SSH 公钥 | 可以 | 核对、重新部署和识别密钥 | 没有私钥不能完成认证 |
| Secure Enclave 私钥 | 按本文方案不能常规导出 | 在受保护环境中签名 | 设备损坏后不能直接搬走 |
| 独立恢复密钥 | 按组织策略保存 | 设备丢失时重新授权 | 不能替代日常使用的 enclave 密钥 |
| Keychain、应用容器或系统备份 | 取决于工具和版本 | 可能保留加密数据 | 不能未经验证就视为可迁移私钥备份 |
ssh-add -L 导出的仍然只是公钥:
ssh-add -L
Secretive 利用 Secure Enclave 保护 SSH 密钥,并阻止常规私钥导出,因此不能把其密钥文件简单复制到另一台 Mac 后继续使用。 Time Machine、iCloud 或 Keychain 中即使存在相关数据,也不等于新设备拥有解密所需的硬件绑定密钥。是否可恢复必须按当前工具版本和官方说明验证,不能从“有备份文件”直接推断出“私钥可以迁移”。
应预先创建一把独立、带口令的 Ed25519 恢复密钥:
ssh-keygen -t ed25519 -f <offline-path>/recovery_ed25519 -C "recovery-<role>"
生成后按组织策略设置口令并离线保存。恢复密钥不是日常登录密钥,也不应放在 CI、共享账号或所有开发机都能读取的位置。
同时归档目标公钥、指纹、负责人、服务器清单和创建日期。日常轮换也应采用“新旧公钥短暂并存、验证新连接、再撤销旧公钥”的顺序。若某个具体版本官方提供了加密导出或恢复功能,需要单独记录其适用版本、恢复步骤和限制,不能把该功能推广成所有 Secure Enclave 方案都支持。
Mac 丢失后怎样恢复访问并撤销旧公钥
设备丢失或 Secure Enclave 损坏后,不能从公钥恢复私钥。恢复访问必须依赖此前保留的恢复密钥、密码通道或管理员权限。
换机顺序应固定为:
- 使用恢复密钥、密码通道或管理员权限取得服务器访问权。
- 在新 Mac 上创建或导入新的 Secure Enclave 密钥。
- 将新公钥与旧公钥暂时并存部署。
- 核对新指纹,并从独立终端验证连接。
- 覆盖所有目标服务器后,再删除服务器上的旧公钥。
- 更新密钥清单、负责人和恢复材料。
团队环境还要统计每台服务器是否仍保留旧公钥,避免只验证一台就批量撤销其他服务器上的旧公钥。如果旧设备无法取得且没有预先保留的恢复密钥或管理员通道,就没有办法从 enclave 公钥重建私钥;此时只能依靠仍获授权的恢复密钥或管理员通道恢复访问。












暂无评论内容