<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Ilya R.</title>
    <description>The latest articles on Forem by Ilya R. (@rivik).</description>
    <link>https://forem.com/rivik</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3796674%2Fcca09c39-da2a-454d-aebd-e9e26bd9b094.jpeg</url>
      <title>Forem: Ilya R.</title>
      <link>https://forem.com/rivik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/rivik"/>
    <language>en</language>
    <item>
      <title>Hardware-backed SSH keys, end to end: YubiKey, PIV, software alternatives, and where SSH CAs fit in</title>
      <dc:creator>Ilya R.</dc:creator>
      <pubDate>Sat, 09 May 2026 18:31:36 +0000</pubDate>
      <link>https://forem.com/rivik/hardware-backed-ssh-keys-end-to-end-yubikey-piv-software-alternatives-and-where-ssh-cas-fit-in-3lob</link>
      <guid>https://forem.com/rivik/hardware-backed-ssh-keys-end-to-end-yubikey-piv-software-alternatives-and-where-ssh-cas-fit-in-3lob</guid>
      <description>&lt;p&gt;This is a working guide to using a YubiKey for SSH on a real Linux fleet, plus the surrounding landscape — PIV, software-only alternatives, and SSH certificate authorities. The goal is to retire file-based SSH keys without breaking daily operations.&lt;/p&gt;

&lt;p&gt;The article is structured around four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What does a hardware-backed key actually do, and what knobs do you control?&lt;/li&gt;
&lt;li&gt;How do you combine those knobs into a policy that works for both root login and Ansible?&lt;/li&gt;
&lt;li&gt;What if you can't ship YubiKeys?&lt;/li&gt;
&lt;li&gt;When should you stop managing keys yourself and adopt an SSH CA?&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The problem with file-based keys
&lt;/h2&gt;

&lt;p&gt;Every classic SSH key is a file in &lt;code&gt;~/.ssh/&lt;/code&gt;. That file holds the private key. To log in to a server, your SSH client reads the file and produces a cryptographic signature.&lt;/p&gt;

&lt;p&gt;There are really two issues here, and they compound:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;File-based key can leak.&lt;/strong&gt; It exists in the filesystem, can be read by anything with sufficient access, can be copied, backed up, accidentally committed, or extracted via a misconfigured recovery scenario. This is a fundamental property of where the key lives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The discipline that would mitigate this rarely survives daily work.&lt;/strong&gt; The cryptography is fine; the operational reality isn't.

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What works in theory:&lt;/strong&gt; a passphrase-protected key combined with &lt;code&gt;ssh-agent -t 10m&lt;/code&gt; is genuinely close to unbreakable. The key is decrypted briefly, signs what it needs, and the agent forgets it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What happens in practice:&lt;/strong&gt; engineers drop passphrases for convenience, or load the key into ssh-agent on first use and leave the agent running for the entire session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent forwarding compounds it:&lt;/strong&gt; with &lt;code&gt;ssh -A&lt;/code&gt;, a key that's been unlocked once can sign on the operator's behalf from any forwarded host for the rest of the agent's lifetime.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Hardware-backed keys remove the need for that discipline. The private key never leaves the device, and signing requires the device's physical presence — there's nothing to forget to passphrase, nothing to leave running for too long, nothing for a forwarded host to sign with silently.&lt;/p&gt;

&lt;p&gt;YubiKey is the most flexible option because the same device works on Linux, macOS, Windows, iOS, and Android with the same protocol and the same key files. Most of this article is about YubiKey + FIDO2; the alternatives come later.&lt;/p&gt;




&lt;h2&gt;
  
  
  How a YubiKey actually signs
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1hjf0lgz29yi7qoy3prj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1hjf0lgz29yi7qoy3prj.png" alt=" " width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The SSH client never sees the private key. It hands the YubiKey a small piece of data to sign (a "nonce"), the YubiKey signs internally and returns the signature. If the device is unplugged, signing is impossible regardless of what's on the laptop.&lt;/p&gt;

&lt;p&gt;This article uses &lt;strong&gt;FIDO2&lt;/strong&gt; (the modern protocol; SSH key types &lt;code&gt;sk-ssh-ed25519@openssh.com&lt;/code&gt; and &lt;code&gt;sk-ecdsa-sha2-nistp256@openssh.com&lt;/code&gt;, generated with &lt;code&gt;ssh-keygen -t ed25519-sk&lt;/code&gt; or &lt;code&gt;-t ecdsa-sk&lt;/code&gt;). FIDO2 has been first-class in OpenSSH since version 8.2 (February 2020). PIV — the older smartcard protocol — is covered later as an alternative.&lt;/p&gt;




&lt;h2&gt;
  
  
  The four knobs
&lt;/h2&gt;

&lt;p&gt;When you generate a FIDO2 key on a YubiKey, four properties determine how it behaves:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Resident vs non-resident&lt;/strong&gt; — where the credential is stored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Touch&lt;/strong&gt; — does signing require a tap on the YubiKey?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PIN&lt;/strong&gt; — does signing require the FIDO2 PIN?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ssh-agent&lt;/strong&gt; — is the key loaded into ssh-agent, or used directly?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are independent yes/no choices. Combined, they describe what it takes to sign with that particular key. The next four sections take them one at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Knob 1: resident vs non-resident
&lt;/h3&gt;

&lt;p&gt;This is the one most people get wrong, so it gets the most space.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F18tnumlf5j06jpewifbq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F18tnumlf5j06jpewifbq.png" alt=" " width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resident&lt;/strong&gt; (created with &lt;code&gt;-O resident&lt;/code&gt;): the credential lives on the YubiKey itself. The file in &lt;code&gt;~/.ssh/&lt;/code&gt; is just a pointer — a label that says "ask the device for credential 0xA3F2…". If you delete the file, you can recreate it on any machine by running &lt;code&gt;ssh-keygen -K&lt;/code&gt;, which queries the YubiKey for all its resident credentials and writes them to disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-resident&lt;/strong&gt; (the default): the credential is split. The YubiKey has a master secret used to derive credentials on demand. The file on disk holds an encrypted handle. To sign, the YubiKey needs the handle from the file &lt;em&gt;plus&lt;/em&gt; its own master secret. Without the file, the YubiKey doesn't know which credential to derive. Without the YubiKey, the file is gibberish.&lt;/p&gt;

&lt;p&gt;The practical consequences:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Resident&lt;/th&gt;
&lt;th&gt;Non-resident&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Can the file be reconstructed from the YubiKey?&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;ssh-keygen -K&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does losing the file matter?&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (re-enroll)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does a passphrase on the file add real security?&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt; — file holds an identifier, not a secret&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; — file holds the encrypted credential handle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is ssh-agent needed?&lt;/td&gt;
&lt;td&gt;No, the YubiKey &lt;em&gt;is&lt;/em&gt; the agent&lt;/td&gt;
&lt;td&gt;Usually yes, to avoid re-typing the passphrase&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The headline rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Resident keys don't need a file passphrase, because the file holds nothing secret. Non-resident keys do, because the file holds the part of the credential that isn't on the YubiKey.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A non-resident key with a passphrase is conceptually identical to a classic passphrase-protected file SSH key — except the actual signing material never leaves the YubiKey. Same mental model, with the YubiKey as a hard-bound second factor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Knob 2: touch
&lt;/h3&gt;

&lt;p&gt;When you sign with a key, the YubiKey can require you to physically touch the gold disc. This is "user presence" — proof that a human is at the device.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Touch required&lt;/strong&gt; (default): every signing produces a touch prompt. The YubiKey's LED blinks, you tap it, the signing completes. Failure to touch within ~15 seconds aborts the signing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No touch&lt;/strong&gt;: signings happen automatically as long as the YubiKey is plugged in. Set with &lt;code&gt;-O no-touch-required&lt;/code&gt; at generation. The server's &lt;code&gt;authorized_keys&lt;/code&gt; must also have &lt;code&gt;no-touch-required&lt;/code&gt; for OpenSSH to accept the signature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You turn touch off when an operation produces many signings — Ansible across hundreds of hosts, an &lt;code&gt;rsync&lt;/code&gt; of 100k files, a deploy that opens 50 sessions. None of these can realistically prompt for a touch each time.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Disable touch &lt;strong&gt;only&lt;/strong&gt; if you plan to use short-lived ssh-agent with &lt;strong&gt;password protected non-resident&lt;/strong&gt; keyfile!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Touch is a defense against silent malicious signing on a host you've connected to (with agent forwarding) or on a compromised laptop you happen to be at. It is &lt;em&gt;not&lt;/em&gt; a defense against device theft — someone holding the device can touch it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Knob 3: PIN
&lt;/h3&gt;

&lt;p&gt;The YubiKey has a FIDO2 PIN, set once with &lt;code&gt;ykman fido access change-pin&lt;/code&gt;. It's separate from touch.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PIN required&lt;/strong&gt; (&lt;code&gt;-O verify-required&lt;/code&gt; at generation): every signing prompts for the PIN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PIN not required&lt;/strong&gt;: signing happens without a PIN (subject to touch policy).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PIN is a defense against device theft. Touch alone doesn't help you here — the thief can touch. PIN does, because the thief doesn't know it.&lt;/p&gt;

&lt;p&gt;The same device PIN gates &lt;code&gt;ssh-keygen -K&lt;/code&gt; and FIDO2 credential management generally. &lt;strong&gt;Even for credentials that don't require PIN to &lt;em&gt;sign&lt;/em&gt;, the device PIN is required to &lt;em&gt;extract&lt;/em&gt; them.&lt;/strong&gt; This becomes important in the four-mode model below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Knob 4: ssh-agent
&lt;/h3&gt;

&lt;p&gt;ssh-agent is a small process that holds keys in memory and signs on behalf of SSH clients that ask. It exists for two reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You don't want to re-enter a file passphrase on every connection.&lt;/strong&gt; Load the key once, use it many times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want agent forwarding&lt;/strong&gt; (&lt;code&gt;ssh -A&lt;/code&gt;). Connecting to host A and then from inside that session to host B, with B able to ask your laptop's agent for signatures back through the forwarded socket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For YubiKey-backed keys, whether you need an agent depends on the connection pattern, not just on storage:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Connection pattern&lt;/th&gt;
&lt;th&gt;Agent needed?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direct SSH (&lt;code&gt;ssh host&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No — ssh client talks to the YubiKey directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ProxyJump (&lt;code&gt;ssh -J jump target&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No — local ssh signs each hop directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent forwarding (&lt;code&gt;ssh -A&lt;/code&gt;, in-session multi-hop)&lt;/td&gt;
&lt;td&gt;Yes — remote host needs to reach your agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-resident key with passphrase&lt;/td&gt;
&lt;td&gt;Yes — to avoid retyping on every connection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;ProxyJump is the modern multi-hop pattern: the local ssh client opens each connection in sequence, signing each against the YubiKey directly. Nothing is exposed on intermediate hosts. Agent forwarding is the older pattern, used when you're already inside a remote shell and need to reach further (e.g., on &lt;code&gt;host1&lt;/code&gt;, running &lt;code&gt;scp host2:file ./&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;For loading resident keys into the agent (when forwarding needed), no passphrase is required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-add ~/.ssh/id_sudo  &lt;span class="c"&gt;# No passphrase prompt; the file holds a reference, not encrypted material.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Never&lt;/strong&gt; do this with no-touch no-pin keys! They must be password protected and added to agent like &lt;code&gt;ssh-add -t 10m ~/.ssh/id_wheel&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Touch-required keys make agent forwarding safe again.&lt;/strong&gt; With file-based keys, an unlocked agent signs anything it's asked to, silently, for the agent's lifetime — agent forwarding became dangerous because a compromised forwarded host could sign as you on every other host you have access to. With FIDO2 touch-required keys, every signing request from a forwarded host produces a touch prompt on your laptop. If you didn't initiate the action, you don't touch, and the signing fails. The classic "never use &lt;code&gt;-A&lt;/code&gt;" advice no longer applies once credentials are hardware-backed and touch-gated.&lt;/p&gt;

&lt;p&gt;This refines the rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Resident is the default. Non-resident is reserved for keys that must live in ssh-agent for the wheel-style mass-automation use case&lt;/strong&gt; — explained next.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The four-mode model
&lt;/h2&gt;

&lt;p&gt;A single key configuration cannot serve both rare root login and Ansible across a fleet. Different operations have different blast radius and different frequency, and they want different policies. The pragmatic answer is four keys, each a deliberate combination of the four knobs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh2wboongeetsex6y9s31.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh2wboongeetsex6y9s31.png" alt=" " width="800" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same model as a table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Touch&lt;/th&gt;
&lt;th&gt;PIN&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;File pass&lt;/th&gt;
&lt;th&gt;ssh-agent&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;root&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;resident&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Direct root SSH login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sudo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;resident&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Daily admin (TOTP at host)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wheel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;non-resident&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NOPASSWD mass automation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;robo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;resident&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Backups, sftp, stage deploys&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why three are resident and one isn't
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;wheel&lt;/code&gt; is the deliberate exception, for three reasons that compound:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Mass automation must use ssh-agent.&lt;/strong&gt; Ansible across 300 hosts produces thousands of signing operations per run. A touch on each is unworkable. So &lt;code&gt;wheel&lt;/code&gt; is generated &lt;code&gt;no-touch-required&lt;/code&gt; AND no &lt;code&gt;verify-required&lt;/code&gt;. Once it's loaded into ssh-agent (so it can be reused across the run), the agent holds the key in memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The file on disk needs a passphrase.&lt;/strong&gt; It's to prevent accidental loading, and to force the operator to deliberately type something before the agent gets the key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The passphrase needs a forcing function.&lt;/strong&gt; ssh-keygen -K on a new machine writes resident credentials into ~/.ssh — &lt;code&gt;id_root&lt;/code&gt;, &lt;code&gt;id_sudo&lt;/code&gt;, &lt;code&gt;id_robo&lt;/code&gt; — none needing passphrases, because they're just references to material on the device. The flow trains you that "resident-export-without-passphrase is safe."&lt;/p&gt;

&lt;p&gt;If wheel were resident, the same command would write &lt;code&gt;id_wheel&lt;/code&gt;, and you'd have to remember the one exception: passphrase this file, the others are fine. Humans don't reliably catch that exception.&lt;br&gt;
Non-resident wheel is structurally outside that flow: ssh-keygen -K can't produce it, and the file you copy from your existing setup already has a passphrase. A physical equivalent: keep wheel on a separate YubiKey with a "passphrase required" sticker.&lt;/p&gt;
&lt;h3&gt;
  
  
  Generation commands
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# root: resident, touch + PIN&lt;/span&gt;
ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519-sk &lt;span class="nt"&gt;-O&lt;/span&gt; resident &lt;span class="nt"&gt;-O&lt;/span&gt; verify-required &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/id_root &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"laptop-root"&lt;/span&gt;

&lt;span class="c"&gt;# sudo: resident, touch only&lt;/span&gt;
ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519-sk &lt;span class="nt"&gt;-O&lt;/span&gt; resident &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/id_sudo &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"laptop-sudo"&lt;/span&gt;

&lt;span class="c"&gt;# wheel: non-resident, no touch, no PIN, passphrase, used via ssh-agent -t 10m&lt;/span&gt;
ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519-sk &lt;span class="nt"&gt;-O&lt;/span&gt; no-touch-required &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/id_wheel &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"laptop-wheel"&lt;/span&gt;
&lt;span class="c"&gt;# Set a real passphrase when prompted.&lt;/span&gt;

&lt;span class="c"&gt;# robo: resident, no touch, no PIN&lt;/span&gt;
ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519-sk &lt;span class="nt"&gt;-O&lt;/span&gt; resident &lt;span class="nt"&gt;-O&lt;/span&gt; no-touch-required &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/id_robo &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"laptop-robo"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;-N ""&lt;/code&gt; skips the file passphrase prompt. Used for the three resident keys. &lt;code&gt;wheel&lt;/code&gt; is the only one without &lt;code&gt;-N ""&lt;/code&gt; — you'll be prompted, and you set a real passphrase.&lt;/p&gt;
&lt;h3&gt;
  
  
  Server-side &lt;code&gt;authorized_keys&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Keys generated with &lt;code&gt;-O no-touch-required&lt;/code&gt; need a matching &lt;code&gt;no-touch-required&lt;/code&gt; option in &lt;code&gt;authorized_keys&lt;/code&gt;, otherwise OpenSSH rejects the signature.&lt;/p&gt;
&lt;h4&gt;
  
  
  root
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;/root/.ssh/authorized_keys&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;sk&lt;/span&gt;-ssh-ed25519@openssh.com AAAA... laptop-root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  wheel
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;~wheel/.ssh/authorized_keys&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;no&lt;/span&gt;-touch-required sk-ssh-ed25519@openssh.com AAAA... laptop-wheel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  sudo
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;~admin/.ssh/authorized_keys&lt;/code&gt; (the daily-admin user with &lt;code&gt;sudo&lt;/code&gt; privileges):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;sk&lt;/span&gt;-ssh-ed25519@openssh.com AAAA... laptop-sudo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pair the &lt;code&gt;sudo&lt;/code&gt; key with &lt;code&gt;pam_google_authenticator.so&lt;/code&gt; at the host's &lt;code&gt;sudo&lt;/code&gt; PAM stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/pam.d/sudo
&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="n"&gt;pam_google_authenticator&lt;/span&gt;.&lt;span class="n"&gt;so&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per-user TOTP secrets in &lt;code&gt;/etc/google_authenticator&lt;/code&gt; (readable only by root) &lt;em&gt;can&lt;/em&gt; protect from stolen YubiKey (touch is not enough for sudo). Also protects you from accidental &lt;code&gt;sudo rm -rf /&lt;/code&gt; .&lt;/p&gt;

&lt;h4&gt;
  
  
  robo
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;~robo/.ssh/authorized_keys&lt;/code&gt; — the most-restricted, non-prod-fleet entry, constrained at source IP and forced command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;no&lt;/span&gt;-touch-required,from="10.0.0.0/8",command="/usr/local/bin/backup-shell" sk-ssh-ed25519@openssh.com AAAA... laptop-robo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  PIV — the older alternative protocol
&lt;/h2&gt;

&lt;p&gt;YubiKey supports a second SSH path: PIV (Personal Identity Verification), a US-government smartcard standard that predates FIDO2 by about a decade.&lt;/p&gt;

&lt;p&gt;PIV-on-YubiKey gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple "slots" (9a, 9c, 9d, 9e, plus retired 82–95) — each holds a separate certificate and key pair.&lt;/li&gt;
&lt;li&gt;Three touch policies per slot: &lt;code&gt;never&lt;/code&gt;, &lt;code&gt;cached&lt;/code&gt; (15-second window), &lt;code&gt;always&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;PIN policies: &lt;code&gt;default&lt;/code&gt;, &lt;code&gt;once&lt;/code&gt;, &lt;code&gt;always&lt;/code&gt;, &lt;code&gt;never&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Standard X.509 certificates, which integrate nicely if your environment already uses smartcards for things like email signing, S/MIME, or government identity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A typical setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate ECCP256 key in slot 9a, with cached touch and PIN-once&lt;/span&gt;
ykman piv keys generate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--algorithm&lt;/span&gt; ECCP256 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--touch-policy&lt;/span&gt; CACHED &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pin-policy&lt;/span&gt; ONCE &lt;span class="se"&gt;\&lt;/span&gt;
  9a /tmp/pubkey.pem

&lt;span class="c"&gt;# Self-signed certificate (or sign with a corporate CA)&lt;/span&gt;
ykman piv certificates generate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subject&lt;/span&gt; &lt;span class="s2"&gt;"CN=admin"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  9a /tmp/pubkey.pem

&lt;span class="c"&gt;# Use it directly via PKCS#11&lt;/span&gt;
ssh &lt;span class="nt"&gt;-I&lt;/span&gt; /usr/lib/x86_64-linux-gnu/libykcs11.so user@host

&lt;span class="c"&gt;# Or load into ssh-agent&lt;/span&gt;
ssh-add &lt;span class="nt"&gt;-s&lt;/span&gt; /usr/lib/x86_64-linux-gnu/libykcs11.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On paper, the &lt;code&gt;cached&lt;/code&gt; touch policy is exactly what you want. One touch unlocks signing for 15 seconds, then it locks again — ideal for &lt;code&gt;rsync&lt;/code&gt; or &lt;code&gt;scp&lt;/code&gt; of many files where one logical operation triggers many SSH transactions.&lt;/p&gt;

&lt;p&gt;In practice, the cache behavior depends on how your SSH client handles the PKCS#11 session. Different clients open and close PKCS#11 sessions differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some open the session once per &lt;code&gt;ssh&lt;/code&gt; invocation and keep it open, so the cache works as advertised.&lt;/li&gt;
&lt;li&gt;Some open and close per cryptographic operation, which resets the cache and produces a touch prompt every signing.&lt;/li&gt;
&lt;li&gt;Behavior varies between OpenSSH versions, between using ssh-agent vs. direct PKCS#11, between Linux distributions and OS package builds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a single user on one machine, PIV with &lt;code&gt;cached&lt;/code&gt; can be made to work once you've found the right combination. &lt;strong&gt;For a fleet with mixed client versions across Linux, macOS, and Windows, the behavior isn't predictable.&lt;/strong&gt; You'll get bug reports for years and your runbooks will accumulate &lt;code&gt;if your client is X, do Y&lt;/code&gt; branches.&lt;/p&gt;

&lt;p&gt;FIDO2 sidesteps this entirely. Per-credential policy is set at generation time, OpenSSH speaks the protocol natively without PKCS#11 in the middle, and behavior is consistent across clients and platforms.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Use PIV if&lt;/strong&gt; you already have smartcard tooling, X.509 workflows, or a strong organizational reason to use the existing standard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use FIDO2 if&lt;/strong&gt; you're starting fresh and want predictable behavior across a heterogeneous fleet.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Software-only alternatives
&lt;/h2&gt;

&lt;p&gt;Hardware tokens cost money and procurement takes time. For distributed contractors, BYOD policies, or organizations without an IT budget for keys, you're sometimes deploying software-only solutions. The options below all keep your private key better-protected than a plain file in &lt;code&gt;~/.ssh/&lt;/code&gt;, but with different trade-offs.&lt;/p&gt;

&lt;p&gt;The dimension that matters: &lt;strong&gt;can the private key be extracted from where it lives?&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;th&gt;Key storage&lt;/th&gt;
&lt;th&gt;Extractable?&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Secretive (macOS)&lt;/td&gt;
&lt;td&gt;Apple Secure Enclave&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Touch ID per signing. Open source.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows Hello SSH&lt;/td&gt;
&lt;td&gt;Windows TPM&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TPM-bound; biometric/PIN per signing. Caveats below.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KeePassXC SSH agent&lt;/td&gt;
&lt;td&gt;Encrypted KDBX database&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (when DB unlocked)&lt;/td&gt;
&lt;td&gt;Keys are read from disk; the DB is just an extra layer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1Password SSH agent&lt;/td&gt;
&lt;td&gt;1Password vault (cloud-synced)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (extractable when vault is unlocked locally)&lt;/td&gt;
&lt;td&gt;Convenient. You're trusting their infrastructure.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LastPass SSH agent&lt;/td&gt;
&lt;td&gt;LastPass vault (cloud-synced)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (2022 breach; weak master passwords brute-forced offline)&lt;/td&gt;
&lt;td&gt;LastPass had a major vault-data breach in 2022.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The categories sort cleanly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardware-backed (Secretive, Windows Hello).&lt;/strong&gt; The private key is generated inside a secure element and never leaves it. Same security model as a YubiKey, but tied to one device. Strong for "I always work from this laptop"; weaker for "I work from three machines."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note on Windows Hello SSH.&lt;/strong&gt; "Windows Hello SSH" gets used to describe three different things, only one of which is genuinely the macOS-Secretive equivalent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TPM-backed via Virtual Smart Card&lt;/strong&gt; — the actual TPM-bound SSH path. Requires &lt;code&gt;tpmvscmgr.exe&lt;/code&gt; to create a virtual smart card, a self-signed cert via the Microsoft Smart Card Key Storage Provider, and PuTTY/Pageant rather than the default OpenSSH client. &lt;strong&gt;&lt;code&gt;tpmvscmgr.exe&lt;/code&gt; is Pro/Enterprise/Education only&lt;/strong&gt; — not available on Windows 11 Home.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows Hello for Business&lt;/strong&gt; — the corporate path, requires Entra ID or AD join. Out of scope for a personal laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ssh-keygen -t ed25519-sk&lt;/code&gt; with Windows Hello as the UV layer&lt;/strong&gt; — the most-documented "Windows Hello SSH" path, but Windows Hello is just the UI layer asking for your PIN. The actual FIDO2 authenticator is still a USB device (typically a YubiKey). On Windows 11 Home, this is effectively the only available option, which means you need external hardware anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The takeaway: on macOS, software-only hardware-backed SSH is one click in Secretive. On Windows it's an enterprise feature with awkward retrofitting, and Home users are pushed toward an external YubiKey regardless. This is one of the practical reasons a YubiKey wins on cross-platform — the same device works the same way on every OS, no per-OS puzzle to solve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Software-encrypted (KeePassXC).&lt;/strong&gt; The key is a normal SSH private key, encrypted in a database. Strictly better than a naked file because there's a master password gating access, but the key is still extractable any time the DB is open. Reasonable when you already use KeePassXC for password management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud-synced (1Password, LastPass).&lt;/strong&gt; The key is stored in the provider's vault. Whoever can read the vault can read the key. You're trusting the provider's infrastructure and operational security. 1Password's design (Secret Key + master password) makes server-side decryption genuinely difficult; LastPass's 2022 breach demonstrated that vault contents can leak in practice. The convenience is real; the trust assumption is non-trivial.&lt;/p&gt;

&lt;p&gt;Pick the strongest option you can ship to your team, and back it with a multi-mode model along the same lines as the YubiKey one — different keys for different operation classes, with the most automated keys getting the strongest restrictions at the server side.&lt;/p&gt;




&lt;h2&gt;
  
  
  SSH CAs — Teleport, step-ca, HashiCorp Boundary
&lt;/h2&gt;

&lt;p&gt;Everything above is about &lt;strong&gt;credential custody&lt;/strong&gt;: where the private key lives and what's required to use it.&lt;/p&gt;

&lt;p&gt;Teleport, step-ca (Smallstep's open-source CA), and HashiCorp Boundary solve a related but distinct problem: &lt;strong&gt;credential lifecycle and access control&lt;/strong&gt;. Instead of long-lived keys, they issue short-lived SSH certificates that expire automatically. They integrate with identity providers (Okta, Google Workspace, Entra ID), log session activity, and can grant just-in-time access that revokes itself.&lt;/p&gt;

&lt;p&gt;Whether you need this depends on scale.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Team size&lt;/th&gt;
&lt;th&gt;Typical reality&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Solo or up to ~15 people&lt;/td&gt;
&lt;td&gt;You know who has access. &lt;code&gt;authorized_keys&lt;/code&gt; is auditable by reading. Offboarding is manual but tractable.&lt;/td&gt;
&lt;td&gt;YubiKey + four-mode model is enough. A CA adds operational overhead without proportional security gain.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15–100 people, growing&lt;/td&gt;
&lt;td&gt;New hires need access; departures need offboarding; "who can SSH to production?" stops being answerable from &lt;code&gt;authorized_keys&lt;/code&gt; alone. Onboarding takes a day per person.&lt;/td&gt;
&lt;td&gt;Adopt a CA system. Pain is real and pays back the investment.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hundreds of devs, regulated industry&lt;/td&gt;
&lt;td&gt;Manual key management is impossible. You can't audit it, you can't rotate it, you can't prove who logged into what after the fact.&lt;/td&gt;
&lt;td&gt;CA system is mandatory. Plan around it from day one.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The operational pain shows up in roughly this order as you grow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Adding a key to N hosts requires Ansible discipline. Doable.&lt;/li&gt;
&lt;li&gt;Removing a key from N hosts requires the same discipline. Often skipped on departures.&lt;/li&gt;
&lt;li&gt;Rotating keys regularly across the whole fleet is a project.&lt;/li&gt;
&lt;li&gt;Answering "is this person's access still active?" requires querying every host. Expensive.&lt;/li&gt;
&lt;li&gt;Proving to an auditor what happened in a session three months ago requires session logging that &lt;code&gt;authorized_keys&lt;/code&gt; doesn't provide.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of these gets harder in a known order, and each has a CA-shaped solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The common confusion: SSH CAs don't replace hardware keys. They complement them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you use a CA, the long-term identity authenticates to the CA's enrollment endpoint and gets a short-lived SSH certificate in return. That long-term identity needs to be protected — if it's a file-based key, an attacker who steals it can request fresh certificates indefinitely. The CA system has moved the problem rather than solved it.&lt;/p&gt;

&lt;p&gt;The right shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Long-term identity:&lt;/strong&gt; YubiKey + the four-mode model (or just &lt;code&gt;sudo&lt;/code&gt;/&lt;code&gt;root&lt;/code&gt; keys, depending on what the CA expects).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short-term access:&lt;/strong&gt; SSH certificates issued by the CA, valid for hours, scoped to specific hosts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit:&lt;/strong&gt; CA logs the issuance; session recording captures what happened during use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hardware-backed identity is the foundation. The CA is the access plane on top of it.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;The four knobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resident vs non-resident&lt;/strong&gt; — where the credential lives. Resident is the default; the file is a label, no passphrase needed. Non-resident is for keys that must be in ssh-agent; the file holds encrypted material and &lt;em&gt;must&lt;/em&gt; have a passphrase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Touch&lt;/strong&gt; — physical proof of presence. Defends against silent signing on a forwarded or compromised host. Not a defense against device theft.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PIN&lt;/strong&gt; — defense against device theft. Also gates &lt;code&gt;ssh-keygen -K&lt;/code&gt; extraction of resident credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ssh-agent&lt;/strong&gt; — not needed for direct SSH or ProxyJump. Needed for agent forwarding (&lt;code&gt;-A&lt;/code&gt;, including in-session multi-hop) and for non-resident keys with passphrases. With FIDO2 + touch-required keys, agent forwarding is safe again because every signing requires a touch on your laptop — silent signing isn't possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The four-mode model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;root&lt;/code&gt; — resident, PIN + touch. Direct root login, rare.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sudo&lt;/code&gt; — resident, touch only. Daily admin. Pair with PAM TOTP at the host.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wheel&lt;/code&gt; — non-resident, no touch, passphrase + ssh-agent. NOPASSWD mass automation. Non-resident specifically so device + PIN cannot extract it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;robo&lt;/code&gt; — resident, no touch, no PIN. Convenience tier, restricted at the server with &lt;code&gt;from=&lt;/code&gt; and &lt;code&gt;command=&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other paths and where they fit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PIV&lt;/strong&gt; is theoretically cleaner (slots, certificates, cached touch policy) but its caching depends on PKCS#11 session handling that drifts between SSH client versions. Avoid for heterogeneous fleets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Software alternatives&lt;/strong&gt; sort by extractability. Secretive and Windows Hello are hardware-backed (non-extractable). KeePassXC, 1Password, and LastPass are extractable to varying degrees of "the provider can see your key."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH CAs&lt;/strong&gt; (Teleport, step-ca, HashiCorp Boundary) solve access management at scale. They don't replace hardware keys — they sit on top of them. Adopt when manual &lt;code&gt;authorized_keys&lt;/code&gt; management starts hurting, typically around 15–100 engineers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shortest possible version: hardware key first, multi-mode policy second, CA system if and when scale demands it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Note about YubiKey Bio
&lt;/h2&gt;

&lt;p&gt;The article above covers YubiKey 5C variants only — Bio Series is out of scope. The Bio Edition is genuinely more convenient for &lt;code&gt;verify-required&lt;/code&gt; SSH keys since one fingerprint tap collapses PIN+touch into a single interaction, but it costs noticeably more than a 5C and the widely available FIDO Edition is FIDO2/U2F only — no PGP, OATH, or PIV. Multi-protocol Edition exists but is sold only via enterprise subscription.&lt;/p&gt;

</description>
      <category>yubikey</category>
      <category>security</category>
      <category>linux</category>
      <category>ssh</category>
    </item>
    <item>
      <title>When TLS 1.3 Silently Dies Inside Your Android Proxy</title>
      <dc:creator>Ilya R.</dc:creator>
      <pubDate>Fri, 20 Mar 2026 16:59:43 +0000</pubDate>
      <link>https://forem.com/rivik/when-tls-13-silently-dies-inside-your-android-proxy-1c3</link>
      <guid>https://forem.com/rivik/when-tls-13-silently-dies-inside-your-android-proxy-1c3</guid>
      <description>&lt;p&gt;We run &lt;a href="https://iproxy.online" rel="noopener noreferrer"&gt;iProxy.online&lt;/a&gt;, a mobile proxy infrastructure. Our Android app turns phones into proxy servers across 100+ countries. Last year we shipped an advanced network health checker that runs a lot of probes through these proxies to a controlled server. That’s when things got weird.&lt;/p&gt;

&lt;p&gt;A small but noticeable percentage of devices started failing HTTPS checks. HTTP worked fine. The failure was always at the TLS handshake stage. And the behavior was completely non-deterministic: broken for two hours, then fine, then broken for five minutes, then fine again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we saw
&lt;/h2&gt;

&lt;p&gt;The correlations were weak and noisy. Android v8-v9 devices showed up more often. Cheaper, lower-spec phones were overrepresented. The strongest signal was memory pressure on the device. When we could catch the failure in real time, the device was almost always low on available RAM. But metrics from memory-starved phones are unreliable by definition, so we couldn’t be sure this wasn’t survivorship bias.&lt;/p&gt;

&lt;p&gt;Two tracks of investigation: find correlations (inconclusive, as described above) and understand what actually breaks.&lt;/p&gt;

&lt;p&gt;The second track was harder than it sounds. These are not our devices. They sit in remote locations. Physical access happens maybe once a month. We can’t deploy debug builds on demand. The bug is intermittent with no predictable trigger. Direct on-device debugging was effectively impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Narrowing it down
&lt;/h2&gt;

&lt;p&gt;Our metrics pointed at TLS handshake failure, so we tried to reproduce manually. curl through the same proxy, same server (Caddy, default Ubuntu 24.04 repos):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-x&lt;/span&gt; socks5://proxy:port https://our-server.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works perfectly. TLS 1.3, clean handshake, 200 OK. Every time.&lt;/p&gt;

&lt;p&gt;Our network checker is written in Go (1.24 at the time). I built a minimal Go client to isolate the behavior. Here’s where it got interesting:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Client&lt;/th&gt;
&lt;th&gt;TLS version&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;curl&lt;/td&gt;
&lt;td&gt;1.3&lt;/td&gt;
&lt;td&gt;works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;1.3&lt;/td&gt;
&lt;td&gt;hangs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;1.2 (forced)&lt;/td&gt;
&lt;td&gt;works&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Forcing TLS 1.2 in Go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;tlsConfig&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tls&lt;/span&gt;&lt;span class="o"&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;MaxVersion&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VersionTLS12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This consistently fixed the issue on affected devices.&lt;/p&gt;

&lt;p&gt;A tcpdump on the client side showed the Go client sending ClientHello and then… nothing. No ServerHello coming back to client. The proxy app just sat there.&lt;/p&gt;

&lt;h2&gt;
  
  
  It gets weirder
&lt;/h2&gt;

&lt;p&gt;The problem was server-dependent. Go + TLS 1.3 failed against our Caddy server, against Cloudflare, against Google. But it worked against some other sites. So the failure depended on the specific TLS implementation on the remote end, not just the client.&lt;/p&gt;

&lt;p&gt;And this wasn’t limited to our checker. When the bug was active on a device, Chrome via same mobile proxy couldn’t open Google over TLS 1.3 either. TLS 1.2 sites loaded fine. This was a device/app-level issue, not something specific to our Go code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Go and curl behave differently
&lt;/h2&gt;

&lt;p&gt;This is the part that took the longest to reason about.&lt;/p&gt;

&lt;p&gt;Go’s &lt;code&gt;crypto/tls&lt;/code&gt; and curl’s underlying OpenSSL/BoringSSL produce different ClientHello messages. Go’s ClientHello has always been somewhat larger due to different extension sets and key share choices. But there’s a much bigger factor here that we didn’t initially consider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Starting with Go 1.23, the post-quantum key exchange X25519Kyber768Draft00 is enabled by default&lt;/strong&gt; when &lt;code&gt;Config.CurvePreferences&lt;/code&gt; is nil (which is the standard case). In Go 1.24, this became X25519MLKEM768. The ML-KEM public key alone is 1184 bytes. This makes the ClientHello big enough to exceed a single TCP packet at typical 1500-byte MTU.&lt;/p&gt;

&lt;p&gt;curl (as of the versions we tested) does not send post-quantum key shares by default. Its ClientHello is much smaller and fits comfortably in a single packet.&lt;/p&gt;

&lt;p&gt;This size difference matters because our Android app is acting as a TCP proxy. It reads data from one socket and writes it to another. The proxy doesn’t terminate TLS; it just forwards bytes. But it still needs to buffer them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The (probable) mechanism
&lt;/h2&gt;

&lt;p&gt;Here’s our working theory. There are two chokepoints, not one.&lt;/p&gt;

&lt;p&gt;Chokepoint 1: the ClientHello. Go 1.24 sends a very big ClientHello due to the ML-KEM post-quantum key share (1184 bytes for the public key alone). curl’s OpenSSL sends ~500-700 bytes with a 32-byte X25519 key share. Chrome 124+ also sends post-quantum key shares by default, producing similarly large ClientHello messages. Our tcpdump on the client side showed the ClientHello leaving the client, then silence. This means the proxy either failed to forward the ClientHello to the server, or forwarded it and failed to relay the server’s response back. We can’t distinguish these two cases from a client-side capture alone, but both point to the same thing: the proxy is the bottleneck.&lt;/p&gt;

&lt;p&gt;Chokepoint 2: the server flight. This explains why TLS 1.3 worked with some servers but not others, even when the ClientHello is identical.&lt;/p&gt;

&lt;p&gt;In TLS 1.3 the server responds with a single flight: ServerHello + EncryptedExtensions + Certificate + CertificateVerify + Finished, all at once. The size of this response depends on the server’s certificate chain. A small site with a single cert and short chain might send 2-3 KB. Google or Cloudflare with full certificate chains send 4-6+ KB. Unfortunately, I don't remember what kind of sites worked with TLS 1.3 and what kind of certificate chain they had, so this is just a guess.&lt;/p&gt;

&lt;p&gt;So even when the ClientHello makes it through the proxy, the server response might not. A memory-starved proxy can relay a 2 KB response but chokes on a 5 KB one. That’s why some sites worked and others didn’t, with the exact same client? It sounds dubious, the numbers are too small, but I didn't have any other hypotheses.&lt;/p&gt;

&lt;p&gt;TLS 1.2 avoids both problems. Its ClientHello is smaller (no PQ key shares), and the handshake is split across multiple round trips with smaller messages in each direction. No single message is large enough to stress the proxy’s buffers.&lt;/p&gt;

&lt;p&gt;Why restart fixes it: killing the app clears leaked memory, resets all socket state, and gives the proxy fresh buffer capacity. The fact that this consistently works is the strongest evidence that the root cause is resource exhaustion, not a protocol bug.​​​​​​​​​​​​​​​​&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;We needed a production fix, not a research paper. We did two things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Immediate mitigation&lt;/strong&gt;: capped TLS to 1.2 for the checker and for proxy traffic where possible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;tlsConfig&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tls&lt;/span&gt;&lt;span class="o"&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;MaxVersion&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VersionTLS12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Observability-driven restart&lt;/strong&gt;: the checker now detects when TLS 1.3 fails but TLS 1.2 succeeds on the same device. When this pattern appears, we send a remote command to fully restart the app (kill the process, clear memory, relaunch). This consistently fixes the problem, which further supports the memory pressure hypothesis.&lt;/p&gt;

&lt;p&gt;We also found and fixed memory leaks in our app that were contributing to the pressure. The correlation between leak fixes and reduced TLS 1.3 failures was visible in our dashboards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we didn’t dig deeper
&lt;/h2&gt;

&lt;p&gt;We don’t have a verified root cause. We have a strong hypothesis, consistent correlations, and effective mitigations that confirm the hypothesis indirectly.&lt;/p&gt;

&lt;p&gt;The devices are remote, not ours, and physically accessible about once a month. The bug is intermittent with no reliable trigger. Production users were affected. We needed fast fix, not a deep research.&lt;/p&gt;

&lt;p&gt;We’re now investing in better telemetry and the ability to capture targeted diagnostics remotely. Next time something like this happens, we’ll have the data to pin it down.​​​​​​​​​​​​​​​​&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For proxy developers on constrained devices&lt;/strong&gt;: TLS 1.3 messages are larger than TLS 1.2, and post-quantum key exchange makes them much larger. If your proxy buffers TCP data, make sure your buffers can handle multi-packet TLS records, especially under memory pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Go developers proxying TLS&lt;/strong&gt;: be aware that Go 1.23+ sends post-quantum key shares by default. If you’re running through proxies or middleboxes, this can break things. Set &lt;code&gt;CurvePreferences&lt;/code&gt; explicitly or use &lt;code&gt;GODEBUG=tlskyber=0&lt;/code&gt; (Go 1.23) / &lt;code&gt;GODEBUG=tlsmlkem=0&lt;/code&gt; (Go 1.24+) to disable it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For anyone debugging intermittent TLS failures&lt;/strong&gt;: if TLS 1.2 works and TLS 1.3 doesn’t, the problem is almost certainly not in TLS itself. It’s in something between client and server that can’t handle the larger messages. Check your middleboxes, proxies, and buffer sizes.&lt;/p&gt;

</description>
      <category>security</category>
      <category>network</category>
      <category>linux</category>
    </item>
    <item>
      <title>The Systemd Bug That Nobody Wants to Own</title>
      <dc:creator>Ilya R.</dc:creator>
      <pubDate>Fri, 27 Feb 2026 19:18:00 +0000</pubDate>
      <link>https://forem.com/rivik/the-systemd-bug-that-nobody-wants-to-own-1i57</link>
      <guid>https://forem.com/rivik/the-systemd-bug-that-nobody-wants-to-own-1i57</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; There’s a namespace bug affecting Ubuntu 20.04, 22.04, and 24.04 servers that causes random service failures. It’s been reported since 2021 across systemd, Ubuntu, Fedora, and Red Hat trackers. Most reports are either expired or labeled “not-our-bug.” Only a reboot fixes it.&lt;/p&gt;




&lt;p&gt;If you’re running Ubuntu servers and have ever seen this in your logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failed to set up mount namespacing: /run/systemd/unit-root/dev: Invalid argument
Failed at step NAMESPACE spawning: Invalid argument
Main process exited, code=exited, status=226/NAMESPACE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Congratulations. You’ve encountered one of the most frustrating bugs in the Linux ecosystem — one that’s been bouncing between the kernel and systemd teams for years with no resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens
&lt;/h2&gt;

&lt;p&gt;Random systemd services — including critical ones like &lt;code&gt;systemd-resolved&lt;/code&gt;, &lt;code&gt;systemd-timesyncd&lt;/code&gt;, &lt;code&gt;systemd-journald&lt;/code&gt;, and your own custom services — suddenly refuse to start. The error mentions “mount namespacing” and “Invalid argument.”&lt;/p&gt;

&lt;p&gt;Restarting the service doesn’t help. &lt;code&gt;systemctl daemon-reload&lt;/code&gt; doesn’t help. The only reliable fix is a full system reboot.&lt;/p&gt;

&lt;p&gt;If you’re running containerized workloads (LXC, LXD, Proxmox), it gets worse: the bug can affect the entire host node, and container reboots won’t fix it — you need to reboot the hypervisor itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Blame Game
&lt;/h2&gt;

&lt;p&gt;I’ve tracked this bug across multiple issue trackers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;systemd/systemd #24798 — Ubuntu 20.04, September 2022&lt;/li&gt;
&lt;li&gt;systemd/systemd #19926 — Labeled &lt;code&gt;not-our-bug&lt;/code&gt;, June 2021&lt;/li&gt;
&lt;li&gt;Ubuntu Launchpad #1990659 — Expired due to inactivity&lt;/li&gt;
&lt;li&gt;Fedora CoreOS #1296 — Affects PXE/diskless boot&lt;/li&gt;
&lt;li&gt;Red Hat Bugzilla #2111863 — Migrated to Jira, status unknown&lt;/li&gt;
&lt;li&gt;dbus-broker #297 — CentOS Stream 9&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern is always the same: user reports the bug, maintainers ask for debug logs, user either provides them or doesn’t respond fast enough, bug expires or gets closed with “not-our-bug.”&lt;/p&gt;

&lt;p&gt;The systemd team says it’s a kernel issue. The kernel team… well, I haven’t found anyone from the kernel team actively investigating this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Causes (As Best We Can Tell)
&lt;/h2&gt;

&lt;p&gt;The bug appears to involve:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Race conditions&lt;/strong&gt; in mount namespace setup — systemd tries to remount &lt;code&gt;/sys&lt;/code&gt; and &lt;code&gt;/dev&lt;/code&gt; while other unmount operations are happening&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mount propagation issues&lt;/strong&gt; — systemd changes the default from &lt;code&gt;MS_PRIVATE&lt;/code&gt; to &lt;code&gt;MS_SHARED&lt;/code&gt;, causing unexpected interactions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource exhaustion&lt;/strong&gt; — sometimes related to inotify limits (&lt;code&gt;fs.inotify.max_user_instances&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container/virtualization edge cases&lt;/strong&gt; — more prevalent in LXC/LXD environments&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But nobody has done a definitive root cause analysis. The bug is intermittent, hard to reproduce on demand, and affects systems that have been running fine for weeks or months.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Irony
&lt;/h2&gt;

&lt;p&gt;Remember when &lt;code&gt;/etc/init.d/&lt;/code&gt; scripts “just worked”? When starting a service meant running a shell script that executed a binary?&lt;/p&gt;

&lt;p&gt;Systemd brought us dependency management, socket activation, cgroups integration, and dozens of security features like &lt;code&gt;PrivateDevices=&lt;/code&gt;, &lt;code&gt;ProtectSystem=&lt;/code&gt;, and &lt;code&gt;PrivateTmp=&lt;/code&gt;. These are genuinely useful features.&lt;/p&gt;

&lt;p&gt;But they also introduced complexity. The namespace isolation that causes this bug exists because systemd creates a private mount namespace for services with security hardening enabled. It’s a feature. Until it breaks.&lt;/p&gt;

&lt;p&gt;The old init system didn’t have this bug because it didn’t have namespaces. Services ran in the global namespace. Less secure? Yes. But also fewer moving parts to fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workarounds
&lt;/h2&gt;

&lt;p&gt;If you’re affected, here are your options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Disable namespace isolation for affected services:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl edit your-service.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;PrivateDevices&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;
&lt;span class="py"&gt;ProtectHome&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;
&lt;span class="py"&gt;ProtectSystem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Clear corrupted systemd state:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /run/systemd/unit-root/
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Increase inotify limits:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"fs.inotify.max_user_instances=512"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/sysctl.conf
sysctl &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Monitor and auto-restart:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;/3 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; systemctl list-units &lt;span class="nt"&gt;--failed&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; NAMESPACE &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, that last one is a scheduled reboot. That’s where we are.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Should Happen
&lt;/h2&gt;

&lt;p&gt;Someone — Canonical, Red Hat, or the systemd team — needs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a reliable reproduction case&lt;/li&gt;
&lt;li&gt;Add instrumentation to capture the exact kernel/systemd state when the failure occurs&lt;/li&gt;
&lt;li&gt;Do a proper root cause analysis&lt;/li&gt;
&lt;li&gt;Fix it in either the kernel, systemd, or both&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Until then, we’re all just rebooting servers and hoping.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Have you encountered this bug? What’s your workaround?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I’d love to hear from anyone who has done deeper investigation or found a permanent fix.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>infrastructure</category>
    </item>
  </channel>
</rss>
