<?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>DEV Community: Rafi Bagaskara Halilintar</title>
    <description>The latest articles on DEV Community by Rafi Bagaskara Halilintar (@masitings).</description>
    <link>https://dev.to/masitings</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F355442%2F84af0b9d-2cc7-4f18-8712-3be8a9e0e873.png</url>
      <title>DEV Community: Rafi Bagaskara Halilintar</title>
      <link>https://dev.to/masitings</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/masitings"/>
    <language>en</language>
    <item>
      <title>PHP2Shell: Find and Patch Every PHP Server You Run (2026)</title>
      <dc:creator>Rafi Bagaskara Halilintar</dc:creator>
      <pubDate>Thu, 22 Oct 2026 19:13:23 +0000</pubDate>
      <link>https://dev.to/masitings/php2shell-find-and-patch-every-php-server-you-run-2026-5hfp</link>
      <guid>https://dev.to/masitings/php2shell-find-and-patch-every-php-server-you-run-2026-5hfp</guid>
      <description>&lt;p&gt;On October 21, 2026, the PHP project shipped emergency releases for PHP2Shell, two critical bugs in the PHP engine itself, tracked as CVE-2026-109618 and CVE-2026-109619. They allow unauthenticated remote code execution and remote reads of worker memory, and they don't depend on your application code. A "Hello World" script on an affected version is enough. This guide is the practical part: how to find every PHP runtime you're actually serving traffic with, patch it, confirm the old workers are gone, and look for signs someone got there first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Summary: What You'll Learn
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What PHP2Shell is and which setups are at risk&lt;/li&gt;
&lt;li&gt;Why &lt;code&gt;php -v&lt;/code&gt; can lie to you about what's serving requests&lt;/li&gt;
&lt;li&gt;How to patch on Debian/Ubuntu, RHEL-family, Docker and FrankenPHP&lt;/li&gt;
&lt;li&gt;Temporary mitigations if you can't patch today, and their limits&lt;/li&gt;
&lt;li&gt;Log signals worth checking for exploitation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is PHP2Shell, and is my server affected?
&lt;/h2&gt;

&lt;p&gt;PHP2Shell is the name Wiz Research gave to a set of memory-safety bugs in how PHP handles fatal errors, mostly out-of-memory errors, while it processes a request. An interrupted request can leave corrupted internal state behind, and a later request handled by the same worker reuses it. Wiz chained these into pre-authentication RCE against the latest WordPress, Drupal and Joomla container images, and separately showed that an attacker can read chunks of worker memory, which can include passwords, tokens and API keys from other requests.&lt;/p&gt;

&lt;p&gt;Per the PHP security advisory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fixed versions:&lt;/strong&gt; PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At risk:&lt;/strong&gt; every PHP installation serving web requests, including PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed. The PHP team rates mod_php as highest risk, because the demonstrated exploit fires before any application code runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PHP 8.1 and older&lt;/strong&gt; get no upstream release. Your distribution or LTS vendor may ship a backport; the PHP team also published minimal source backports for 7.4, 8.0 and 8.1 for people building from source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Affected range:&lt;/strong&gt; Wiz reports every release back to PHP 5.4 is hit by at least one of the bugs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run anything on PHP, assume you're affected until you've checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I find which PHP versions are actually serving traffic?
&lt;/h2&gt;

&lt;p&gt;This is where most patch rounds go wrong. The CLI binary, the FPM binary and the Apache module are separate packages with separate config, and they can be on different versions. Checking &lt;code&gt;php -v&lt;/code&gt; over SSH tells you about the CLI only.&lt;/p&gt;

&lt;p&gt;Start by finding what's running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;-eo&lt;/span&gt; pid,user,args | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'[p]hp-fpm|[p]hp-cgi|[f]rankenphp|[l]sphp'&lt;/span&gt;
apachectl &lt;span class="nt"&gt;-M&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line shows FPM masters and pools, CGI, FrankenPHP and LiteSpeed processes. The second shows whether Apache has mod_php loaded.&lt;/p&gt;

&lt;p&gt;For PHP-FPM, ask the running binary directly rather than whatever &lt;code&gt;php&lt;/code&gt; is on your PATH:&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="nv"&gt;FPM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; /proc/&lt;span class="si"&gt;$(&lt;/span&gt;pgrep &lt;span class="nt"&gt;-o&lt;/span&gt; php-fpm&lt;span class="si"&gt;)&lt;/span&gt;/exe&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FPM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FPM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pgrep -o&lt;/code&gt; picks the oldest matching process, which is the FPM master. If &lt;code&gt;readlink&lt;/code&gt; prints a path ending in &lt;code&gt;(deleted)&lt;/code&gt;, the binary on disk was replaced by an update but the old master is still running the previous code. That server is not patched yet, whatever the package manager says.&lt;/p&gt;

&lt;p&gt;For containers, check inside each one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Names}}\t{{.Image}}'&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &amp;lt;container&amp;gt; php &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For FrankenPHP, the PHP version is embedded in the binary. A small script run through its CLI mode tells you which one:&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="s1"&gt;'&amp;lt;?php echo PHP_VERSION, PHP_EOL;'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/phpver.php
frankenphp php-cli /tmp/phpver.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Distro packages: why the version number isn't the whole answer
&lt;/h2&gt;

&lt;p&gt;Debian, Ubuntu and RHEL often backport security fixes into the version they already ship instead of moving to upstream 8.3.36 or 8.2.35. A package can be fixed while &lt;code&gt;php -v&lt;/code&gt; still prints an older-looking number. Check the package changelog for the CVE instead:&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;# Debian / Ubuntu&lt;/span&gt;
apt-get changelog php8.3-fpm | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'CVE-2026-10961[89]'&lt;/span&gt;

&lt;span class="c"&gt;# RHEL / Alma / Rocky&lt;/span&gt;
rpm &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;--changelog&lt;/span&gt; php-fpm | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'CVE-2026-10961[89]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap the package name for whatever you run (&lt;code&gt;libapache2-mod-php8.3&lt;/code&gt;, &lt;code&gt;php8.2-cgi&lt;/code&gt;, and so on). No match means your vendor hasn't shipped the fix for that package yet, or you're on a repo that doesn't track it.&lt;/p&gt;

&lt;p&gt;One specific warning from the PHP advisory: if you rely on the volunteer-run &lt;code&gt;rpms.remirepo.net&lt;/code&gt;, it did not have updates on disclosure day. The advisory recommends switching to distribution packages or building from source so you can patch immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I patch PHP for PHP2Shell?
&lt;/h2&gt;

&lt;p&gt;The patch itself is ordinary. The part people skip is restarting every worker.&lt;/p&gt;

&lt;p&gt;On Debian or Ubuntu:&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;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--only-upgrade&lt;/span&gt; &lt;span class="s1"&gt;'php8.3*'&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart php8.3-fpm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On RHEL-family systems:&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;dnf upgrade &lt;span class="s1"&gt;'php*'&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart php-fpm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use mod_php, restart Apache (&lt;code&gt;systemctl restart apache2&lt;/code&gt; or &lt;code&gt;httpd&lt;/code&gt;), since the module lives inside the Apache workers. For Docker, pull or rebuild the image on a fixed tag and redeploy. If you pin images by digest, the old digest stays vulnerable until you update it. For FrankenPHP, upgrade the binary or image to a release built on a fixed PHP version and restart the service. If you followed our &lt;a href="https://termique.app/blog/deploy-laravel-frankenphp-vps/" rel="noopener noreferrer"&gt;Laravel with FrankenPHP guide&lt;/a&gt;, that means replacing the binary and restarting the systemd unit.&lt;/p&gt;

&lt;p&gt;Then re-run the checks from the previous section. You're done with a host when the running binary reports a fixed version (or the changelog shows the CVE), &lt;code&gt;readlink&lt;/code&gt; no longer shows &lt;code&gt;(deleted)&lt;/code&gt;, and the master process started after the update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;lstart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;pgrep &lt;span class="nt"&gt;-o&lt;/span&gt; php-fpm&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What can I do if I can't patch today?
&lt;/h2&gt;

&lt;p&gt;The PHP team is explicit that these measures reduce exposure to specific attack paths and do not make an installation safe. Use them to buy hours, not weeks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Disable session upload progress.&lt;/strong&gt; Set &lt;code&gt;session.upload_progress.enabled=0&lt;/code&gt; in &lt;code&gt;php.ini&lt;/code&gt;, the FPM pool or the server config, then restart every worker. Setting it with &lt;code&gt;ini_set()&lt;/code&gt; or a &lt;code&gt;.user.ini&lt;/code&gt; file does not help, because the bug fires during request parsing, before those apply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lower request size limits.&lt;/strong&gt; &lt;code&gt;post_max_size = 8M&lt;/code&gt; if the app takes no uploads, &lt;code&gt;32M&lt;/code&gt; if it does, with &lt;code&gt;upload_max_filesize&lt;/code&gt; below it. Enforce limits on request body size, multipart part count and header count at the proxy too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove the controllable memory-limit failure.&lt;/strong&gt; The advisory suggests &lt;code&gt;memory_limit=-1&lt;/code&gt;, set where applications can't override it, plus a container or cgroup memory limit as the availability boundary. Don't use &lt;code&gt;RLIMIT_AS&lt;/code&gt; or &lt;code&gt;RLIMIT_DATA&lt;/code&gt; for this; those can trigger the vulnerable allocation failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On PHP 8.5,&lt;/strong&gt; set &lt;code&gt;fatal_error_backtraces=0&lt;/code&gt; and restart workers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In an FPM pool, &lt;code&gt;php_admin_*&lt;/code&gt; values can't be overridden by the application:&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="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;php&lt;/span&gt;/&lt;span class="m"&gt;8&lt;/span&gt;.&lt;span class="m"&gt;3&lt;/span&gt;/&lt;span class="n"&gt;fpm&lt;/span&gt;/&lt;span class="n"&gt;pool&lt;/span&gt;.&lt;span class="n"&gt;d&lt;/span&gt;/&lt;span class="n"&gt;www&lt;/span&gt;.&lt;span class="n"&gt;conf&lt;/span&gt;
&lt;span class="n"&gt;php_admin_flag&lt;/span&gt;[&lt;span class="n"&gt;session&lt;/span&gt;.&lt;span class="n"&gt;upload_progress&lt;/span&gt;.&lt;span class="n"&gt;enabled&lt;/span&gt;] = &lt;span class="n"&gt;off&lt;/span&gt;
&lt;span class="n"&gt;php_admin_value&lt;/span&gt;[&lt;span class="n"&gt;post_max_size&lt;/span&gt;] = &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;
&lt;span class="n"&gt;php_admin_value&lt;/span&gt;[&lt;span class="n"&gt;upload_max_filesize&lt;/span&gt;] = &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember the CLI has its own &lt;code&gt;php.ini&lt;/code&gt; on most distros (&lt;code&gt;/etc/php/8.3/cli/&lt;/code&gt; vs &lt;code&gt;/etc/php/8.3/fpm/&lt;/code&gt; on Debian). Checking &lt;code&gt;php -i&lt;/code&gt; from the shell doesn't tell you what FPM loaded.&lt;/p&gt;

&lt;p&gt;Several CDNs and WAF vendors, including Cloudflare and Fastly, announced rules for PHP2Shell. That's a useful layer, but it only covers traffic that actually goes through them. Anything reachable directly by IP bypasses it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I check whether a server was already exploited?
&lt;/h2&gt;

&lt;p&gt;Wiz notes these bugs fire below the application layer, so application logs may show very little. The signals they recommend looking for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unusual multipart requests followed by memory-limit errors or repeated HTTP 500s&lt;/li&gt;
&lt;li&gt;Unexpectedly large or malformed &lt;code&gt;PHPSESSID&lt;/code&gt; cookies in responses&lt;/li&gt;
&lt;li&gt;Unexplained PHP worker churn&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A first pass on a typical nginx + FPM box:&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="s1"&gt;'Allowed memory size'&lt;/span&gt; /var/log/php&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="nt"&gt;-fpm&lt;/span&gt;.log /var/log/nginx/error.log 2&amp;gt;/dev/null | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="s1"&gt;'exited on signal'&lt;/span&gt; /var/log/php&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="nt"&gt;-fpm&lt;/span&gt;.log 2&amp;gt;/dev/null | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt;
&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'$9 == 500'&lt;/span&gt; /var/log/nginx/access.log | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first finds out-of-memory fatals, the second finds workers that crashed (segfaults are worth a closer look), and the third counts 500s in the current access log, assuming the default combined format. None of these proves exploitation on its own. A cluster of all three around the same multipart POSTs is what deserves an incident.&lt;/p&gt;

&lt;p&gt;If anything looks wrong, treat it as a compromise of everything that process could read: rotate database passwords, API keys and session secrets the app uses, and invalidate active sessions. The memory disclosure bug means secrets can leak without any code execution. Then check the host itself, including &lt;code&gt;authorized_keys&lt;/code&gt;, cron entries and new users, as you would after any RCE. The &lt;a href="https://termique.app/blog/complete-guide-ssh-security-2026/" rel="noopener noreferrer"&gt;complete guide to SSH security&lt;/a&gt; covers what to look at on the access side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where termique fits
&lt;/h2&gt;

&lt;p&gt;None of this needs special tooling, but it gets tedious past a handful of servers. If you manage your hosts in termique, a few things help. Save the version checks above as &lt;a href="https://docs.termique.app/snippets" rel="noopener noreferrer"&gt;snippets&lt;/a&gt; and run them in each session instead of retyping them. Snippets sync as plaintext, which is fine for commands like these but not for anything containing a secret. On Pro, incident investigation can run read-only commands across several selected hosts and assemble the results into a report, which suits the log checks in the last section; anything that changes the system still asks you first. And on shared hosts with command logging turned on, you get a record of who ran the patch and when, which is the question someone will ask later. More on that in &lt;a href="https://termique.app/blog/per-command-audit-logs-shared-ssh-access/" rel="noopener noreferrer"&gt;what a per-command audit log actually catches&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is updating WordPress, Drupal or Laravel enough?
&lt;/h3&gt;

&lt;p&gt;No. The bugs are in PHP itself, not in the CMS or framework. You need a fixed PHP build, and every worker running it, regardless of what the application is.&lt;/p&gt;

&lt;h3&gt;
  
  
  I'm on PHP 8.1 or older. What now?
&lt;/h3&gt;

&lt;p&gt;There's no upstream release for 8.1 and older. Check whether your distribution or LTS vendor shipped a backport (the changelog check above), build from the PHP team's source backports for 7.4 to 8.1, or use this as the push to move to a supported version.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does &lt;code&gt;php -v&lt;/code&gt; tell me if I'm patched?
&lt;/h3&gt;

&lt;p&gt;Only for the CLI, and only if you're on upstream version numbers. Check the running FPM, CGI or Apache module binary, and on distro packages check the changelog for the CVE IDs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a WAF or CDN enough protection?
&lt;/h3&gt;

&lt;p&gt;It reduces exposure for traffic routed through it. It does not protect a server that's reachable directly, and the PHP team's position is that updating is the only complete fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to rotate secrets even if I patched quickly?
&lt;/h3&gt;

&lt;p&gt;If your servers were exposed and you see the log signals above, yes. The memory disclosure path can leak credentials without leaving the kind of trace an RCE would. If logs are clean and you patched early, rotation is a judgment call; for internet-facing apps that handle payments or auth, it's cheap insurance.&lt;/p&gt;

&lt;p&gt;If you're juggling a lot of servers this week, &lt;a href="https://termique.app/download/" rel="noopener noreferrer"&gt;termique&lt;/a&gt; is free to try.&lt;/p&gt;

</description>
      <category>ssh</category>
      <category>terminal</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>termique v0.7.0 – a redesigned workspace</title>
      <dc:creator>Rafi Bagaskara Halilintar</dc:creator>
      <pubDate>Mon, 19 Oct 2026 12:30:40 +0000</pubDate>
      <link>https://dev.to/masitings/termique-v070-a-redesigned-workspace-fd5</link>
      <guid>https://dev.to/masitings/termique-v070-a-redesigned-workspace-fd5</guid>
      <description>&lt;p&gt;termique v0.7.0 is a big one. The whole workspace is rebuilt around one content panel, the investigation page is redesigned, and underneath that sits a long list of reliability, privacy and accessibility fixes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Update from the in-app updater when the prompt appears, or grab a fresh build from the &lt;a href="https://termique.app/download/" rel="noopener noreferrer"&gt;download page&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A redesigned workspace.&lt;/strong&gt; A top bar and an Explorer sidebar now frame a single content panel, replacing the old sidebar and tab strip. Empty and success states get new illustrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calmer lists.&lt;/strong&gt; Each list's views now sit in one dropdown next to its title instead of a row of controls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring at a glance.&lt;/strong&gt; The monitoring detail view shows live resources as one strip instead of donut charts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A redesigned investigation page.&lt;/strong&gt; Commands are grouped into one step list you can skip, edit or run inline, next to a case file that shows the status, the hosts involved and the report verdict.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Investigations always reach their report.&lt;/strong&gt; A failed turn can be retried without retyping it, and a step that doesn't name a host asks which host to run on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Command logging is off by default for new hosts.&lt;/strong&gt; The share dialog offers to turn it on when it's off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recover from a changed host key.&lt;/strong&gt; When a host's key has changed, you can forget the old key and reconnect, behind a clear warning and a confirmation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard resizing.&lt;/strong&gt; Split panes, the Files view and the assistant panel can be resized from the keyboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smaller minimum window size&lt;/strong&gt;, so the app fits small laptop screens and scaled displays.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Command logging is now a choice you make per host. Hosts you already have keep their current setting, and when you share a host without logging, the share dialog tells you and lets you turn it on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A changed host key is either a server rebuild or something you should stop and look at. The new flow doesn't skip that decision, it just stops you from having to edit &lt;code&gt;known_hosts&lt;/code&gt; by hand once you've made it. If you're not sure which case you're in, compare the new fingerprint with the server's before you confirm. &lt;a href="https://termique.app/blog/ssh-host-key-verification-explained/" rel="noopener noreferrer"&gt;Host key verification explained&lt;/a&gt; walks through how to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Improvements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The master password lockout can no longer be reset by restarting the app.&lt;/li&gt;
&lt;li&gt;Commands proposed by the assistant are checked more strictly before they run automatically.&lt;/li&gt;
&lt;li&gt;The AI assistant no longer receives your hosts' addresses or ports, only each host's label and user.&lt;/li&gt;
&lt;li&gt;Share recipients are looked up without putting their email address in a URL.&lt;/li&gt;
&lt;li&gt;Assistant messages no longer load remote images.&lt;/li&gt;
&lt;li&gt;Credentials are removed from command logs before upload.&lt;/li&gt;
&lt;li&gt;Error reports no longer contain IP addresses, host names, email addresses or tokens.&lt;/li&gt;
&lt;li&gt;Faster startup: each page now loads the first time you open it, and charts and message formatting load only when needed.&lt;/li&gt;
&lt;li&gt;An upgraded SSH engine that fixes several security issues.&lt;/li&gt;
&lt;li&gt;If your system's web view is too old to draw the new interface, the app now tells you what to update.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Your server addresses now stay on your machine when you use the assistant. It only sees the name you gave a host and the user you log in as.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two of these are worth calling out. Labels are often less sensitive than addresses, so if your host labels contain internal hostnames or IPs, the assistant will still see those; rename them if that matters to you. And command logs are stored on termique's backend without end-to-end encryption, which is why stripping credentials before upload, plus logging being opt-in for new hosts, matters. If you're deciding whether to turn logging on for a shared host, &lt;a href="https://termique.app/blog/per-command-audit-logs-shared-ssh-access/" rel="noopener noreferrer"&gt;what a per-command audit log actually catches&lt;/a&gt; covers the tradeoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug fixes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Fixed SSH sessions hanging or blocking each other when one channel is slow, and connections left open after a failed connect.&lt;/li&gt;
&lt;li&gt;Fixed shared hosts disappearing after a background refresh, and save conflicts when editing a host.&lt;/li&gt;
&lt;li&gt;Fixed terminal scrollback being lost when tabs are merged or the app locks.&lt;/li&gt;
&lt;li&gt;Commands that the assistant or an investigation runs on a shared host now appear in the owner's activity log.&lt;/li&gt;
&lt;li&gt;Fixed the window freezing during database, keychain and file operations.&lt;/li&gt;
&lt;li&gt;Rounded window corners now stay when the window is maximized on macOS.&lt;/li&gt;
&lt;li&gt;The command palette, Explorer controls and previously unlabelled buttons now work properly with the keyboard and screen readers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Update to v0.7.0 to get the new workspace and these fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://termique.app/blog/termique-v0-6-5/" rel="noopener noreferrer"&gt;termique v0.6.5 – jump host support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://termique.app/blog/ai-coding-agents-ssh-command-approval/" rel="noopener noreferrer"&gt;AI coding agents over SSH: who approves the commands they run&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://termique.app/blog/per-command-audit-logs-shared-ssh-access/" rel="noopener noreferrer"&gt;What a per-command audit log actually catches&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ssh</category>
      <category>terminal</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>ssh-keygen Best Practices for 2026: Keys, Rounds, FIDO</title>
      <dc:creator>Rafi Bagaskara Halilintar</dc:creator>
      <pubDate>Mon, 19 Oct 2026 12:17:38 +0000</pubDate>
      <link>https://dev.to/masitings/ssh-keygen-best-practices-for-2026-keys-rounds-fido-15nk</link>
      <guid>https://dev.to/masitings/ssh-keygen-best-practices-for-2026-keys-rounds-fido-15nk</guid>
      <description>&lt;p&gt;&lt;strong&gt;Quick Summary: What You'll Learn&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The one &lt;code&gt;ssh-keygen&lt;/code&gt; command most people should run in 2026, and what each flag in it does.&lt;/li&gt;
&lt;li&gt;How to choose between Ed25519, RSA, FIDO-backed &lt;code&gt;-sk&lt;/code&gt; keys and the new post-quantum hybrid &lt;code&gt;mldsa44-ed25519&lt;/code&gt; type that OpenSSH 10.6 enabled.&lt;/li&gt;
&lt;li&gt;Why &lt;code&gt;-a&lt;/code&gt; (KDF rounds), a real comment and a passphrase matter more than key size.&lt;/li&gt;
&lt;li&gt;The file permissions, &lt;code&gt;~/.ssh/config&lt;/code&gt; lines and fingerprint checks that keep a good key from becoming a bad habit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most ssh-keygen best practices guides stop at "use Ed25519". That's still the right default in 2026, but it's only one decision out of five. The key type, the passphrase, the KDF rounds that protect that passphrase, the comment that tells you which laptop the key came from, and where the private key ends up afterwards all decide whether a stolen file is a non-event or an incident. OpenSSH also shipped changes this year that affect new keys, so some older advice is out of date. Every flag below is checked against the current OpenBSD &lt;code&gt;ssh-keygen(1)&lt;/code&gt; manual and the OpenSSH release notes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the best ssh-keygen command to run in 2026?
&lt;/h2&gt;

&lt;p&gt;For a personal key on a laptop or workstation, this is a sane default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-a&lt;/span&gt; 100 &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"you@laptop-2026-10"&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/id_ed25519_laptop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each part does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-t ed25519&lt;/code&gt; picks the key type. Since OpenSSH 9.5 (October 2023), Ed25519 is what &lt;code&gt;ssh-keygen&lt;/code&gt; generates when you pass no &lt;code&gt;-t&lt;/code&gt; at all, so this flag is mostly there to be explicit in scripts and docs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-a 100&lt;/code&gt; sets the number of bcrypt_pbkdf rounds used to encrypt the private key with your passphrase. The manual's default is 32. Higher means slower passphrase checks and slower offline cracking if the file is stolen. More on choosing a number below.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-C "you@laptop-2026-10"&lt;/code&gt; sets the comment. The default is &lt;code&gt;user@host&lt;/code&gt;, which tells you almost nothing two years later. Put the device and the date in it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-f ~/.ssh/id_ed25519_laptop&lt;/code&gt; names the file, so the key doesn't silently overwrite an existing &lt;code&gt;id_ed25519&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When it prompts for a passphrase, give it one. The manual's own guidance is that good passphrases are 10 to 30 characters, not simple sentences, with mixed character classes. A random phrase from a password manager is easier than inventing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which SSH key algorithm should you choose?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ssh-keygen -t&lt;/code&gt; accepts &lt;code&gt;ecdsa&lt;/code&gt;, &lt;code&gt;ecdsa-sk&lt;/code&gt;, &lt;code&gt;ed25519&lt;/code&gt;, &lt;code&gt;ed25519-sk&lt;/code&gt;, &lt;code&gt;mldsa44-ed25519&lt;/code&gt; and &lt;code&gt;rsa&lt;/code&gt; in the current manual. In practice, the choice looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ed25519&lt;/strong&gt;: the default for new keys. Small public keys, fixed length (&lt;code&gt;-b&lt;/code&gt; is ignored), and supported by OpenSSH since 6.5 (January 2014). Pick this unless something below applies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RSA&lt;/strong&gt;: only for systems that can't do Ed25519 (some network gear, older appliances, a few hosted services). The manual's default size is 3072 bits and it calls 3072 "generally sufficient". Many teams use &lt;code&gt;-b 4096&lt;/code&gt; anyway; that's fine, just slower.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ECDSA&lt;/strong&gt;: no strong reason to choose it over Ed25519 for user keys today. &lt;code&gt;-b&lt;/code&gt; selects the curve (256, 384 or 521); other values fail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ed25519-SK / ECDSA-SK&lt;/strong&gt;: keys backed by a FIDO authenticator (a hardware security key). The private part never leaves the hardware. Best choice when you have the token and your servers run a modern OpenSSH.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mldsa44-ed25519&lt;/strong&gt;: a hybrid signature that combines ML-DSA-44 (a post-quantum scheme) with Ed25519. Read the next section before adopting it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing people still mix up: OpenSSH 8.8 disabled the old &lt;code&gt;ssh-rsa&lt;/code&gt; &lt;em&gt;signature algorithm&lt;/em&gt; (RSA with SHA-1) by default. That did not make RSA &lt;em&gt;keys&lt;/em&gt; obsolete. The same RSA key signs with &lt;code&gt;rsa-sha2-256&lt;/code&gt; or &lt;code&gt;rsa-sha2-512&lt;/code&gt;, and the release notes say exactly that. If an old RSA key stopped working against a new server, the usual culprit is a client or server too old to negotiate SHA-2 signatures, not the key itself.&lt;/p&gt;

&lt;p&gt;To see which key types your local OpenSSH supports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-V&lt;/span&gt;
ssh &lt;span class="nt"&gt;-Q&lt;/span&gt; key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Is the post-quantum mldsa44-ed25519 key ready to use?
&lt;/h3&gt;

&lt;p&gt;OpenSSH 10.4 (July 2026) added &lt;code&gt;mldsa44-ed25519&lt;/code&gt; as an experimental, off-by-default composite signature scheme. OpenSSH 10.6 (released 2026-10-06) enabled it as a standard algorithm and dropped the &lt;code&gt;@openssh.com&lt;/code&gt; suffix the experimental version used. The release notes are explicit that keys generated with the experimental support must be regenerated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; mldsa44-ed25519 &lt;span class="nt"&gt;-a&lt;/span&gt; 100 &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"you@laptop-2026-10-pq"&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/id_mldsa44_laptop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Practical guidance for now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both ends need an OpenSSH release that understands the new algorithm name. A server on an older release won't accept the key, so check with &lt;code&gt;ssh -Q key&lt;/code&gt; on the server side too.&lt;/li&gt;
&lt;li&gt;If you generated one of these during the experimental 10.4/10.5 window, regenerate it on 10.6 rather than debugging why it stopped matching.&lt;/li&gt;
&lt;li&gt;Keep an Ed25519 key alongside it. Post-quantum key exchange already protects session traffic against record-now-decrypt-later attacks in recent OpenSSH releases; the signature side matters for a future attacker forging authentication, which is a slower-moving risk. There's no rush to make the hybrid key your only key.&lt;/li&gt;
&lt;li&gt;Third-party clients, libraries and hosted git services may not parse it yet. Test before you depend on it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How many -a rounds should you use?
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;-a&lt;/code&gt; value only matters for keys that have a passphrase. It controls how much work it takes to test one passphrase guess against a stolen private key file. The man page states the default (32) and that higher numbers mean slower verification and more resistance to brute force; it doesn't prescribe a value.&lt;/p&gt;

&lt;p&gt;A workable way to pick one: generate a throwaway key with a candidate value, then time how long unlocking it takes on your slowest machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-a&lt;/span&gt; 100 &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="s1"&gt;'test-only-passphrase'&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /tmp/kdf-test &lt;span class="nt"&gt;-q&lt;/span&gt;
&lt;span class="nb"&gt;time &lt;/span&gt;ssh-keygen &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="s1"&gt;'test-only-passphrase'&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /tmp/kdf-test &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;span class="nb"&gt;rm&lt;/span&gt; /tmp/kdf-test /tmp/kdf-test.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-y&lt;/code&gt; reads the private key and prints the public key, so it has to decrypt the file first. If it takes a fraction of a second, you can go higher. If it's annoying on every connection, an agent (below) means you only pay it once per session. Don't type real passphrases on the command line like this; &lt;code&gt;-N&lt;/code&gt; and &lt;code&gt;-P&lt;/code&gt; end up in shell history. It's fine for a throwaway test key only.&lt;/p&gt;

&lt;p&gt;You don't need a new key to raise the rounds on an existing one. Changing the passphrase re-encrypts the file and accepts a new &lt;code&gt;-a&lt;/code&gt; value (you can keep the same passphrase):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; 100 &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/id_ed25519_laptop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Should every SSH key have a passphrase?
&lt;/h2&gt;

&lt;p&gt;For keys that live on a laptop or a phone, yes. A passphrase turns "someone copied my &lt;code&gt;~/.ssh&lt;/code&gt; folder" from an immediate compromise into "they have an encrypted file and a lot of guessing ahead of them". The manual is blunt about the downside: there is no way to recover a lost passphrase. If you lose it, you generate a new key and replace the public key everywhere. Store it in your password manager.&lt;/p&gt;

&lt;p&gt;The exceptions are keys that a machine has to use unattended: host keys (the manual says these must have an empty passphrase), and automation keys on CI runners or deploy boxes. For those, compensate on the server side instead. Restrict what the key can do in &lt;code&gt;authorized_keys&lt;/code&gt; (a &lt;code&gt;from=&lt;/code&gt; source address and a forced command, for example), or use short-lived certificates so a leaked key expires on its own. Our comparison of &lt;a href="https://termique.app/blog/ssh-certificate-authority-vs-public-key-auth-teams/" rel="noopener noreferrer"&gt;SSH certificates vs plain public keys&lt;/a&gt; covers when certificates are worth the setup.&lt;/p&gt;

&lt;p&gt;To stop typing the passphrase on every connection, let &lt;code&gt;ssh-agent&lt;/code&gt; hold the decrypted key for the session. In &lt;code&gt;~/.ssh/config&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;Host&lt;/span&gt; *
    &lt;span class="k"&gt;AddKeysToAgent&lt;/span&gt; &lt;span class="no"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;AddKeysToAgent yes&lt;/code&gt;, the key and passphrase are added to a running agent the first time the key is loaded from a file. If you hit "incorrect passphrase supplied to decrypt private key" along the way, our &lt;a href="https://termique.app/blog/ssh-key-passphrase-not-working/" rel="noopener noreferrer"&gt;passphrase troubleshooting guide&lt;/a&gt; walks through the usual causes.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should you use a FIDO hardware key with ssh-keygen?
&lt;/h2&gt;

&lt;p&gt;When the key protects something you'd hate to lose, and you already carry a security key. With &lt;code&gt;ed25519-sk&lt;/code&gt;, the file in &lt;code&gt;~/.ssh&lt;/code&gt; is only a key handle. The actual private key sits inside the authenticator and can't be exported, so copying the file without the hardware gets an attacker nothing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&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="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"you@yubikey-2026-10"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-O resident&lt;/code&gt; stores the key handle on the authenticator itself, so you can load it on another computer with &lt;code&gt;ssh-keygen -K&lt;/code&gt; (or &lt;code&gt;ssh-add -K&lt;/code&gt;). The manual notes the tradeoff: storing both parts on the device makes a stolen authenticator more useful to an attacker. Resident keys typically require a PIN on the token.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-O verify-required&lt;/code&gt; requires user verification (currently a PIN) for each signature, not just a touch. Not every authenticator supports it.&lt;/li&gt;
&lt;li&gt;By default every signature needs a physical touch. &lt;code&gt;-O no-touch-required&lt;/code&gt; turns that off, and &lt;code&gt;sshd&lt;/code&gt; refuses such signatures unless the &lt;code&gt;authorized_keys&lt;/code&gt; entry explicitly allows it. Leave the touch requirement on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The caveats: the server needs a modern OpenSSH to accept &lt;code&gt;-sk&lt;/code&gt; keys, and you should register a second authenticator as a backup, because losing the only token locks you out the same way a lost passphrase does.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should you name, store and check SSH keys?
&lt;/h2&gt;

&lt;p&gt;Generating the key well is half of it. These habits keep it manageable later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One key per device.&lt;/strong&gt; If a laptop is lost, you revoke one public key instead of rotating a key that lives on four machines. The comment is how you tell them apart in &lt;code&gt;authorized_keys&lt;/code&gt;, which is why &lt;code&gt;-C "you@laptop-2026-10"&lt;/code&gt; beats the default. Our guide to &lt;a href="https://termique.app/blog/managing-multiple-ssh-keys-across-devices/" rel="noopener noreferrer"&gt;managing multiple SSH keys across devices&lt;/a&gt; has a naming scheme that scales past three machines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix a bad comment without regenerating:&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;ssh-keygen &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"you@laptop-2026-10"&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/id_ed25519_laptop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Permissions.&lt;/strong&gt; &lt;code&gt;ssh(1)&lt;/code&gt; ignores a private key file that other users can read, and recommends that &lt;code&gt;~/.ssh&lt;/code&gt; itself is accessible only by you:&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;chmod &lt;/span&gt;700 ~/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 ~/.ssh/id_ed25519_laptop
&lt;span class="nb"&gt;chmod &lt;/span&gt;644 ~/.ssh/id_ed25519_laptop.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Point each host at the right key.&lt;/strong&gt; Without this, &lt;code&gt;ssh&lt;/code&gt; offers every key in your agent in turn, which can trip a server's &lt;code&gt;MaxAuthTries&lt;/code&gt; before it reaches the right one:&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;Host&lt;/span&gt; prod-bastion
    &lt;span class="k"&gt;HostName&lt;/span&gt; bastion.example.com
    &lt;span class="k"&gt;User&lt;/span&gt; deploy
    &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/id_ed25519_laptop
    &lt;span class="k"&gt;IdentitiesOnly&lt;/span&gt; &lt;span class="no"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;IdentitiesOnly yes&lt;/code&gt; tells &lt;code&gt;ssh&lt;/code&gt; to use only the configured identity files, even when the agent holds others.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install the public key, and only that one:&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;ssh-copy-id &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/id_ed25519_laptop.pub deploy@bastion.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-i&lt;/code&gt; makes sure only the key you named gets installed, with the comment from the &lt;code&gt;.pub&lt;/code&gt; file, rather than everything in your agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check fingerprints.&lt;/strong&gt; When you add a key to a server or a git host, compare fingerprints instead of eyeballing base64:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-lf&lt;/span&gt; ~/.ssh/id_ed25519_laptop.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SHA-256 is the default fingerprint hash. If you lost the &lt;code&gt;.pub&lt;/code&gt; file, regenerate it from the private key with &lt;code&gt;ssh-keygen -y -f ~/.ssh/id_ed25519_laptop &amp;gt; ~/.ssh/id_ed25519_laptop.pub&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan the rotation now.&lt;/strong&gt; Put the year in the comment and you'll know which keys are overdue. Our &lt;a href="https://termique.app/blog/ssh-key-rotation-best-practices/" rel="noopener noreferrer"&gt;SSH key rotation best practices&lt;/a&gt; post covers how often to rotate and how to do it without locking anyone out, and the &lt;a href="https://termique.app/blog/ssh-key-management-for-teams/" rel="noopener noreferrer"&gt;team key management guide&lt;/a&gt; covers offboarding.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ssh-keygen advice is outdated in 2026?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Generate RSA 2048."&lt;/strong&gt; Still accepted by &lt;code&gt;sshd&lt;/code&gt; (the &lt;code&gt;RequiredRSASize&lt;/code&gt; default is 1024 bits), but there's no reason to create new 2048-bit RSA keys when Ed25519 is available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"DSA is fine for old systems."&lt;/strong&gt; OpenSSH 10.0 removed DSA support entirely. Any remaining &lt;code&gt;id_dsa&lt;/code&gt; key needs replacing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"You must pass -t ed25519 or you get RSA."&lt;/strong&gt; Not since OpenSSH 9.5; Ed25519 is the default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Use -o for the new key format."&lt;/strong&gt; The OpenSSH private key format has been the default for years, and current &lt;code&gt;ssh-keygen&lt;/code&gt; doesn't list &lt;code&gt;-o&lt;/code&gt; in its synopsis at all. Use &lt;code&gt;-m PEM&lt;/code&gt; only when an old tool insists on PEM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Post-quantum SSH keys aren't a thing yet."&lt;/strong&gt; As of OpenSSH 10.6 they are, as a hybrid. They're just early.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where does termique fit into SSH key management?
&lt;/h2&gt;

&lt;p&gt;termique is a free, native ssh manager we build, with end-to-end encrypted credentials. On the key side, it does a few specific things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Generate or import.&lt;/strong&gt; You can generate &lt;code&gt;ed25519&lt;/code&gt; (recommended) or &lt;code&gt;rsa&lt;/code&gt; keys in Settings → SSH keys, or import an existing private key in OpenSSH or PEM format, including passphrase-protected keys. For FIDO &lt;code&gt;-sk&lt;/code&gt; keys and the new &lt;code&gt;mldsa44-ed25519&lt;/code&gt; type, generate with &lt;code&gt;ssh-keygen&lt;/code&gt; as shown above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encrypted before it's stored.&lt;/strong&gt; Private keys are encrypted on your device with a key derived from your master password before they're written locally or synced. The sync backend stores ciphertext only. Hosts, groups and snippets sync as plaintext metadata, so don't put secrets in host names or snippets. The &lt;a href="https://termique.app/security/" rel="noopener noreferrer"&gt;security page&lt;/a&gt; has the full model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same keys on desktop and phone.&lt;/strong&gt; Keys sync across macOS, Windows, Linux and iOS, so the key you use from your laptop is available in the mobile app without emailing a private key to yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is a real tradeoff against the one-key-per-device advice above. A synced key is one key on several devices: convenient, and protected by end-to-end encryption, but a lost phone means rotating that key everywhere it's authorized. If your policy is strictly one key per device, generate a separate key on each device instead; termique handles both, and the comment you set still tells you which is which.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the most secure SSH key algorithm in 2026?
&lt;/h3&gt;

&lt;p&gt;For everyday use, Ed25519 backed by a FIDO authenticator (&lt;code&gt;ed25519-sk&lt;/code&gt;), because the private key can't be copied off the hardware. Without a hardware key, plain Ed25519 with a strong passphrase. The &lt;code&gt;mldsa44-ed25519&lt;/code&gt; hybrid adds post-quantum resistance to signatures, but needs OpenSSH 10.6 on both ends and isn't widely supported outside OpenSSH yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is RSA 4096 better than Ed25519?
&lt;/h3&gt;

&lt;p&gt;Not in any way that matters for most users. RSA 4096 is secure, but keys and signatures are much larger and slower. The manual considers 3072-bit RSA sufficient. Use RSA only for systems that can't handle Ed25519.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I add a passphrase to an existing SSH key?
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;ssh-keygen -p -f ~/.ssh/your_key&lt;/code&gt;. It prompts for the old passphrase (press Enter if there is none) and the new one twice. Add &lt;code&gt;-a 100&lt;/code&gt; to raise the KDF rounds at the same time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use the same SSH key on every computer?
&lt;/h3&gt;

&lt;p&gt;Usually not. One key per device means a lost laptop costs you one revocation instead of a rotation across every server. Syncing one key across devices with end-to-end encryption is a valid choice for convenience, as long as you accept that losing any of those devices means rotating it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does the -C flag do in ssh-keygen?
&lt;/h3&gt;

&lt;p&gt;It sets the key's comment, which is stored in the private key file and appended to the public key line in &lt;code&gt;authorized_keys&lt;/code&gt;. It has no security effect, but a comment like &lt;code&gt;you@laptop-2026-10&lt;/code&gt; tells you which device and year a key belongs to when you audit a server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try termique free
&lt;/h2&gt;

&lt;p&gt;If you want your keys, hosts and snippets on every machine you use, with private keys end-to-end encrypted before they leave the device, &lt;a href="https://termique.app/download/" rel="noopener noreferrer"&gt;download termique&lt;/a&gt; and start on the free plan.&lt;/p&gt;

</description>
      <category>ssh</category>
      <category>terminal</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>Alright my first weekly challenge on Hacktoberfest!</title>
      <dc:creator>Rafi Bagaskara Halilintar</dc:creator>
      <pubDate>Fri, 02 Oct 2026 20:20:47 +0000</pubDate>
      <link>https://dev.to/masitings/alright-my-first-weekly-challenge-on-hacktoberfest-n2c</link>
      <guid>https://dev.to/masitings/alright-my-first-weekly-challenge-on-hacktoberfest-n2c</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/masitings/kas-rumah-a-receipt-reading-expense-tracker-for-my-wife-that-never-leaves-our-laptop-28jo" class="crayons-story__hidden-navigation-link"&gt;Kas Rumah: a receipt-reading expense tracker for my wife that never leaves our laptop&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/masitings" class="crayons-avatar  crayons-avatar--l  "&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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F355442%2F84af0b9d-2cc7-4f18-8712-3be8a9e0e873.png" alt="masitings profile" class="crayons-avatar__image" width="460" height="460"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/masitings" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Rafi Bagaskara Halilintar
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Rafi Bagaskara Halilintar
                
                
              
              &lt;div id="story-author-preview-content-4788965" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/masitings" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F355442%2F84af0b9d-2cc7-4f18-8712-3be8a9e0e873.png" class="crayons-avatar__image" alt="" width="460" height="460"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Rafi Bagaskara Halilintar&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/masitings/kas-rumah-a-receipt-reading-expense-tracker-for-my-wife-that-never-leaves-our-laptop-28jo" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 2&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/masitings/kas-rumah-a-receipt-reading-expense-tracker-for-my-wife-that-never-leaves-our-laptop-28jo" id="article-link-4788965"&gt;
          Kas Rumah: a receipt-reading expense tracker for my wife that never leaves our laptop
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devchallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devchallenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/weekendchallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;weekendchallenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/hf26challenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;hf26challenge&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/masitings/kas-rumah-a-receipt-reading-expense-tracker-for-my-wife-that-never-leaves-our-laptop-28jo#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>devchallenge</category>
      <category>opensource</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Kas Rumah: a receipt-reading expense tracker for my wife that never leaves our laptop</title>
      <dc:creator>Rafi Bagaskara Halilintar</dc:creator>
      <pubDate>Fri, 02 Oct 2026 20:13:35 +0000</pubDate>
      <link>https://dev.to/masitings/kas-rumah-a-receipt-reading-expense-tracker-for-my-wife-that-never-leaves-our-laptop-28jo</link>
      <guid>https://dev.to/masitings/kas-rumah-a-receipt-reading-expense-tracker-for-my-wife-that-never-leaves-our-laptop-28jo</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;My wife runs our household money. Groceries, the electricity bill, gas refills, the occasional ShopeeFood order, envelopes for weddings. She has tried expense apps before and every one of them died the same way: typing each purchase by hand is tedious, so after a week the app gets ignored and the receipts pile up in her bag.&lt;/p&gt;

&lt;p&gt;Kas Rumah ("house cash" in Indonesian) removes the typing. She can take a photo of a receipt, upload a bank transfer screenshot, or type something like &lt;code&gt;beli sayur 45rb sama galon 20rb&lt;/code&gt; ("bought vegetables 45k and a water gallon 20k"). A local Gemma model turns it into categorized expenses, she checks them on a review screen, and once a week she gets a short summary in casual Indonesian telling her where the money went and which budget is running hot.&lt;/p&gt;

&lt;p&gt;Everything runs on our laptop at home. Her phone talks to it over our Wi-Fi.&lt;/p&gt;

&lt;p&gt;To be upfront: she hasn't used it yet. I built it over this weekend, and everything in this post was tested with sample receipts and screenshots, not ours. Handing it to her is the next step, and her first week with it will tell me more than any test set did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fticanh509mkp3qugdui9.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fticanh509mkp3qugdui9.png" alt="Expenses screen for October showing a monthly total of Rp1,094,856, a per-category breakdown, and five receipts saved on 3 October" width="800" height="1738"&gt;&lt;/a&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F577os9c5segdsls3wxbm.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F577os9c5segdsls3wxbm.png" alt="Review screen with a clinic receipt read as Rp765,000 under Health, with editable date, merchant, category and notes" width="800" height="1738"&gt;&lt;/a&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqhp9mxl5yj14hcgzpmge.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqhp9mxl5yj14hcgzpmge.png" alt="Weekly summary screen with a short paragraph written by Gemma, the week's total, and this week compared to the 4-week average" width="800" height="1738"&gt;&lt;/a&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc9fbdvh8ji8gdg9buyvv.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc9fbdvh8ji8gdg9buyvv.png" alt="Review screen with an Indomaret receipt that has a voucher, a cancelled item and a discount, read correctly as Rp183,200" width="800" height="1738"&gt;&lt;/a&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1merwvy8w46kbr9z3ktj.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1merwvy8w46kbr9z3ktj.png" alt="Expenses screen earlier in the day with three saved receipts totalling Rp146,656" width="800" height="1738"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/masitings" rel="noopener noreferrer"&gt;
        masitings
      &lt;/a&gt; / &lt;a href="https://github.com/masitings/kasrumah" rel="noopener noreferrer"&gt;
        kasrumah
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Kas Rumah — household expense tracker. Local Gemma 3 (Ollama) reads receipts / transfer screenshots / typed notes. Laravel 13 + Inertia + React, 100% offline AI.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Kas Rumah&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A household expense tracker for a spouse managing the family's budget. Snap a photo of a receipt or bank transfer screenshot, or type a casual note like "beli sayur 45rb" — a &lt;strong&gt;local Gemma 3 model via Ollama&lt;/strong&gt; parses it into a categorized expense. Nothing ever leaves this laptop: zero cloud AI, zero third-party expense APIs.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Stack&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Laravel 13 + Inertia (React, TypeScript) with the official starter kit&lt;/li&gt;
&lt;li&gt;SQLite or MySQL database&lt;/li&gt;
&lt;li&gt;Ollama running &lt;code&gt;gemma3:4b&lt;/code&gt; (vision-capable) at &lt;code&gt;http://127.0.0.1:11434&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Money is stored as &lt;strong&gt;integer rupiah&lt;/strong&gt; (no decimals)&lt;/li&gt;
&lt;li&gt;UI copy is casual Indonesian; code, comments, and commit messages are English&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;One-time Setup&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;composer install
npm install
cp .env.example .env
php artisan key:generate
php artisan migrate&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Make sure Ollama is installed and the model is downloaded:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;ollama pull gemma3:4b   &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; ~3.3 GB&lt;/span&gt;
ollama serve&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Optional overrides in &lt;code&gt;.env&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-dotenv notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-v"&gt;OLLAMA_URL&lt;/span&gt;&lt;span class="pl-k"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;http://127.0.0.1:11434&lt;/span&gt;
&lt;span class="pl-v"&gt;OLLAMA_MODEL&lt;/span&gt;&lt;span class="pl-k"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;gemma3:4b&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Development&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;composer run dev   &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; sail-style:&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/masitings/kasrumah" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Laravel with the React starter kit (Inertia), MySQL, and &lt;a href="https://deepmind.google/models/gemma/" rel="noopener noreferrer"&gt;Gemma 3 4B&lt;/a&gt; running through &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; on an M5 MacBook with 16 GB of RAM.&lt;/p&gt;

&lt;p&gt;The app has five screens: Catat (record), Review, Pengeluaran (expenses), Ringkasan (summary), and Budget. The interesting part is how little I ask the model to do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading receipts
&lt;/h3&gt;

&lt;p&gt;One &lt;code&gt;ExpenseExtractor&lt;/code&gt; service sends either an image or a line of text to Ollama's &lt;code&gt;/api/chat&lt;/code&gt;. Two things made the 4B model usable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Structured outputs.&lt;/strong&gt; Instead of asking nicely for JSON, I pass a JSON schema in Ollama's &lt;code&gt;format&lt;/code&gt; field, with the category list as an &lt;code&gt;enum&lt;/code&gt;. The model cannot invent a category, and I never parse broken JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate prompts for images and text.&lt;/strong&gt; My first version used one prompt for both and said "one receipt is one expense, don't split items." That rule made the model return nothing for &lt;code&gt;sayur 45rb sama galon 20rb&lt;/code&gt;, because the text clearly has two expenses. Splitting the prompts fixed it. The image prompt also tells the model to ignore payment lines like TUNAI (cash), KEMBALI (change) and QRIS, because on the first run it logged the cash handed to the cashier as a second expense.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Images are downscaled to 1600px before they go to the model, which keeps each receipt at about 4 seconds.&lt;/p&gt;

&lt;p&gt;Nothing the model returns is saved directly. Every result lands on the Review screen as a draft, and rows with low confidence are highlighted. She fixes what is wrong and taps Simpan.&lt;/p&gt;

&lt;h3&gt;
  
  
  The weekly summary
&lt;/h3&gt;

&lt;p&gt;I don't let the model do math. PHP computes this week's total per category, the 4-week average, and budget usage, then hands Gemma those numbers with a prompt that says to use only them. Gemma's job is just to turn a small JSON object into three to five friendly sentences. A 4B model is good at that and bad at arithmetic, so that is the split.&lt;/p&gt;

&lt;h3&gt;
  
  
  Voice input and iPhone photos for free
&lt;/h3&gt;

&lt;p&gt;I planned to add Whisper for voice notes. Then I realized the iPhone keyboard already has a dictation button, so the text field is the voice input. Same story with HEIC photos: setting &lt;code&gt;accept="image/jpeg,image/png"&lt;/code&gt; on the file input makes iOS Safari convert to JPEG before upload, so the server never sees a HEIC file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where my AI coding agent cheated, and the honest numbers
&lt;/h3&gt;

&lt;p&gt;I used an AI coding agent for a lot of the build and gave it a regression test: three receipt photos and four text samples with known answers. It came back with 7/7 passing. Then I read the prompt. It had written the expected totals and store names from the test receipts straight into the instructions. The tests passed because the answers were in the question.&lt;/p&gt;

&lt;p&gt;I made it remove every value from the test set, split the fixtures into a tuning set and a holdout set it was only allowed to run once, and run every case three times. These are the real numbers for &lt;code&gt;gemma3:4b&lt;/code&gt; on that small set of sample receipts and screenshots:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Correct&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Amount (images)&lt;/td&gt;
&lt;td&gt;4 of 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Category (images)&lt;/td&gt;
&lt;td&gt;5 of 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merchant name (images)&lt;/td&gt;
&lt;td&gt;2 of 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text input&lt;/td&gt;
&lt;td&gt;4 of 4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Amounts and categories, the fields that matter for a household budget, are solid. Merchant names are weak: it could not read a stylized store logo, and for a transfer screenshot it picked the payment app instead of the person receiving the money. One pharmacy receipt read 23,000 instead of 25,000, the same way every time. Those are all things the Review screen catches in a second, so I stopped tuning there instead of chasing a nicer number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;This app reads my family's receipts and bank transfer screenshots. Those show what we buy, where we shop, who we send money to, and sometimes account details. I was not willing to send that to a third-party API, and I don't think my wife would have agreed to use it if I had. With Gemma running locally, the photos and the numbers stay on a laptop in our house.&lt;/p&gt;

&lt;p&gt;It also costs nothing to run. A cloud vision API would be cheap per call, but it would still be a monthly bill for a tool whose whole point is helping us spend less.&lt;/p&gt;

&lt;p&gt;And because the model is open, I could pick the size that fits the job. A 4B model on a laptop is fast enough for a person waiting with their phone in hand, and the design (strict schema, PHP does the math, a human confirms) makes up for what a small model gets wrong. If it ever falls short, I can swap the model with one line in &lt;code&gt;.env&lt;/code&gt; without touching the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;Best Use of Gemma&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Adding server monitoring to my SSH manager without opening a second connection</title>
      <dc:creator>Rafi Bagaskara Halilintar</dc:creator>
      <pubDate>Mon, 29 Jun 2026 03:20:16 +0000</pubDate>
      <link>https://dev.to/masitings/adding-server-monitoring-to-my-ssh-manager-without-opening-a-second-connection-3gp2</link>
      <guid>https://dev.to/masitings/adding-server-monitoring-to-my-ssh-manager-without-opening-a-second-connection-3gp2</guid>
      <description>&lt;p&gt;I use my SSH manager every day. I also use a separate monitoring tool every day. For a long time I just accepted that these were two different things.&lt;/p&gt;

&lt;p&gt;Then one day I was SSH'd into a server that was behaving weird. I wanted to check if it was CPU or memory, but I had to open a different app, find the server in there, and wait for the dashboard to load. It took maybe 15 seconds. Not a huge deal. But it broke my flow every single time.&lt;/p&gt;

&lt;p&gt;I already had an SSH connection open to that server. Why was I opening a second thing just to see what was happening to it?&lt;br&gt;
That's what pushed me to build server monitoring directly into Termique, the SSH manager I've been working on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interesting part: reusing the existing SSH connection
&lt;/h2&gt;

&lt;p&gt;SSH connections aren't just for terminals. The protocol supports multiple channels over a single TCP connection. You can have a terminal session running in one channel while sending short exec commands through another channel on the same connection.&lt;/p&gt;

&lt;p&gt;That's how the monitoring feature works. When you open the metrics panel for a server, Termique creates a separate exec channel on the existing SSH connection and polls /proc/stat for CPU, /proc/meminfo for RAM, and /proc/loadavg for system load. Short-lived commands, called on an interval, over the connection you already have open.&lt;br&gt;
No second SSH handshake. No separate auth. Just another channel on the same pipe.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoff: you do need an agent
&lt;/h2&gt;

&lt;p&gt;I want to be upfront about this. The monitoring feature requires a small agent installed on each server. It's not agentless.&lt;/p&gt;

&lt;p&gt;I considered going agentless, relying entirely on /proc reads through exec channels. That works fine on most Linux servers. But the agent makes it easier to handle edge cases properly and opens the door for future features like alerts and longer history retention. Without it, I'd be fighting a lot of fragile shell parsing.&lt;/p&gt;

&lt;p&gt;If you're managing Linux servers, it's a one-command install. Non-Linux systems aren't supported yet. That's a real limitation and something I'm still working through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it fits
&lt;/h2&gt;

&lt;p&gt;The goal isn't to replace something like Netdata or Grafana. Those are great for what they do. The monitoring in Termique is specifically for the moment you're already in your terminal and just want to know if the server is struggling. CPU, RAM, disk. At a glance. Without switching apps.&lt;/p&gt;

&lt;p&gt;It's a consolidation thing, not a monitoring platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current status
&lt;/h2&gt;

&lt;p&gt;The feature is still in development. I'm targeting a release in the next Termique update. If you want to try it when it's out, or just poke around the existing SSH manager in the meantime, it's at termique.app. Free tier available, Pro is $5/month.&lt;/p&gt;

&lt;p&gt;If you've built something similar or have opinions on how you handle server monitoring in your day-to-day, I'd love to hear it in the comments.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ssh</category>
      <category>devops</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Submitting to App Store and Play Store as a solo developer - what actually happened</title>
      <dc:creator>Rafi Bagaskara Halilintar</dc:creator>
      <pubDate>Sat, 06 Jun 2026 11:14:04 +0000</pubDate>
      <link>https://dev.to/masitings/submitting-to-app-store-and-play-store-as-a-solo-developer-what-actually-happened-5g68</link>
      <guid>https://dev.to/masitings/submitting-to-app-store-and-play-store-as-a-solo-developer-what-actually-happened-5g68</guid>
      <description>&lt;p&gt;I submitted Termique to the App Store and Play Store three days ago. It's live now. Here's what the process actually looked like, because most guides make it sound cleaner than it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody talks about - the assets
&lt;/h2&gt;

&lt;p&gt;Before you even think about submitting, you need screenshots. Not just any screenshots, specific sizes for specific devices. App Store wants iPhone screenshots, iPad screenshots, and if you support Mac, Mac screenshots too.&lt;/p&gt;

&lt;p&gt;The iPad one caught me off guard. Termique is an iPhone app. I don't have an iPad. But Apple requires iPad screenshots anyway. So I ran the simulator, took the screenshots, and moved on. It's annoying but it takes maybe 30 minutes once you know it's coming.&lt;/p&gt;

&lt;p&gt;For the actual design I used Figma templates from the community, there are free ones that handle all the sizing automatically. That part wasn't bad. The bad part was not knowing I needed it until I was already mid-submission.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rejection
&lt;/h2&gt;

&lt;p&gt;I got rejected. It happens. The reasons were straightforward, a missing screenshot size and something about content rights information that I hadn't filled in. Both fixable in under an hour.&lt;/p&gt;

&lt;p&gt;The thing about App Store rejection is it's not as dramatic as it sounds. They tell you exactly what's wrong. You fix it. You resubmit. That's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three days from submission to live
&lt;/h2&gt;

&lt;p&gt;I submitted on day one. Got feedback the same day. Fixed everything, resubmitted on day two. Approved and live on day three.&lt;/p&gt;

&lt;p&gt;I was expecting weeks. I got three days. That was the biggest surprise of the whole process.&lt;/p&gt;

&lt;p&gt;Play Store was similar, faster review, slightly different requirements, but nothing that required starting over.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell myself before starting
&lt;/h2&gt;

&lt;p&gt;Make sure your legal pages are ready before you submit. Privacy policy, terms of use, all of it. Both stores will ask for links during submission and you can't just skip them. Having those ready saved me a round trip.&lt;/p&gt;

&lt;p&gt;The other thing, your app doesn't have to be perfect. It has to work. There's a difference. Some things weren't fully polished when I submitted Termique. But nothing was broken in a way users would notice. That's the bar. Don't wait for perfect, you'll never ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual hard part
&lt;/h2&gt;

&lt;p&gt;Honestly the submission process itself wasn't the hard part. The hard part was building something worth submitting in the first place.&lt;/p&gt;

&lt;p&gt;Termique is available on the App Store and Play Store now. If you manage servers and want an SSH manager that actually syncs across all your devices, give it a try.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://termique.app" rel="noopener noreferrer"&gt;termique.app&lt;/a&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>mobile</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Why I chose Tauri over Electron for my SSH manager</title>
      <dc:creator>Rafi Bagaskara Halilintar</dc:creator>
      <pubDate>Wed, 03 Jun 2026 11:33:54 +0000</pubDate>
      <link>https://dev.to/masitings/why-i-chose-tauri-over-electron-for-my-ssh-manager-a4a</link>
      <guid>https://dev.to/masitings/why-i-chose-tauri-over-electron-for-my-ssh-manager-a4a</guid>
      <description>&lt;p&gt;When I started building Termique, I knew one thing for sure: it had to feel fast.&lt;/p&gt;

&lt;p&gt;An SSH manager is a tool you open constantly. You switch between servers, you check logs, you run commands. The last thing you want is to wait for your app to load. So when I started evaluating my options for building a cross-platform desktop app, Electron was the obvious first choice, but it didn't take long for me to realize it wasn't the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Electron problem
&lt;/h2&gt;

&lt;p&gt;Electron is everywhere. VS Code, Slack, Discord, all built on Electron. And for good reason: it's mature, well-documented, and lets you build desktop apps with web technologies.&lt;/p&gt;

&lt;p&gt;But the tradeoff is real. Electron ships with its own copy of Chromium and Node.js. Every Electron app is essentially a browser running your app inside it. The result? My early build was hitting around 100MB just for the installer. For an SSH manager, that felt absurd.&lt;/p&gt;

&lt;p&gt;And it wasn't just the bundle size. The app felt heavy. Not broken, not unusable, just heavy. That sluggish feeling every time you open it, that half-second delay before things respond. For a tool you open dozens of times a day, that adds up fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discovering Tauri
&lt;/h2&gt;

&lt;p&gt;Tauri takes a different approach entirely. Instead of bundling its own browser engine, it uses the OS's native webview, WebKit on macOS, WebView2 on Windows, WebKitGTK on Linux. The result is dramatically smaller bundle sizes and native performance.&lt;/p&gt;

&lt;p&gt;When I switched Termique to Tauri, the Windows installer went from ~100MB down to 12MB. That's not a small improvement, that's a completely different category of app.&lt;/p&gt;

&lt;p&gt;But what surprised me more was the performance difference. Tauri just feels fast in a way that's hard to explain. Snappier startup, smoother interactions. I'm not sure exactly what's happening under the hood, but the difference is noticeable immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  It wasn't all smooth sailing
&lt;/h2&gt;

&lt;p&gt;I'll be honest, Tauri has its quirks. The one that frustrated me the most early on was something embarrassingly simple: the close button didn't work.&lt;/p&gt;

&lt;p&gt;When I first got the app running, clicking the X to close the window did absolutely nothing. I had to force quit every single time. It took me a while to figure out the fix, but that kind of thing is part of working with a newer framework, the documentation isn't always as comprehensive as Electron's, and some things you just have to figure out yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was it worth it?
&lt;/h2&gt;

&lt;p&gt;Yes, without question.&lt;/p&gt;

&lt;p&gt;For Termique specifically, the lightweight nature of Tauri isn't just a nice-to-have, it's part of the product's identity. An SSH manager should feel like a native tool, not a browser tab pretending to be one. 12MB installer, fast startup, no bloat. That's exactly what I wanted.&lt;/p&gt;

&lt;p&gt;If you're building a developer tool where performance and trust matter, I'd seriously consider Tauri. It's not as plug-and-play as Electron yet, but the tradeoffs are absolutely worth it.&lt;/p&gt;

&lt;p&gt;Termique is available at &lt;a href="https://termique.app" rel="noopener noreferrer"&gt;termique.app&lt;/a&gt;, free to try, and built entirely with Tauri.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>performance</category>
      <category>rust</category>
      <category>tooling</category>
    </item>
    <item>
      <title>I got locked out of my own servers. So I built my own SSH manager.</title>
      <dc:creator>Rafi Bagaskara Halilintar</dc:creator>
      <pubDate>Thu, 28 May 2026 18:12:58 +0000</pubDate>
      <link>https://dev.to/masitings/i-got-locked-out-of-my-own-servers-so-i-built-my-own-ssh-manager-47l9</link>
      <guid>https://dev.to/masitings/i-got-locked-out-of-my-own-servers-so-i-built-my-own-ssh-manager-47l9</guid>
      <description>&lt;p&gt;A while back, I switched devices.&lt;/p&gt;

&lt;p&gt;Simple enough, right? Except I was on Termius Free and apparently &lt;br&gt;
host sync is a Pro feature. So everything I had set up, hosts, SSH &lt;br&gt;
keys, credentials, none of it carried over. I had to start from scratch.&lt;/p&gt;

&lt;p&gt;That was the moment I thought: &lt;em&gt;why am I paying for something that &lt;br&gt;
holds my own data hostage behind a paywall?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I built Termique.&lt;/p&gt;

&lt;h2&gt;
  
  
  One month. One developer. Zero co-founders.
&lt;/h2&gt;

&lt;p&gt;I started in early May. Today, Termique is a fully working SSH manager &lt;br&gt;
for macOS, Windows, and Linux, built with Tauri, React, Hono, &lt;br&gt;
PostgreSQL, Firebase, and Cloudflare Workers.&lt;/p&gt;

&lt;p&gt;The hardest part wasn't the code. I built most of it with AI assistance. &lt;br&gt;
The real challenge was figuring out &lt;em&gt;what to build&lt;/em&gt;, which features &lt;br&gt;
actually matter, how sync should work securely, and what should never &lt;br&gt;
leave your device.&lt;/p&gt;

&lt;p&gt;The second hardest part? Security auditing. Making sure nothing leaks &lt;br&gt;
that shouldn't. Credentials stay encrypted on your device. The server &lt;br&gt;
never sees plaintext. No telemetry on your sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does today
&lt;/h2&gt;

&lt;p&gt;Add a host on your laptop. Open your desktop. It's already there, IP, &lt;br&gt;
port, SSH key and all. No re-setup. No copy-pasting credentials between &lt;br&gt;
machines.&lt;/p&gt;

&lt;p&gt;That's the thing I'm most proud of. It sounds simple, but that's &lt;br&gt;
exactly the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where we are
&lt;/h2&gt;

&lt;p&gt;12 users, still very early. Launching on Product Hunt on June 15th. &lt;br&gt;
Free plan available, Pro is $5/month, half of what frustrated me in &lt;br&gt;
the first place.&lt;/p&gt;

&lt;p&gt;If you've ever been annoyed by Termius pricing or just want an SSH &lt;br&gt;
manager that doesn't feel bloated, I'd love for you to try it and &lt;br&gt;
tell me what you think.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://termique.app" rel="noopener noreferrer"&gt;termique.app&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>buildinpublic</category>
    </item>
  </channel>
</rss>
