<?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 Field Notes</title><link>https://hk.xbcatm.com/tags/ssh/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://hk.xbcatm.com/tags/ssh/index.xml" rel="self" type="application/rss+xml"/><item><title>Forwarding an SSH agent</title><link>https://hk.xbcatm.com/posts/linux-ssh-agent-forwarding/</link><pubDate>Wed, 09 Feb 2028 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/posts/linux-ssh-agent-forwarding/</guid><description>Agent forwarding keeps private key material local, but lets a remote host request authentication signatures.</description><content:encoded>&lt;p&gt;Agent forwarding is not the agent itself. A local agent can answer authentication requests without sending the private key to the server. Forwarding additionally makes that service reachable through a socket on a remote host. Keeping key material local therefore does not mean keeping its authentication capability local.&lt;/p&gt;
&lt;p&gt;That distinction matters when choosing a route through a bastion. If the task is simply to log into a private machine, use &lt;code&gt;ProxyJump&lt;/code&gt; with forwarding disabled. Forwarding is useful when a program actually running on the remote machine needs your agent, such as a deliberately authorized second SSH connection.&lt;/p&gt;
&lt;h2 id="the-remote-gets-a-capability"&gt;The remote gets a capability&lt;/h2&gt;
&lt;p&gt;The forwarded socket is normally exposed through &lt;code&gt;SSH_AUTH_SOCK&lt;/code&gt;. Processes with access to it, including a sufficiently privileged attacker on that host, can request operations from the originating agent. This is not permission for every unrelated account on the machine, and it is not an export of the private key.&lt;/p&gt;
&lt;p&gt;An unconstrained loaded identity can nevertheless authorize unwanted logins while the forwarding channel remains available. Key lifetime, confirmation requirements, destination restrictions, and server authorization all affect what succeeds. Calling this unrestricted ownership of the key overstates the mechanism, but understating socket access is just as dangerous.&lt;/p&gt;
&lt;p&gt;An earlier version cited an unexplained 88-byte signature as evidence. Signature length alone establishes neither the OpenSSH agent protocol nor a forwarded authentication exchange. The exercise below instead tests a narrow, observable property with the actual client: its effective forwarding configuration.&lt;/p&gt;
&lt;h2 id="forwarding-follows-effective-configuration"&gt;Forwarding follows effective configuration&lt;/h2&gt;
&lt;p&gt;Omitting &lt;code&gt;-A&lt;/code&gt; does not turn forwarding off. A matching &lt;code&gt;Host&lt;/code&gt; block, an included configuration file, or system configuration can already enable it. Conversely, &lt;code&gt;-A&lt;/code&gt; explicitly enables forwarding even when a configuration file says &lt;code&gt;ForwardAgent no&lt;/code&gt;. Use &lt;code&gt;-a&lt;/code&gt; or an unambiguous &lt;code&gt;-o ForwardAgent=no&lt;/code&gt; when disabling it for a connection.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ProxyJump&lt;/code&gt; supplies a TCP route to the destination; it does not reset the destination&amp;rsquo;s forwarding option. The target SSH connection and its authentication still originate locally, so ordinary jump access needs no forwarded agent. Inspect settings for the destination and jump hosts separately, because their configuration is not necessarily identical.&lt;/p&gt;
&lt;p&gt;On a remote shell, a subsequent SSH client evaluates its own configuration. Count effective settings, not appearances of &lt;code&gt;-A&lt;/code&gt; in command history. Also check connection multiplexing when diagnosing an existing session: printing configuration describes a proposed invocation, not every property of a previously established connection.&lt;/p&gt;
&lt;h2 id="limit-the-delegation"&gt;Limit the delegation&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ssh-add -c&lt;/code&gt; requests confirmation through an askpass program; absence of a terminal alone does not explain whether approval works. &lt;code&gt;ssh-add -t&lt;/code&gt; limits an identity&amp;rsquo;s lifetime. Destination constraints, available since OpenSSH 8.9, use known host keys and require cooperating client and server support along forwarded paths. Every cooperating hop must be allowed. A hostile hop can relay the socket again using other tools, while signing remains restricted to permitted destinations.&lt;/p&gt;
&lt;p&gt;Server-side &lt;code&gt;AllowAgentForwarding no&lt;/code&gt; disables OpenSSH&amp;rsquo;s built-in facility. It does not prevent a user with unrestricted shell access from arranging another forwarder. Keep that restriction, but do not mistake it for isolation from a hostile shell user.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;A compromised bastion can misuse an accessible forwarded agent while the channel and relevant identity remain usable. Replacing the private-key file on disk does not unload the agent&amp;rsquo;s old identity. Respond by ending the forwarding connection and removing the affected loaded identity; revoke the corresponding server authorization if compromise warrants it. Already established remote sessions need separate handling.&lt;/p&gt;
&lt;p&gt;Another failure is enabling forwarding in &lt;code&gt;Host *&lt;/code&gt; and forgetting it. Prefer narrow host exceptions and an agent containing only the needed identities. Selecting &lt;code&gt;IdentityFile&lt;/code&gt; for one login is not a filter over everything a forwarded agent exposes.&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.en.svg" alt="A route ladder is separate from a two-way local-agent and remote-socket capability diagram; four retained effective-configuration rows show no, yes, yes, no before destination and relay limits." width="560" height="3080"&gt;
&lt;figcaption&gt;ProxyJump supplies a route; forwarding exposes signing capability. Effective configuration and cooperating destination restrictions need separate checks.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;This disposable configuration prints four settings for manual comparison; it opens no network connection or private key. It has no value assertions or failure/cleanup trap:&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;The retained OpenSSH 10.0p2 record reports no, yes, yes, no; this review did not rerun it. This verifies option handling, not socket permissions, signing, destination constraints, or a live multi-hop connection. For a trusted real configuration, inspect &lt;code&gt;ssh -G destination&lt;/code&gt; and jump-host aliases; evaluating Match exec can run local commands and hostname canonicalization can invoke DNS. A forwarding value of yes should have a specific reason and a trusted recipient.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh.1"&gt;OpenBSD: ssh&lt;/a&gt;, command-line forwarding controls, jump connections, and configuration inspection, checked 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, and configuration precedence, checked 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 sockets and authentication without transferring private keys, checked 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 confirmation, lifetimes, and destination-constraint requirements, checked 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 and the shell-access limitation, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>ssh-agent remembers your decrypted keys while you work</title><link>https://hk.xbcatm.com/posts/ssh-agent-holds-keys-in-memory/</link><pubDate>Sat, 27 Feb 2027 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/posts/ssh-agent-holds-keys-in-memory/</guid><description>An SSH agent delegates signing through a protected socket; identity lifetimes, confirmation, destination constraints, and server authorization remain separate controls.</description><content:encoded>&lt;p&gt;An SSH agent provides signing operations for public-key authentication. For ordinary software keys, loading an encrypted private key unlocks material that the agent can then use without asking for the file passphrase on every connection. Clients receive public identities and signatures, not an exported private key through the normal agent protocol. Hardware-backed identities are different: the authenticator can retain the private key and require touch or verification.&lt;/p&gt;
&lt;h2 id="the-socket-provides-access-to-a-service"&gt;The socket provides access to a service&lt;/h2&gt;
&lt;p&gt;OpenSSH normally discovers the agent through SSH_AUTH_SOCK, but IdentityAgent in client configuration can select a different socket or disable agent use. Knowing a pathname is not equivalent to having permission to connect. Socket and parent-directory permissions matter, while root and other processes running as the same user are important threats. Do not treat an environment variable itself as a secret that can replace operating-system isolation.&lt;/p&gt;
&lt;p&gt;Agent access is also not an unconditional promise that every signing request succeeds. Key constraints, confirmation requirements, hardware presence, and protocol restrictions can refuse requests. Even a successful signature does not force a remote server to accept that key or grant access. Separate local signing capability from the server&amp;rsquo;s authorization decision.&lt;/p&gt;
&lt;h2 id="bound-both-the-key-and-the-delegation"&gt;Bound both the key and the delegation&lt;/h2&gt;
&lt;p&gt;ssh-add loads an identity, asking for its passphrase when needed. Other configured paths, such as AddKeysToAgent in ssh, can also load identities; ssh-add is not the only possible unlock path. The agent avoids repeatedly reading the encrypted key file, but does not guarantee that every copy of private material is absent from swap, dumps, backups, or other processes.&lt;/p&gt;
&lt;p&gt;The -t option assigns a lifetime to an added identity. When that lifetime expires, the identity is removed; the option is not merely a reminder. The agent can supply a default lifetime; without either limit, an ordinary identity can remain until removed or the agent exits. Removing or replacing the disk file does not automatically replace a previously loaded copy.&lt;/p&gt;
&lt;p&gt;The -c option requires confirmation through ssh-askpass. It is the helper&amp;rsquo;s successful exit status that approves use; a missing terminal on the remote host is not the determining test. Verify the local confirmation mechanism and reject unexpected requests. Confirmation reduces unattended use but does not replace understanding which host or operation a prompt represents.&lt;/p&gt;
&lt;p&gt;Destination constraints, introduced in OpenSSH 8.9, further restrict allowed authentication paths using host keys. Forwarded use needs supporting clients and servers plus correct known-host data. A hostile hop can relay the socket again outside cooperating SSH, but signing remains limited to permitted destinations. These constraints are not a firewall; verify compatibility and keep OpenSSH patched.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;Agent forwarding lets a remote host relay signing requests while the forwarding channel exists. An administrator or compromised process there may use that capability without extracting the private key. Prefer avoiding forwarding when it is unnecessary, and review the effective client configuration rather than assuming that omitting -A proves it is disabled.&lt;/p&gt;
&lt;p&gt;Deleting an identity from the agent does not revoke a stolen private-key copy, remove a server&amp;rsquo;s authorized_keys entry, or necessarily terminate an already established SSH session. Conversely, removing server authorization can stop new logins while the agent still holds the key. A recovery plan must address each affected copy, authorization record, and active session separately.&lt;/p&gt;
&lt;p&gt;Modern OpenSSH agents restrict remote provider loading and certain FIDO signatures; identifying remote clients relies on cooperating SSH forwarding. These safeguards reduce specific misuse, including using forwarded authenticators outside SSH. They are not a reason to expose the socket broadly or enable relaxed options without a reviewed need. Defaults and socket locations can vary across versions and platform integrations.&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.en.svg" alt="Software-agent and hardware-authenticator storage panels feed a protected signing-service explanation; server authorization, signing constraints and explicit-versus-none IdentityAgent configuration remain separate." width="560" height="2568"&gt;
&lt;figcaption&gt;Socket access can delegate signing while private-key storage and server authorization remain separate. The retained fixture checks configuration selection only.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Save as &lt;code&gt;agent_config.py&lt;/code&gt; and run &lt;code&gt;python3 agent_config.py&lt;/code&gt; with OpenSSH installed. The fixture creates a temporary configuration and evaluates it using ssh -G. It neither starts an agent nor reads a private key, opens a connection, changes an existing agent, or depends on a real socket. Its synthetic socket differs from the configuration. No example ran in this source review.&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;The first result selects the explicit alternate socket despite SSH_AUTH_SOCK. The second selects none. PASS confirms these configuration expectations only: ssh -G does not test socket access, signing, expiry, confirmation, or remote authentication. IdentityAgent none disables agent use for that invocation; it does not disable authentication using files or other mechanisms.&lt;/p&gt;
&lt;p&gt;In a separate authorized test session, inspect loaded fingerprints and test lifetime and confirmation with a disposable identity. Do not run ssh-add -D against a shared everyday agent merely to reproduce an article: it removes all loaded identities and can interrupt other work. Keep the observation exercise separate from changes to credentials or live sessions.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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;, signing service, socket access, provider and FIDO restrictions, checked 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;, lifetime, confirmation, removal, and destination constraints, checked 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, and forwarding configuration, checked 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;, host-key binding and forwarding compatibility, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>SSH host keys are part of the deployment record</title><link>https://hk.xbcatm.com/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/notes/ssh-host-keys-are-part-of-the-deployment-record/</guid><description>Keep first contact explicit and reject unexpected host-key changes.</description><content:encoded>&lt;p&gt;In public-key authentication, a client proves possession of its private key; it does not tell the client which server it reached. The server&amp;rsquo;s host key and the local &lt;code&gt;known_hosts&lt;/code&gt; database provide that second half of the identity check.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/ssh-host-key.detail.en.svg" alt="Client private-key authentication and server host-key identity are separate checks. Compare the observed candidate with an independently trusted owner or console fingerprint before installation. yes refuses unknown and changed keys; accept-new accepts unseen keys without independent proof but refuses changes. Review rotations, keep private operational records, and do not treat ssh -G as a handshake." width="720" height="2300"&gt;&lt;figcaption&gt;SSH server identity depends on comparing the observed host key with a separately trusted reference. Strict checking rejects unknown or changed keys. Trust on first use and a network key scan do not independently authenticate first contact.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="separate-first-contact-from-rotation"&gt;Separate first contact from rotation&lt;/h2&gt;
&lt;p&gt;With &lt;code&gt;StrictHostKeyChecking=yes&lt;/code&gt;, SSH refuses unknown hosts and changed keys. That is the safest default for a fixed production endpoint, provided the expected key is installed through a trusted channel. &lt;code&gt;accept-new&lt;/code&gt; is a narrower convenience for a fleet where new hosts are expected: it accepts a never-seen key but still refuses a changed key.&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;Do not “fix” a warning by adding &lt;code&gt;StrictHostKeyChecking=no&lt;/code&gt; to a deployment script. It permits connections despite changed host keys, subject to client restrictions; warnings may still be printed, but the intended fail-closed identity check has been weakened. If a host is rebuilt, record the replacement fingerprint and review why the old key is no longer valid.&lt;/p&gt;
&lt;h2 id="make-the-fingerprint-reviewable"&gt;Make the fingerprint reviewable&lt;/h2&gt;
&lt;p&gt;Obtain the fingerprint from the host owner or console before the first connection, then compare it with what the client sees. A key change should have a ticket, maintenance note, or other explanation that another operator can inspect.&lt;/p&gt;
&lt;p&gt;The same rule applies to automation: the known-hosts file is configuration with security consequences, not disposable cache data. Back it up with the deployment record, but do not publish private keys or copied credentials with the site artifact.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;accept-new&lt;/code&gt; is trust on first use, not independent proof that the first server was genuine. &lt;code&gt;ssh-keyscan&lt;/code&gt; can collect a candidate public key, but fetching it over the same untrusted network does not authenticate it. Compare its fingerprint with the separately trusted value before installing it.&lt;/p&gt;
&lt;p&gt;Inspect the effective configuration without connecting, using a standalone copy of the example saved as &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;Expect the configured host, user, known-hosts path, and strict checking (&lt;code&gt;true&lt;/code&gt; or &lt;code&gt;yes&lt;/code&gt;, depending on client output). This parses configuration only; it does not test an actual handshake. For a nonstandard port, known-hosts entries normally use &lt;code&gt;[hostname]:port&lt;/code&gt;. Keep public-key fingerprints with the private operational release record, not the public website; hostnames can reveal internal topology even though host public keys are not secrets.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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; and &lt;code&gt;UserKnownHostsFile&lt;/code&gt;, checked 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; manual&lt;/a&gt;, fingerprint inspection, checked 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;, unverified collected keys do not prevent man-in-the-middle attacks, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh"&gt;OpenSSH ssh&lt;/a&gt;, -G prints evaluated client configuration without opening a session, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>SSH key hygiene is mostly limiting where a key works</title><link>https://hk.xbcatm.com/posts/ssh-key-hygiene-basics/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/posts/ssh-key-hygiene-basics/</guid><description>Inventory SSH access by fingerprint, enforce server-side scope, distinguish RSA keys from SHA-1 signatures, and verify what an audit actually proves.</description><content:encoded>&lt;p&gt;SSH key hygiene starts with knowing which credential grants which access, who owns it, and how that access ends. A key comment helps people maintain an inventory, but it is not an authenticated owner record or an authorization rule. Track fingerprints alongside accountable owners, intended accounts, permitted operations, and a removal plan. Generating a modern key is only the beginning.&lt;/p&gt;
&lt;h2 id="scope-the-server-side-permission"&gt;Scope the server-side permission&lt;/h2&gt;
&lt;p&gt;A public key in authorized_keys normally authorizes access to an account, subject to the server&amp;rsquo;s other controls. For automation, combine a narrowly privileged account with deliberately selected options. A forced command can restrict the executed operation, while from can limit source addresses. Neither magically confines the process to a working directory or makes an unsafe wrapper safe.&lt;/p&gt;
&lt;p&gt;The restrict option disables forwarding, PTY allocation, and ~/.ssh/rc, but explicit companion options can re-enable individual facilities. It does not itself forbid shell or command execution, so an unattended job often also needs a carefully implemented forced command. A no-agent-forwarding option only blocks agent forwarding; it is not evidence that the key lacks general shell access. principals has a special meaning for trusted certificate-authority entries, not arbitrary ordinary keys.&lt;/p&gt;
&lt;p&gt;Review the effective server configuration and alternative authorization sources. AuthorizedKeysCommand, certificate authorities, account privileges, and Match rules can change the result beyond one file. An inventory tool that sees an option name should report that observation, not issue a blanket “restricted and safe” verdict. Source restrictions also depend on the addresses actually seen by the server, including proxies and jump hosts.&lt;/p&gt;
&lt;h2 id="separate-key-types-from-signatures"&gt;Separate key types from signatures&lt;/h2&gt;
&lt;p&gt;An RSA public key line still uses the ssh-rsa key-type label even when authentication uses rsa-sha2-256 or rsa-sha2-512 signatures. OpenSSH 8.8 disabled the SHA-1 ssh-rsa signature scheme by default, not every RSA key. Inspect key size and actual negotiated signature support rather than labeling every RSA entry obsolete from its first word. DSA and weak or unsupported configurations require their own migration plan.&lt;/p&gt;
&lt;p&gt;For new credentials, choose supported algorithms and suitable protection for the actual environment. A passphrase protects an encrypted private-key file against immediate use after copying, while hardware-backed keys can add different protections. Neither substitutes for a secure workstation, controlled agent access, and server-side scope. Automation that cannot prompt needs an explicit credential-storage and renewal design.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;Rotation that only appends a new public key leaves the old credential authorized. Remove retired entries from every relevant authorization source, verify the replacement, and consider existing sessions separately. Ordinary raw public keys do not carry an automatic expiry. Enforce expiry through supported server options, SSH certificates, or deliberate lifecycle management; writing a date in a comment does nothing.&lt;/p&gt;
&lt;p&gt;Permissions are another boundary, but the exact claim matters. Private keys should be owner-only, commonly mode 0600; a private directory commonly uses 0700. Public authorized_keys contents are not secret, and mode 0644 can be acceptable when ownership and write protections satisfy the server&amp;rsquo;s checks. A stricter 0600 policy is reasonable without pretending every other mode proves compromise. ACLs, parent directories, and platform rules need separate inspection.&lt;/p&gt;
&lt;p&gt;Naively splitting authorized_keys on whitespace breaks commands containing quoted spaces. Escaped quotes and commas make a homemade parser especially easy to misread. Use OpenSSH to inspect key fingerprints, and review option semantics independently. A fingerprint match establishes which public key is present, not whether its restrictions are effective or its private half has a passphrase.&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.en.svg" alt="An access-inventory ledger precedes server restriction caveats, a separate RSA key-type versus SHA-2 signature comparison, and old-authorization retirement with a limited local-fixture evidence panel." width="560" height="2555"&gt;
&lt;figcaption&gt;Inventory identifies a key; real authorization defines where it works. Key labels, negotiated signatures and retirement are distinct checks.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Save as &lt;code&gt;ssh_hygiene.py&lt;/code&gt; and run &lt;code&gt;python3 ssh_hygiene.py&lt;/code&gt; with OpenSSH installed. It generates throwaway Ed25519 and 3072-bit RSA keys inside a temporary directory. Their empty passphrases are only for this disposable exercise. The script does not read or change your real SSH files, contact a host, or load an agent.&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;Expect a matching Ed25519 fingerprint despite the quoted command, an ssh-rsa public-key label alongside advertised RSA SHA-2 algorithms, owner-only private-key modes, and PASS. Fingerprints and temporary paths change on each run. The context manager attempts to remove generated files on exit; this is not secure erasure. No private key contents are printed, and this source review generated no keys or reran this fixture.&lt;/p&gt;
&lt;p&gt;The client algorithm query reports local capability, not a server&amp;rsquo;s accepted configuration. Likewise, ssh-keygen&amp;rsquo;s fingerprint parsing is not an end-to-end test of the forced command or every option. For a real audit, map each fingerprint to its owner, verify actual server authorization and command behavior in an isolated environment, and distinguish observed facts from controls that remain untested.&lt;/p&gt;
&lt;h2 id="further-reading"&gt;Further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/posts/linux-ssh-agent-forwarding/"&gt;SSH agent forwarding delegates signing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/posts/ssh-agent-holds-keys-in-memory/"&gt;ssh-agent remembers your decrypted keys while you work&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="sources"&gt;Sources&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 format, options, and file ownership checks, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.openssh.com/txt/release-8.8"&gt;OpenSSH 8.8 release notes&lt;/a&gt;, RSA keys versus deprecated SHA-1 signatures, checked 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;, key generation and fingerprint inspection, checked 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 access inventory and lifecycle management, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>