<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>SSH · XB 现场笔记</title><link>https://hk.xbcatm.com/zh/tags/ssh/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://hk.xbcatm.com/zh/tags/ssh/index.xml" rel="self" type="application/rss+xml"/><item><title>SSH 代理转发的权限边界</title><link>https://hk.xbcatm.com/zh/posts/linux-ssh-agent-forwarding/</link><pubDate>Wed, 09 Feb 2028 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/posts/linux-ssh-agent-forwarding/</guid><description>Agent 转发把私钥材料留在本地，却允许远端主机请求用于认证的签名。</description><content:encoded>&lt;p&gt;agent 转发并不等于 agent 本身。本地 agent 可以替客户端完成认证操作，而不把私钥交给服务器；转发则进一步在远端开放一个套接字，让那边的程序也能请求这项服务。因此，「私钥材料留在本地」和「身份认证能力只留在本地」是两个不同的承诺。&lt;/p&gt;
&lt;p&gt;选择堡垒机访问方式时，这个区别尤其重要。如果只是要登录内网机器，通常用 &lt;code&gt;ProxyJump&lt;/code&gt; 并关闭 agent 转发就够了。只有程序确实需要在远端发起认证，例如一条经过明确授权的后续 SSH 连接，才需要考虑把 agent 的使用能力交过去。不要把网络可达性和身份委托混成一个开关。&lt;/p&gt;
&lt;h2 id="远端拿到的是能力"&gt;远端拿到的是能力&lt;/h2&gt;
&lt;p&gt;转发出来的套接字通常通过远端的 &lt;code&gt;SSH_AUTH_SOCK&lt;/code&gt; 暴露。能访问它的进程，包括拥有足够权限的本机攻击者，都可以向原始 agent 请求操作。这不意味着服务器上的任何普通账户都有权限，更不意味着私钥文件已经被复制过去；风险来自可以调用身份，而不是必须先偷到密钥。&lt;/p&gt;
&lt;p&gt;一把没有额外约束的已加载密钥，在转发通道仍然可用时，可能被拿去完成你没有打算批准的登录。密钥的存活期、逐次确认、目标限制，以及目标服务器是否接受这个身份，都会改变请求能否成功。把它说成「完整拥有私钥」不够准确，把它说成「只是一只无害套接字」同样会误导操作判断。&lt;/p&gt;
&lt;p&gt;旧版本曾把一枚没有解释来源的八十八字节签名当作证据。单看长度，既不能证明实验实现了 OpenSSH agent 协议，也不能证明一次经过转发的认证成功。下面改用真正的 SSH 客户端，验证一个范围明确、读者可以复现的性质：最终生效的转发选项。这个检查不冒充真实网络集成测试。&lt;/p&gt;
&lt;h2 id="转发由生效配置决定"&gt;转发由生效配置决定&lt;/h2&gt;
&lt;p&gt;命令里没有 &lt;code&gt;-A&lt;/code&gt;，并不代表关闭了转发。匹配的 &lt;code&gt;Host&lt;/code&gt; 段、被包含的配置文件，或者系统配置，都可能已经把它打开。反过来，即使文件写着 &lt;code&gt;ForwardAgent no&lt;/code&gt;，命令行上的 &lt;code&gt;-A&lt;/code&gt; 仍会显式启用转发。需要关闭时，应当使用 &lt;code&gt;-a&lt;/code&gt;，或者没有相互矛盾选项的 &lt;code&gt;-o ForwardAgent=no&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ProxyJump&lt;/code&gt; 提供到目标的 TCP 路径，并不会顺便重置目标连接的转发选项。目标 SSH 连接及其认证仍从本地发起，所以单纯借道跳板机不需要把 agent 暴露过去。目标和跳板机的配置也不一定一样，应分别检查相应别名，不要把某一条 &lt;code&gt;ssh -G&lt;/code&gt; 的输出理解成整条路径的所有设置。&lt;/p&gt;
&lt;p&gt;如果已经进入远端 shell，再运行一次 SSH，那台机器上的客户端会读取它自己的配置。判断哪一跳继续转发，要看每次调用的有效设置，不能数历史命令里出现过几次 &lt;code&gt;-A&lt;/code&gt;。排查已有连接时还要留意连接复用：打印当前配置描述的是这次拟议调用，不能反推所有既有主连接的运行状态。&lt;/p&gt;
&lt;h2 id="缩小委托范围"&gt;缩小委托范围&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ssh-add -c&lt;/code&gt; 通过 askpass 程序请求使用确认，不能只因为请求方没有终端，就断言确认必然失败。&lt;code&gt;ssh-add -t&lt;/code&gt; 可以限制身份留在 agent 中的时间。自 OpenSSH 8.9 起提供的目标约束依据已知主机密钥识别主机，经过转发路径使用时还需要客户端和服务端配合支持。协作路径上的每一跳都须获准；恶意节点仍可用其他工具再次中继套接字，但签名仍受获准目的地限制，不能把路径约束当作绝对防中继机制。&lt;/p&gt;
&lt;p&gt;服务端设置 &lt;code&gt;AllowAgentForwarding no&lt;/code&gt;，可以关闭 OpenSSH 自带的转发设施，但不能阻止拥有不受限制 shell 的用户另行搭建转发器。这个限制有配置卫生价值，却不能单独隔离一个恶意 shell 用户。仍然应该把服务器信任关系和可执行程序的权限纳入判断。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;堡垒机被攻陷后，攻击者可能在转发通道和相应身份仍可用的窗口里滥用 agent。在磁盘上换掉私钥文件，并不会自动卸载 agent 内存里的旧身份。处置时应终止转发连接、移除相关的已加载身份；如果风险需要，再撤销服务器接受该身份的授权。已经建立的远程会话不会因此自动消失，需要另外处理。&lt;/p&gt;
&lt;p&gt;另一个常见问题是把 &lt;code&gt;ForwardAgent yes&lt;/code&gt; 放进 &lt;code&gt;Host *&lt;/code&gt; 后忘记它。最好只为确实需要的主机设置例外，并让用于转发的 agent 只包含必要身份。给某次登录指定 &lt;code&gt;IdentityFile&lt;/code&gt;，并不会把整个转发 agent 过滤成只有那一把钥匙。登录时选什么身份，和远端能向 agent 请求什么，是两个不同层次的问题。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;
&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/posts-linux-ssh-agent-forwarding.detail.zh.svg" alt="路由阶梯与本地代理、远端套接字的双向能力图分开；四行保留配置为 no、yes、yes、no，随后说明目的地与中继限制。" width="560" height="2524"&gt;
&lt;figcaption&gt;ProxyJump 提供路径，代理转发暴露签名能力。生效配置与协作式目的地限制须分别检查。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;下面的一次性配置打印四项设置，供人工比较，不连接网络或读取私钥。代码没有值断言，也没有失败与清理陷阱；示例域名仅用于配置匹配：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Host *\n ForwardAgent no\n&amp;#39;&lt;/span&gt; &amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh -G -F &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; target.invalid &lt;span class="p"&gt;|&lt;/span&gt; grep &lt;span class="s1"&gt;&amp;#39;^forwardagent &amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh -G -F &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -A target.invalid &lt;span class="p"&gt;|&lt;/span&gt; grep &lt;span class="s1"&gt;&amp;#39;^forwardagent &amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Host *\n ForwardAgent yes\n&amp;#39;&lt;/span&gt; &amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh -G -F &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -J bastion.invalid target.invalid &lt;span class="p"&gt;|&lt;/span&gt; grep &lt;span class="s1"&gt;&amp;#39;^forwardagent &amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh -G -F &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -a -J bastion.invalid target.invalid &lt;span class="p"&gt;|&lt;/span&gt; grep &lt;span class="s1"&gt;&amp;#39;^forwardagent &amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;保留的 OpenSSH 10.0p2 记录报告顺序为 no、yes、yes、no；本轮没有重新执行。这验证了选项处理，不验证套接字权限、签名操作、目标约束，也没有建立真实的多跳连接。检查可信配置时，可以查看 &lt;code&gt;ssh -G destination&lt;/code&gt; 和各跳板别名；但 Match exec 求值可执行本地命令，主机名规范化也可能触发 DNS，不能把任意配置检查都当作无副作用。每一个 yes 都应该能回答两个问题：为什么需要转发，以及接收方为什么值得信任。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh.1"&gt;OpenBSD：ssh&lt;/a&gt;，命令行转发开关、跳板连接与配置检查，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh_config.5"&gt;OpenBSD：ssh_config&lt;/a&gt;，ForwardAgent、ProxyJump 与配置优先级，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh-agent.1"&gt;OpenBSD：ssh-agent&lt;/a&gt;，agent 套接字与无需传输私钥的认证方式，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh-add.1"&gt;OpenBSD：ssh-add&lt;/a&gt;，askpass 确认、存活期与目标约束的支持条件，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/sshd_config.5"&gt;OpenBSD：sshd_config&lt;/a&gt;，AllowAgentForwarding 与 shell 访问下的限制，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>ssh-agent 在会话期间替你记住已解密的密钥</title><link>https://hk.xbcatm.com/zh/posts/ssh-agent-holds-keys-in-memory/</link><pubDate>Sat, 27 Feb 2027 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/posts/ssh-agent-holds-keys-in-memory/</guid><description>SSH 代理通过受保护的套接字委托签名；身份存活期、逐次确认、目的地约束与服务端授权仍是不同控制。</description><content:encoded>&lt;p&gt;SSH 代理为公钥认证提供签名操作。对于普通软件密钥，加载加密私钥时会解锁相关材料，此后代理便可以使用它，不必每次连接都重新询问文件口令。正常代理协议向客户端返回公钥身份和签名，而不是导出私钥。硬件支持的身份有所不同：私钥可以一直保留在认证器里，并要求触摸或用户验证，不能把所有代理身份都描述成完整私钥必然存放在代理内存中。&lt;/p&gt;
&lt;h2 id="套接字提供的是服务访问能力"&gt;套接字提供的是服务访问能力&lt;/h2&gt;
&lt;p&gt;OpenSSH 通常通过 SSH_AUTH_SOCK 查找代理，但客户端配置中的 IdentityAgent 可以选择另一个套接字，也可以关闭代理使用。知道路径并不等于有连接权限。套接字和父目录的权限都重要；root 以及与用户同身份运行的其他进程，则是需要考虑的威胁。环境变量本身不能代替操作系统隔离，隐藏路径也不是可靠的保护边界。&lt;/p&gt;
&lt;p&gt;能够访问代理，并不保证每个签名请求都会成功。密钥约束、确认要求、硬件在场和协议限制都可能拒绝请求。即使签名成功，远端服务器也不必接受这把密钥或授予权限。因此，本地签名能力与服务端授权决定应分开观察。诊断“代理里有钥匙但登录失败”时，不能只停留在身份列表这一层。&lt;/p&gt;
&lt;h2 id="同时限制身份与委托"&gt;同时限制身份与委托&lt;/h2&gt;
&lt;p&gt;ssh-add 加载身份，并在需要时询问文件口令。但 ssh 的 AddKeysToAgent 等配置路径也能加载身份，不能说只有 ssh-add 才可能解锁。代理避免反复读取加密私钥文件，却不保证交换空间、转储、备份或其他进程中绝不存在私钥材料。操作系统与密钥存储策略仍然需要独立审阅，代理不是整台机器的保密证明。&lt;/p&gt;
&lt;p&gt;-t 为添加的身份设置存活期限。到期后身份会被移除，这个参数不只是提醒；代理也可以提供默认期限；两种期限都没有时，普通身份才可能一直留到显式删除或代理退出。移走、删除或者替换磁盘文件，并不会自动替换代理里已经加载的副本。轮换时应检查实际使用的指纹，而不是只确认文件名已经改变。&lt;/p&gt;
&lt;p&gt;-c 通过 ssh-askpass 要求确认。决定是否批准的是辅助程序是否成功退出，而不是远端主机有没有终端。需要验证本地确认机制确实能工作，并拒绝意外请求。确认可以减少无人值守的使用，但仍需理解提示所代表的主机和操作，不能看到弹窗就习惯性通过，也不能把没有远端终端当作必然拒绝的证据。&lt;/p&gt;
&lt;p&gt;OpenSSH 8.9 引入的目的地约束使用主机密钥进一步限制允许的认证路径。经过转发时，客户端与服务端都需要支持相关协议，已知主机信息也必须正确识别目标。恶意中转节点仍可绕开协作式 SSH 再次中继套接字，但签名仍受获准目的地限制。这不是防火墙；依赖约束前应核对兼容性、保持更新，并理解主机密钥变化对路径的影响。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;代理转发允许远端在转发通道存在期间中继签名请求。那里的管理员或被入侵进程可能使用这份能力，而不需要提取私钥。没有必要时应避免转发，并查看客户端最终生效的配置；命令行没有 -A，不足以证明配置文件没有开启它。风险在于签名委托，不在于私钥文件一定已经复制到远端。&lt;/p&gt;
&lt;p&gt;从代理删除身份，并不会撤销已经被复制的私钥、删除服务器 authorized_keys 中的条目，也不保证关闭已建立的 SSH 会话。反过来，服务端撤销授权可能阻止新登录，即使代理仍保存这把钥匙。恢复计划应分别处理受影响的副本、授权记录和活动会话，而不是把其中一个动作误当成全部撤销。&lt;/p&gt;
&lt;p&gt;现代 OpenSSH 代理还限制远端加载提供方和部分 FIDO 签名用途，但识别远端客户端依赖协作式 SSH 转发；这些机制可减少把转发认证器用于 SSH 之外的特定风险。这些保护不是公开套接字或随意放宽选项的理由。不同版本和平台集成的默认值、套接字位置可能不同；排查时应阅读当前实现的文档，不能把某次输出的路径和权限数字当成永远成立的协议常量。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;
&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/posts-ssh-agent-holds-keys-in-memory.detail.zh.svg" alt="软件代理与硬件认证器存储面板接到受保护签名服务说明；服务端授权、签名约束和显式或 none 的 IdentityAgent 配置分别呈现。" width="560" height="2084"&gt;
&lt;figcaption&gt;套接字访问可委托签名，而私钥存储与服务端授权分开。保留夹具只检查配置选择。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;保存为 &lt;code&gt;agent_config.py&lt;/code&gt;，在安装了 OpenSSH 的环境执行 &lt;code&gt;python3 agent_config.py&lt;/code&gt;。实验创建临时配置，再通过 ssh -G 求出最终配置，不启动代理、不读取私钥、不建立连接，也不修改现有代理。合成环境变量里的套接字与显式配置刻意不同，两条路径都不需要存在。本轮源码复核没有执行示例，也没有接触真实认证服务。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;tempfile&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;agent-config-&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;alternate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;alternate.sock&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;config&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&amp;#39;Host chosen-agent
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; HostName example.invalid
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; IdentityAgent &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;alternate&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;Host no-agent
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; HostName example.invalid
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; IdentityAgent none
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PATH&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;SSH_AUTH_SOCK&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;environment.sock&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;chosen-agent&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alternate&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;no-agent&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;none&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ssh&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-F&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;identityagent&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;unexpected IdentityAgent&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;identityagent=&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;identityagent&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;alternate.sock&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;environment.sock&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;the fixture must not create an agent socket&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PASS: explicit socket overrides environment; none disables agent use&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第一条结果选择显式配置的替代套接字，即使 SSH_AUTH_SOCK 指向别处；第二条选择 none。PASS 只确认这些配置规则。ssh -G 不验证套接字可达性、签名、到期、确认或远端登录。IdentityAgent none 关闭的是该次调用的代理使用，并不关闭文件密钥或其他认证机制，不能据此声称这个连接绝不可能认证成功。&lt;/p&gt;
&lt;p&gt;若需要验证加载、存活期和确认，应另开得到授权的测试会话，并使用可丢弃的身份。不要为了重现文章，就对日常共用代理执行 ssh-add -D：它会删除全部已加载身份，可能中断其他工作。应把观察配置的实验，与改变凭据或真实会话的操作清楚分开，依据具体测试目标逐项取得证据。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh-agent.1"&gt;OpenBSD: ssh-agent&lt;/a&gt;，签名服务、套接字访问、提供方和 FIDO 限制，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh-add.1"&gt;OpenBSD: ssh-add&lt;/a&gt;，存活期、确认、删除与目的地约束，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh_config.5"&gt;OpenBSD: ssh_config&lt;/a&gt;，IdentityAgent、AddKeysToAgent 与转发配置，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.openssh.com/agent-restrict.html"&gt;OpenSSH: agent restriction&lt;/a&gt;，主机密钥绑定与转发兼容性，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>SSH 主机密钥是部署记录的一部分</title><link>https://hk.xbcatm.com/zh/notes/ssh-host-keys-are-part-of-the-deployment-record/</link><pubDate>Tue, 28 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/ssh-host-keys-are-part-of-the-deployment-record/</guid><description>让首次接触保持显式，并拒绝未预期的主机密钥变化。</description><content:encoded>&lt;p&gt;在公钥认证中，客户端证明自己持有私钥，但它并不告诉客户端到达的是哪台服务器。服务器的主机密钥与本地 &lt;code&gt;known_hosts&lt;/code&gt; 数据库，提供身份检查的后一半。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/ssh-host-key.detail.zh.svg" alt="客户端私钥认证与服务器主机身份核验是不同检查。安装前，将候选指纹与独立可信的所有者或控制台指纹比较。yes 拒绝未知或变化的密钥；accept-new 接受首次见到的密钥但没有独立身份保证，仍拒绝变化。复核轮换、保存私有运维记录，ssh -G 不等于握手测试。" width="720" height="2300"&gt;&lt;figcaption&gt;SSH 服务器身份依赖观察到的主机密钥与独立可信参考之间的比较。严格检查拒绝未知或变化的密钥。首次使用时信任与网络密钥扫描，都不能独立认证首次接触的服务器。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="把首次接触与轮换分开"&gt;把首次接触与轮换分开&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;StrictHostKeyChecking=yes&lt;/code&gt; 下，SSH 拒绝未知主机与变化的密钥。对于一个固定生产端点，只要期望密钥通过可信渠道安装，这就是最安全的默认值。&lt;code&gt;accept-new&lt;/code&gt; 是面向预期会出现新主机的、更窄的方便选项：它接受一个从未见过的密钥，但仍拒绝被更改的密钥。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-sshconfig" data-lang="sshconfig"&gt;Host production-site
HostName www.example.test
User deploy
IdentityFile ~/.ssh/site_ed25519
StrictHostKeyChecking yes
UserKnownHostsFile ~/.ssh/known_hosts
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;不要通过给部署脚本加 &lt;code&gt;StrictHostKeyChecking=no&lt;/code&gt; 来&amp;quot;修复&amp;quot;警告。它会在客户端限制条件下允许连接到密钥已变化的主机；警告可能仍被打印，但原本遇到身份问题就拒绝的检查已被削弱。如果主机被重建，请记录替换后的指纹，并复核旧密钥为何不再有效。&lt;/p&gt;
&lt;h2 id="让指纹可复核"&gt;让指纹可复核&lt;/h2&gt;
&lt;p&gt;在首次连接之前，从主机所有者或控制台获取指纹，再与客户端看到的值比较。密钥变更应有工单、维护说明或其他他人可检视的解释。&lt;/p&gt;
&lt;p&gt;同一规则适用于自动化：known-hosts 文件是带有安全后果的配置，不是可丢弃的缓存数据。把它与部署记录一起备份，但不要把私钥或复制的凭据随站点制品一起公开。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;accept-new&lt;/code&gt; 是首次使用时信任，并不能独立证明第一次连接的服务器真实可信。&lt;code&gt;ssh-keyscan&lt;/code&gt; 能采集候选公钥，但通过同一条不可信网络取回它，并不能认证它。安装前先将指纹与独立可信渠道取得的值比较。&lt;/p&gt;
&lt;p&gt;把上面的配置单独保存为 &lt;code&gt;deploy.conf&lt;/code&gt;，在不连接服务器的情况下检查有效配置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh -G -F ./deploy.conf production-site &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; grep -E &lt;span class="s1"&gt;&amp;#39;^(hostname|user|stricthostkeychecking|userknownhostsfile) &amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;结果应显示配置中的主机、用户、known-hosts 路径与严格检查（客户端输出可能为 &lt;code&gt;true&lt;/code&gt; 或 &lt;code&gt;yes&lt;/code&gt;）。这只解析配置，并不测试真实握手。非标准端口的 known-hosts 条目通常采用 &lt;code&gt;[hostname]:port&lt;/code&gt;。将主机公钥指纹保存在私有运维发布记录中，不要随公共网站发布；即使主机公钥不是秘密，主机名也可能暴露内部拓扑。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh_config"&gt;OpenBSD &lt;code&gt;ssh_config&lt;/code&gt;&lt;/a&gt;，&lt;code&gt;StrictHostKeyChecking&lt;/code&gt; 与 &lt;code&gt;UserKnownHostsFile&lt;/code&gt;，已核验 2026-07-28。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh-keygen"&gt;OpenSSH &lt;code&gt;ssh-keygen&lt;/code&gt; 手册&lt;/a&gt;，指纹检视，已核验 2026-07-28。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh-keyscan"&gt;OpenSSH ssh-keyscan&lt;/a&gt;，未经核验采集的密钥不能防止中间人攻击，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh"&gt;OpenSSH ssh&lt;/a&gt;，-G 不建立会话，打印求值后的客户端配置，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>SSH 密钥卫生，主要是限制密钥能干什么</title><link>https://hk.xbcatm.com/zh/posts/ssh-key-hygiene-basics/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/posts/ssh-key-hygiene-basics/</guid><description>按指纹盘点 SSH 访问，执行服务端权限范围，区分 RSA 密钥与 SHA-1 签名，并核对审计实际证明的内容。</description><content:encoded>&lt;p&gt;SSH 密钥卫生首先要回答：哪份凭据授予什么访问，谁负责，以及如何结束这份授权。密钥注释方便维护清单，却不是经过认证的所有者记录，更不是授权规则。应把指纹与负责人、目标账号、允许操作和移除计划对应起来。生成现代密钥只是开始，不能用一条生成命令代替完整的访问生命周期管理。&lt;/p&gt;
&lt;h2 id="限定服务端权限"&gt;限定服务端权限&lt;/h2&gt;
&lt;p&gt;authorized_keys 中的公钥通常用于授权访问某个账号，同时还受服务器其他控制约束。自动化任务应结合权限有限的账号和经过选择的选项。强制命令能限制执行的操作，from 可以限制来源地址，但二者都不会自动把进程关在某个工作目录，也不会让一个会拼接不可信命令的包装脚本变得安全。&lt;/p&gt;
&lt;p&gt;restrict 禁用转发、伪终端分配和 ~/.ssh/rc，但显式搭配选项可以重新启用个别功能；它本身并不禁止 shell 或命令执行。因此，无人值守任务通常还需要谨慎实现的强制命令。no-agent-forwarding 只禁止代理转发，不能证明密钥没有普通 shell 权限。principals 对可信证书颁发机构条目有专门含义，并不是加在任意普通公钥前面就能形成通用身份约束。&lt;/p&gt;
&lt;p&gt;还要审阅最终生效的服务端配置和其他授权来源。AuthorizedKeysCommand、证书颁发机构、账号权限和 Match 规则，都会让结果超出单个文件的内容。清单工具看到选项名时应报告观察事实，不能直接给出“已限制，所以安全”的结论。来源限制也取决于服务器实际看到的地址，包括代理、跳板机或地址转换后的来源，不能只按客户端想象的拓扑解释。&lt;/p&gt;
&lt;h2 id="区分密钥类型与签名算法"&gt;区分密钥类型与签名算法&lt;/h2&gt;
&lt;p&gt;RSA 公钥行仍使用 ssh-rsa 类型标签，即使认证时采用 rsa-sha2-256 或 rsa-sha2-512 签名。OpenSSH 8.8 默认禁用的是使用 SHA-1 的 ssh-rsa 签名方案，而不是所有 RSA 密钥。应检查密钥长度与实际支持的签名算法，不能只凭一行的第一个词就把所有 RSA 条目判成过时。DSA、弱参数和不受支持的配置，需要各自的迁移计划。&lt;/p&gt;
&lt;p&gt;新凭据应选择当前环境支持的算法和合适的保护。口令可以保护加密私钥文件，避免文件被复制后立刻就能使用；硬件密钥则提供不同的边界。它们都不能代替安全工作站、受控代理访问和服务端权限限制。无法交互输入口令的自动化，需要明确的凭据保管和更新设计，不能简单地把“没有提示”当成设计完成。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;只追加新公钥的轮换，会让旧凭据继续得到授权。应从所有相关来源移除退役条目，验证替代凭据，并单独处理现有会话。普通裸公钥不会自动携带到期机制；需要通过支持的服务端选项、SSH 证书或明确的生命周期操作执行过期。在注释里写一个日期不会产生任何强制效果，负责人离职也不会自动让那一行失效。&lt;/p&gt;
&lt;p&gt;权限数字需要准确解释。私钥应限制为所有者访问，常见模式是 0600；私有目录常见模式是 0700。authorized_keys 的公钥内容并不是秘密，在所有权和写保护满足检查时，0644 也可能被服务器接受。采用更严格的 0600 策略合理，却不能声称每个其他模式都证明已经泄露。ACL、父目录和平台规则仍需分别核对，简单的模式检查并不涵盖全部访问路径。&lt;/p&gt;
&lt;p&gt;按空白直接切分 authorized_keys，会破坏含有带引号空格的命令。转义引号和逗号又会进一步增加自制解析器误读的机会。可使用 OpenSSH 检查指纹，再独立审阅选项语义。指纹一致只能说明找到的是哪把公钥，不能证明限制真的生效，也不能证明对应私钥设置了口令。自动审计输出应避免给出超出这些证据的保证。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;
&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/posts-ssh-key-hygiene-basics.detail.zh.svg" alt="访问清单后列服务端限制注意事项，独立对比 RSA 密钥类型与 SHA-2 签名，再展示旧授权退役及本地夹具证据边界。" width="560" height="2102"&gt;
&lt;figcaption&gt;清单识别密钥，实际授权定义它能在哪里使用。密钥标签、协商签名与退役是独立检查。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;保存为 &lt;code&gt;ssh_hygiene.py&lt;/code&gt;，在安装了 OpenSSH 的环境执行 &lt;code&gt;python3 ssh_hygiene.py&lt;/code&gt;。程序在临时目录生成可丢弃的 Ed25519 和三千零七十二位 RSA 密钥。空口令仅用于这个隔离实验；脚本不会读取或更改真实 SSH 文件，不连接任何主机，也不加载代理。正常退出会尝试清理生成文件；实验不会向任何服务器添加授权。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;stat&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;tempfile&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PATH&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defpath&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ssh-hygiene-&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Throwaway, passphrase-less keys for this isolated fixture only.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;demo_ed25519&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;rsa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;demo_rsa&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ssh-keygen&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-q&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-t&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;ed25519&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-N&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-C&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;fixture-ed&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-f&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ed&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ssh-keygen&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-q&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-t&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;rsa&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;3072&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-N&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-C&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;fixture-rsa&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-f&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rsa&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;public&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;with_suffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;.pub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;authorized_keys&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;restrict,command=&amp;#34;printf &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;&amp;#34;two words&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;&amp;#34;&amp;#34; &amp;#39;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;public&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chmod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mo"&gt;0o600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ssh-keygen&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-l&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-f&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ssh-keygen&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-l&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-f&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;with_suffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;.pub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;quoted options changed the key fingerprint&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;rsa_public&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rsa&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;with_suffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;.pub&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rsa_public&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;ssh-rsa&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;unexpected RSA public key type&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;signatures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ssh&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;-Q&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;key-sig&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;rsa-sha2-256&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;rsa-sha2-512&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;signatures&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;client does not advertise RSA SHA-2 signatures&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;modes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;oct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S_IMODE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_mode&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rsa&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S_IMODE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_mode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mo"&gt;0o077&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rsa&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;fixture private key permissions are too broad&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;quoted authorized_keys fingerprint:&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;RSA key label:&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rsa_public&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;; SHA-2 signature algorithms supported&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;fixture modes:&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;modes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PASS: native key parsing, fingerprint agreement, RSA distinction, private modes&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;预期看到带引号命令后仍匹配的 Ed25519 指纹、ssh-rsa 公钥标签以及本机支持 RSA SHA-2 签名的结果、所有者专用的私钥权限，最后打印 PASS。指纹和临时路径每次都会变化。上下文管理器会在退出时尝试删除生成文件，但不提供安全擦除保证。输出不包含私钥内容，本轮源码复核也没有生成密钥或重跑夹具。这个过程证明特定输入可以被原生工具识别，而不是证明任意配置都会被服务器接受。&lt;/p&gt;
&lt;p&gt;客户端算法查询只说明本地能力，不说明某台服务器实际允许什么。同样，ssh-keygen 读取指纹也不是强制命令或全部选项的端到端测试。真正审计时，应把每个指纹映射到负责人，在隔离环境验证服务端授权和命令行为，并清楚区分已观察事实与尚未验证的控制。需要修改授权文件时，还应保留受控恢复方式，避免把清理操作变成意外锁定。&lt;/p&gt;
&lt;h2 id="延伸阅读"&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/zh/posts/linux-ssh-agent-forwarding/"&gt;SSH 代理转发委托签名&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/zh/posts/ssh-agent-holds-keys-in-memory/"&gt;ssh-agent 在会话期间替你记住已解密的密钥&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/sshd"&gt;OpenBSD: sshd&lt;/a&gt;，authorized_keys 格式、选项与文件所有权检查，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.openssh.com/txt/release-8.8"&gt;OpenSSH 8.8 发布说明&lt;/a&gt;，RSA 密钥与弃用的 SHA-1 签名的区别，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh-keygen.1"&gt;OpenBSD: ssh-keygen&lt;/a&gt;，密钥生成与指纹检查，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://csrc.nist.gov/pubs/ir/7966/final"&gt;NIST IR 7966&lt;/a&gt;，SSH 访问清单与生命周期管理，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>