<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged ssh</title>
<link>https://coderlegion.com/tag/ssh</link>
<description>Powered by Question2Answer</description>
<item>
<title>Build a small reusable Linux checklist in FastRemote</title>
<link>https://coderlegion.com/27820/build-a-small-reusable-linux-checklist-in-fastremote</link>
<description>&lt;blockquote&gt;&lt;p&gt;Disclosure: This tutorial is provided by the FastRemote product team and introduces our product. It was generated by an AI agent at our request, using documentation for released 1.1.5 functionality. Examples are for authorized test environments. FastRemote is a commercial SSH/SFTP desktop app for Windows, macOS, and Linux. Its 15-day trial must be started explicitly and does not require a payment method.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A command library is most useful when it contains commands you understand. Start with four checks you repeat, rather than importing a large collection and hoping every command suits your server.&lt;/p&gt;
&lt;p&gt;This exercise uses a Linux test host. The commands inspect state and do not need elevated privileges.&lt;/p&gt;
&lt;h3 id=&quot;choose-four-tasks&quot;&gt;Choose four tasks&lt;/h3&gt;
&lt;p&gt;Create a group in FastRemote's command library for basic Linux checks and add separate entries:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Entry name &lt;/th&gt;&lt;th&gt;Command&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Current user &lt;/td&gt;&lt;td&gt;whoami&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Hostname &lt;/td&gt;&lt;td&gt;hostname&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Working directory &lt;/td&gt;&lt;td&gt;pwd&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Filesystem space &lt;/td&gt;&lt;td&gt;df -h&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Use the task as the entry name. Add a short note describing what the output means. Do not store passwords or API keys in command text or notes.&lt;/p&gt;
&lt;h3 id=&quot;test-each-command-yourself&quot;&gt;Test each command yourself&lt;/h3&gt;
&lt;p&gt;Connect to your test server. Check the selected SSH target and run each command once in the terminal.&lt;/p&gt;
&lt;p&gt;This establishes both what the command does and whether it is suitable for that operating system. A Linux command should not be assumed to behave the same way in Windows PowerShell.&lt;/p&gt;
&lt;p&gt;If you manage several systems, use separate groups and clear names.&lt;/p&gt;
&lt;h3 id=&quot;paste-before-executing&quot;&gt;Paste before executing&lt;/h3&gt;
&lt;p&gt;When taking a command from FastRemote's command library, beginners should start with the paste-only action. Read the text and check the target, then press Enter.&lt;/p&gt;
&lt;p&gt;This is particularly helpful once commands contain variables, paths with spaces, or multiple lines. A saved command is still code that will run with your remote account's permissions.&lt;/p&gt;
&lt;p&gt;The direct execution action can save an interaction once you are comfortable with an entry and its context.&lt;/p&gt;
&lt;h3 id=&quot;connect-output-to-a-decision&quot;&gt;Connect output to a decision&lt;/h3&gt;
&lt;p&gt;A disk-space check should tell you which filesystem deserves investigation. It should not automatically trigger a deletion command.&lt;/p&gt;
&lt;p&gt;An unexpected username should send you back to the connection settings rather than immediately to a privilege-escalation command.&lt;/p&gt;
&lt;h3 id=&quot;keep-the-library-maintainable&quot;&gt;Keep the library maintainable&lt;/h3&gt;
&lt;p&gt;For each later addition, record its purpose, supported system, and whether it changes data. Review exported command files for sensitive values before sharing them.&lt;/p&gt;
&lt;p&gt;You are finished when you can retrieve a familiar check without hunting through browser tabs, understand its output, and run it on the intended machine. FastRemote supplies the organization and reuse; your notes preserve the reasoning behind each entry.&lt;/p&gt;
&lt;p&gt;Product information and downloads: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.ynpe.com/en&quot;&gt;FastRemote website&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/27820/build-a-small-reusable-linux-checklist-in-fastremote</guid>
<pubDate>Sun, 20 Sep 2026 10:17:22 +0000</pubDate>
</item>
<item>
<title>My Servers Get Scanned Every 40 Seconds. Here's How I Stopped Caring — And Fixed It Anyway</title>
<link>https://coderlegion.com/26873/my-servers-get-scanned-every-40-seconds-heres-how-i-stopped-caring-and-fixed-it-anyway</link>
<description>&lt;p&gt;I'm exhausted. Genuinely, professionally exhausted. Every server I stand up, no matter how boring, no matter how &quot;it's just a staging box, nobody even knows the IP&quot; — gets scanned before the SSL cert finishes provisioning. I used to take it personally. I used to open &lt;code&gt;/var/log/auth.log&lt;/code&gt;, see four hundred failed logins from some IP block in a country I've never shipped a package to, and think: &lt;em&gt;why me specifically?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It's not you specifically. That's actually the first thing you need to internalize before any of the technical advice below will make sense to you emotionally instead of just intellectually. Internet-wide scanning is background radiation now. GreyNoise's Global Observation Grid clocked roughly &lt;strong&gt;212 exploitation attempts per second&lt;/strong&gt; across the back half of 2025, from nearly &lt;strong&gt;3 billion malicious sessions&lt;/strong&gt; and &lt;strong&gt;3.8 million unique source IPs&lt;/strong&gt; hitting internet-facing infrastructure. You are not being hunted. You are being &lt;em&gt;sampled&lt;/em&gt;, by the same dumb scripts that are sampling literally everyone else with a public IP, including your neighbor's smart fridge.&lt;/p&gt;
&lt;p&gt;Once that sinks in, the panic turns into engineering, which is a much more useful emotional state. So let's do the engineering.&lt;/p&gt;
&lt;h3 id=&quot;myth-1-changing-the-ssh-port-is-security-&quot;&gt;Myth #1: Changing the SSH Port Is &quot;Security&quot;&lt;/h3&gt;
&lt;p&gt;No. It isn't. I need you to hear this from someone who has changed the SSH port on every server they've ever owned: it is not security, it is noise reduction. A full port scan finds your &quot;hidden&quot; SSH port in about four seconds, and any attacker who is actually targeting &lt;em&gt;you specifically&lt;/em&gt; — as opposed to sweeping the entire IPv4 space with a botnet — will find it just as fast.&lt;/p&gt;
&lt;p&gt;What it &lt;em&gt;does&lt;/em&gt; do, and this is legitimately worth doing, is cut out the overwhelming majority of low-effort automated scanners that only ever probe port 22 because probing every port on every IP is expensive even for a botnet. Cleaner logs. Less fail2ban noise. Fewer 3 AM &quot;wait, is this real&quot; moments. Do it, but do it with your eyes open: you're sweeping the porch, not installing a deadbolt.&lt;/p&gt;
&lt;h3 id=&quot;the-actual-deadbolt-ssh-hardening-that-isn-t-theater&quot;&gt;The Actual Deadbolt: SSH Hardening That Isn't Theater&lt;/h3&gt;
&lt;p&gt;Here's the sshd config that actually matters, and I want you to notice something about it: none of it is exotic. It's the boring stuff nobody does because it involves editing a file instead of installing a shiny dashboard.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /etc/ssh/sshd_config.d/99-hardening.conf

PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
PermitEmptyPasswords no
MaxAuthTries 3
MaxSessions 4
LoginGraceTime 20

AllowAgentForwarding no
AllowTcpForwarding no
X11Forwarding no
PermitTunnel no

AllowUsers your_actual_username

ClientAliveInterval 300
ClientAliveCountMax 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Generate an Ed25519 key if you're still rocking RSA-4096 out of nostalgia — it's smaller, it's faster, and the public key is about 68 characters instead of 800-plus:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t ed25519 -C &quot;you@host&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Test your new config in a &lt;em&gt;second terminal&lt;/em&gt; before you reload sshd. I'm not going to explain why. If you've never locked yourself out of a box at 2 AM over a typo in &lt;code&gt;sshd_config&lt;/code&gt;, you will, and that day will teach you faster than I ever could. Just do the test-in-a-second-terminal thing now and skip the lesson.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo sshd -t &amp;amp;&amp;amp; sudo systemctl reload sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Password authentication being off is the single highest-leverage change on this entire list. GreyNoise's edge report noted a credential-spraying botnet targeting exposed RDP that grew from &lt;strong&gt;2,000 to 300,000 participating IPs in 72 days&lt;/strong&gt; — a 150x jump in about two and a half months, with the majority of it riding on compromised residential connections. That's the fate of anything on the internet still accepting &lt;code&gt;admin&lt;/code&gt; / &lt;code&gt;Password123!&lt;/code&gt; in 2026. Don't be the box that's still allowing it out of laziness.&lt;/p&gt;
&lt;h3 id=&quot;default-deny-firewall-because-allow-everything-block-what-i-notice-isn-t-a-strategy&quot;&gt;Default-Deny Firewall, Because &quot;Allow Everything, Block What I Notice&quot; Isn't a Strategy&lt;/h3&gt;
&lt;p&gt;Whatever you use — nftables, ufw, firewalld — the policy should be &lt;strong&gt;deny by default, allow the minimum set of ports you actually serve.&lt;/strong&gt; Not &quot;allow everything and block IPs after they annoy me.&quot; Deny-by-default means the scanners hitting the other 65,530 ports just get silently dropped, which also, pleasantly, makes your box slightly harder to fingerprint.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw limit 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;limit&lt;/code&gt; instead of &lt;code&gt;allow&lt;/code&gt; on SSH isn't a typo — ufw's rate-limiting will auto-block an IP that hits the port more than six times in thirty seconds. It's a free, built-in speed bump before fail2ban even wakes up.&lt;/p&gt;
&lt;h3 id=&quot;fail2ban-vs-crowdsec-pick-a-fighter&quot;&gt;Fail2ban vs CrowdSec: Pick a Fighter&lt;/h3&gt;
&lt;p&gt;This is where I watch people either overthink it or underthink it, so let's settle it cleanly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fail2ban&lt;/strong&gt; watches your logs, notices repeated failures, and bans the offending IP at the firewall. It's been quietly doing this job for over a decade, it has zero external dependencies, it phones home to nobody, and on a 512MB box it barely registers. If you're running one server with SSH and maybe a web app, fail2ban is completely sufficient and there's a real argument for not overengineering past it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /etc/fail2ban/jail.local
[sshd]
enabled = true
maxretry = 3
bantime = 1h
findtime = 10m
ignoreip = your.own.ip.address
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That last line matters more than any other line in this entire article. Add your own IP to &lt;code&gt;ignoreip&lt;/code&gt; before you enable anything. Locking yourself out of your own server over your own typo is a rite of passage nobody actually enjoys.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CrowdSec&lt;/strong&gt; does the same local detection but adds a crowdsourced reputation layer: your agent optionally shares anonymized attack signals and receives a community blocklist in return, so an IP that hammered someone else's Ghost blog in Estonia last week gets pre-emptively blocked on your box before it ever sends you a single packet. It splits detection from enforcement — an engine that decides, and separate &quot;bouncers&quot; that actually carry out the ban at the firewall, at your reverse proxy, or in front of multiple servers from one shared decision list.&lt;/p&gt;
&lt;p&gt;If you're running a single personal box, fail2ban is fine — go ahead, sleep well. If you're running anything public-facing at scale, multiple servers, or you're just tired of every server relearning the same lessons independently, CrowdSec's shared blocklist and centralized decision-making genuinely earns its extra moving parts.&lt;/p&gt;
&lt;h3 id=&quot;patch-the-old-stuff-no-really-the-old-stuff&quot;&gt;Patch the Old Stuff. No, Really, the &lt;em&gt;Old&lt;/em&gt; Stuff&lt;/h3&gt;
&lt;p&gt;Here's the part that should offend your sense of narrative logic: GreyNoise's 2026 State of the Edge report found that &lt;strong&gt;pre-2015 CVEs generated roughly four times more exploitation traffic than CVEs disclosed in 2023–2024.&lt;/strong&gt; Shellshock. Old PHP-CGI bugs. Ancient WebLogic flaws. Vulnerabilities old enough to vote are still out there generating millions of exploitation attempts, because there are still millions of boxes nobody ever patched, and scanning for a decade-old known-good exploit is cheaper and more reliable for an attacker than gambling on something novel.&lt;/p&gt;
&lt;p&gt;Your bot army isn't targeting you with next-generation zero-days. It's targeting you with the software equivalent of trying every car door in the parking lot, because somewhere, someone genuinely still left it unlocked. Run &lt;code&gt;unattended-upgrades&lt;/code&gt;, set a reboot window, and stop assuming &quot;it's old, nobody cares about it anymore&quot; is a security posture. It is, statistically, the opposite of one.&lt;/p&gt;
&lt;h3 id=&quot;the-ip-reputation-illusion-is-over&quot;&gt;The IP-Reputation Illusion Is Over&lt;/h3&gt;
&lt;p&gt;If your entire defense strategy is &quot;block IPs that look sketchy,&quot; I have bad news. GreyNoise's residential-proxy research, based on 4 billion malicious sessions over 90 days, found that &lt;strong&gt;nearly 4 in 10 attacking IPs are residential&lt;/strong&gt; — ordinary home broadband, mobile data, small-business routers, quietly compromised and rented out as attack infrastructure. One residential proxy network Google's Threat Intelligence Group took down in January 2026 was running &lt;strong&gt;9 to 11 million daily active proxies&lt;/strong&gt; used by over 550 distinct threat groups, including state-sponsored ones. Your grandmother's router might, statistically, be more dangerous than she is.&lt;/p&gt;
&lt;p&gt;The takeaway isn't &quot;give up on IP blocking.&quot; It's &quot;don't let IP blocking be your &lt;em&gt;only&lt;/em&gt; layer,&quot; because the entire premise that you can infer intent from an IP address is, as GreyNoise's own CEO put it, broken at scale. Behavior-based detection (what CrowdSec's scenario engine does, what a WAF does) matters more now than reputation lists alone.&lt;/p&gt;
&lt;h3 id=&quot;troll-them-back-tarpits-are-underrated-and-extremely-funny&quot;&gt;Troll Them Back: Tarpits Are Underrated and Extremely Funny&lt;/h3&gt;
&lt;p&gt;This is my favorite category, because most security advice is defensive and boring, and tarpits let you be defensive &lt;em&gt;and&lt;/em&gt; petty, which is a combination I fully endorse.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Endlessh&lt;/strong&gt; is an SSH tarpit: point it at port 22 (move your real sshd elsewhere), and it exploits a genuinely funny loophole in the SSH RFC — the server is allowed to send junk banner lines before the real version string, with no upper limit on how long it can stall. Endlessh sends an infinite trickle of random banner bytes, a few bytes every few seconds, and just… holds the connection open. Forever. Automated SSH brute-forcers, which are almost always single-threaded per target and not built to handle a server that never actually says hello, just sit there, burning a thread, going nowhere, for as long as you let them.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d -p 22:2222 -p 127.0.0.1:2222:2222 --name endlessh figiel/endlessh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Nepenthes and its cousin Iocaine&lt;/strong&gt; do the same thing for web crawlers instead of SSH bots: an infinite maze of auto-generated fake pages, all linking back into themselves, all stuffed with Markov-chain gibberish that's just structured enough to look like content. Point a misbehaving scraper at it and it doesn't get an error, it doesn't get a block page — it gets an infinite hallway with no exit, and it keeps walking because nothing in its logic tells it to stop. It's less &quot;you shall not pass&quot; and more &quot;you shall pass, forever, into nothing.&quot; Deploy it behind a &lt;code&gt;Disallow&lt;/code&gt; in &lt;code&gt;robots.txt&lt;/code&gt; and let anything that ignores that directive enjoy the consequences.&lt;/p&gt;
&lt;p&gt;Neither of these replaces real hardening. Think of them as the security equivalent of leaving out a bowl of pennies for door-to-door scammers — it wastes nobody's time who matters, and it wastes a genuinely satisfying amount of time for everyone else.&lt;/p&gt;
&lt;h3 id=&quot;least-exposure-turn-off-what-you-re-not-using&quot;&gt;Least Exposure: Turn Off What You're Not Using&lt;/h3&gt;
&lt;p&gt;Every listening service is an entry point you now have to defend, patch, and think about at 3 AM. Run &lt;code&gt;ss -tulpn&lt;/code&gt;, look at every single line, and ask yourself honestly whether you meant to expose it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ss -tulpn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Uninstall what you don't need. Remove build tools and compilers from production hosts — if an attacker does get a foothold, you'd rather they didn't have &lt;code&gt;gcc&lt;/code&gt; sitting there ready to compile their next stage for them. This is unglamorous work and it will not show up in a conference talk, but it closes more real doors than any single clever tool on this list.&lt;/p&gt;
&lt;h3 id=&quot;the-layered-reality-check&quot;&gt;The Layered Reality Check&lt;/h3&gt;
&lt;p&gt;None of the above is a silver bullet, and if anyone tries to sell you one tool as &quot;solves scanning forever,&quot; check their LinkedIn for how many &quot;growth hacker&quot; job titles they've had. Scanners aren't going away. GreyNoise isn't reporting billions of sessions because attackers are winning some grand campaign against you personally — they're reporting it because sweeping the entire internet costs them almost nothing and occasionally, somewhere, it works. Your job isn't to make yourself unscannable. It's to make yourself boring: patched, key-only, minimally exposed, quietly logged, and — if you're feeling generous — wasting a few bot-hours a day in a tarpit out of pure spite.&lt;/p&gt;
&lt;p&gt;Stack the boring layers. Skip the panic. Let the bots enjoy the maze.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/26873/my-servers-get-scanned-every-40-seconds-heres-how-i-stopped-caring-and-fixed-it-anyway</guid>
<pubDate>Fri, 11 Sep 2026 17:01:14 +0000</pubDate>
</item>
<item>
<title>Keeping a Web-Backed SSH Terminal Alive Across SwiftUI Navigation</title>
<link>https://coderlegion.com/24308/keeping-a-web-backed-ssh-terminal-alive-across-swiftui-navigation</link>
<description>&lt;p&gt;A desktop SSH client can keep the remote connection alive and still give the user the impression that the terminal was reset. The failure often sits one layer above SSH: the UI destroys and rebuilds the terminal surface when the user switches to another section.&lt;/p&gt;
&lt;p&gt;That distinction matters when a macOS app embeds xterm.js in &lt;code&gt;WKWebView&lt;/code&gt;. The SSH process, terminal model, WebKit process, DOM, scrollback, focus chain, and visible geometry all have different lifetimes. Treating them as one SwiftUI view makes normal navigation surprisingly destructive.&lt;/p&gt;
&lt;p&gt;This article describes the design rules we ended up using in a native macOS SSH client.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://nexusshell.app/assets/img/screens/terminal.png&quot; alt=&quot;A native macOS SSH terminal with multiple sessions and a visible server workspace&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;why-rebuilding-is-not-a-harmless-refresh&quot;&gt;Why rebuilding is not a harmless refresh&lt;/h3&gt;
&lt;p&gt;A straightforward SwiftUI implementation conditionally creates the selected section:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;if selectedSection == .terminal {
    TerminalSurface()
} else {
    FilesView()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the condition changes, SwiftUI is free to dismantle the terminal subtree. If that subtree owns a &lt;code&gt;WKWebView&lt;/code&gt;, the app can lose much more than a view object:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the WebContent process and xterm.js DOM may be destroyed;&lt;/li&gt;
&lt;li&gt;scrollback stored only in the browser process disappears;&lt;/li&gt;
&lt;li&gt;alternate-screen applications such as &lt;code&gt;vim&lt;/code&gt;, &lt;code&gt;nano&lt;/code&gt;, or &lt;code&gt;top&lt;/code&gt; cannot be reconstructed from plain text;&lt;/li&gt;
&lt;li&gt;restoring a bounded output snapshot may omit older history;&lt;/li&gt;
&lt;li&gt;WebKit initialization and snapshot replay add visible latency;&lt;/li&gt;
&lt;li&gt;focus and resize events can arrive in a different order after reconstruction.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keeping the SSH handle alive does not solve any of these UI-state losses. The remote PTY may still be healthy while the local terminal surface has forgotten how to represent it.&lt;/p&gt;
&lt;h3 id=&quot;separate-four-lifetimes&quot;&gt;Separate four lifetimes&lt;/h3&gt;
&lt;p&gt;The useful mental model is to separate at least four things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Logical session&lt;/strong&gt;: the tab, server identity, name, working directory, and user-facing state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transport session&lt;/strong&gt;: the SSH handle and remote PTY that carry input and output.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Terminal renderer&lt;/strong&gt;: the &lt;code&gt;WKWebView&lt;/code&gt;, xterm.js DOM, parser state, and scrollback.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visible attachment&lt;/strong&gt;: whether that renderer currently participates in the AppKit view hierarchy and responder chain.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Navigation should normally change only the fourth lifetime. Closing a tab can end all four. A network failure may replace the transport while retaining the logical tab and renderer. Conflating those transitions is where most of the surprising behavior begins.&lt;/p&gt;
&lt;h3 id=&quot;mount-once-then-deactivate&quot;&gt;Mount once, then deactivate&lt;/h3&gt;
&lt;p&gt;After the terminal section is opened for the first time, keep its SwiftUI subtree mounted. Other sections can still be created normally, while the terminal surface changes visibility and interaction state:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;if terminalHasLoaded || selectedSection == .terminal {
    TerminalSurface(isActive: selectedSection == .terminal)
        .opacity(selectedSection == .terminal ? 1 : 0)
        .allowsHitTesting(selectedSection == .terminal)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This preserves the representable and its coordinator instead of asking SwiftUI to rebuild them on every sidebar click. The &lt;code&gt;isActive&lt;/code&gt; value also gives terminal-specific shortcuts, toolbars, and event monitors an explicit lifecycle. Hidden terminal controls should not continue handling keyboard shortcuts while the user is working in Files or Settings.&lt;/p&gt;
&lt;p&gt;Opacity alone, however, is not sufficient for a WebKit-backed terminal.&lt;/p&gt;
&lt;h3 id=&quot;preserve-the-wkwebview-object-not-necessarily-its-attachment&quot;&gt;Preserve the WKWebView object, not necessarily its attachment&lt;/h3&gt;
&lt;p&gt;On macOS, an embedded &lt;code&gt;WKWebView&lt;/code&gt; participates in AppKit focus, drag registration, and window behavior. Keeping it attached but transparent can leave side effects in sections that should have nothing to do with the terminal.&lt;/p&gt;
&lt;p&gt;A more reliable arrangement is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;let &lt;code&gt;NSViewRepresentable&lt;/code&gt; return a lightweight container;&lt;/li&gt;
&lt;li&gt;let its coordinator hold a strong reference to the &lt;code&gt;WKWebView&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;attach the web view to the container only while the terminal section is active;&lt;/li&gt;
&lt;li&gt;remove it from the hierarchy while inactive, without releasing the object;&lt;/li&gt;
&lt;li&gt;attach the same object again when the user returns.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The WebKit object, WebContent process, xterm.js DOM, parser state, and scrollback stay alive even while the view is temporarily detached. The rest of the app no longer has an invisible WebKit view interfering with hit testing or drag sessions.&lt;/p&gt;
&lt;p&gt;This is different from serializing terminal text and creating a new renderer. It preserves the renderer itself.&lt;/p&gt;
&lt;h3 id=&quot;hand-focus-back-before-detaching&quot;&gt;Hand focus back before detaching&lt;/h3&gt;
&lt;p&gt;Focus is a correctness issue for terminals, not just polish. If the window's first responder remains inside the web view during a section switch, later keystrokes can be delivered somewhere the user cannot see. In the worst case, a stray character is interpreted by the remote shell.&lt;/p&gt;
&lt;p&gt;Before detaching an active terminal web view:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Check whether the window's first responder is the web view or one of its descendants.&lt;/li&gt;
&lt;li&gt;Explicitly clear or move the first responder.&lt;/li&gt;
&lt;li&gt;Remove the web view from its container.&lt;/li&gt;
&lt;li&gt;Disable terminal-only keyboard monitors and shortcut controls.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When reattaching, restore focus deliberately after the container is visible. Avoid assuming that SwiftUI's appearance callback and AppKit's responder-chain update happen in the same frame.&lt;/p&gt;
&lt;h3 id=&quot;freeze-hidden-geometry&quot;&gt;Freeze hidden geometry&lt;/h3&gt;
&lt;p&gt;A hidden but mounted view can still receive geometry changes. If its width follows every animation in the visible section, xterm.js may repeatedly run &lt;code&gt;fit()&lt;/code&gt;, and each result may resize the remote PTY.&lt;/p&gt;
&lt;p&gt;That creates a subtle class of bugs: opening a file-transfer panel or animating another page changes the dimensions of a terminal the user is not looking at. Full-screen terminal programs then redraw against &quot;ghost&quot; sizes.&lt;/p&gt;
&lt;p&gt;Record the terminal's last active width and freeze the inactive container at that width. On reattachment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;align the web view to the visible container once;&lt;/li&gt;
&lt;li&gt;let the renderer's resize observer compute the new rows and columns;&lt;/li&gt;
&lt;li&gt;send one meaningful PTY resize instead of a frame-by-frame storm.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The same rule applies to side panels: reserve their width only in the section that owns them, not at a shared ancestor that also sizes the hidden terminal.&lt;/p&gt;
&lt;h3 id=&quot;keep-output-flowing-without-flooding-the-ui&quot;&gt;Keep output flowing without flooding the UI&lt;/h3&gt;
&lt;p&gt;Detaching the renderer should not pause the SSH transport. Output can continue updating the logical session and renderer while the user visits another section.&lt;/p&gt;
&lt;p&gt;High-volume PTY output needs its own boundary. Read callbacks can arrive hundreds of times per second, while sending every chunk through the main thread and WebKit IPC creates unnecessary contention. A practical policy is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;deliver sparse output immediately so typing echo feels instant;&lt;/li&gt;
&lt;li&gt;merge continuous bursts into roughly one flush per display frame;&lt;/li&gt;
&lt;li&gt;update the bounded recovery buffer before notifying the renderer;&lt;/li&gt;
&lt;li&gt;keep session logs on a separate path from visual replay.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The recovery buffer remains useful if WebKit really crashes, but it is no longer the normal navigation mechanism.&lt;/p&gt;
&lt;h3 id=&quot;test-transitions-not-just-steady-states&quot;&gt;Test transitions, not just steady states&lt;/h3&gt;
&lt;p&gt;The most valuable tests are sequences:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;start &lt;code&gt;top&lt;/code&gt;, switch to Files, resize the window, and return;&lt;/li&gt;
&lt;li&gt;scroll far back, visit Settings, and confirm the same scrollback remains;&lt;/li&gt;
&lt;li&gt;focus the terminal, switch sections, type immediately, and verify nothing reaches SSH;&lt;/li&gt;
&lt;li&gt;open and close a side panel while the terminal is hidden;&lt;/li&gt;
&lt;li&gt;disconnect while hidden, then return and exercise the reconnect path;&lt;/li&gt;
&lt;li&gt;close a terminal tab while its web view is detached and confirm observers and script handlers are removed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These tests cross SwiftUI, AppKit, WebKit, and SSH boundaries. A unit test around one layer will not reveal the ordering failures between them.&lt;/p&gt;
&lt;h3 id=&quot;the-broader-rule&quot;&gt;The broader rule&lt;/h3&gt;
&lt;p&gt;View visibility and resource lifetime are not the same thing. A terminal is a long-lived interactive resource with a UI surface, not a disposable page. Once the logical session, transport, renderer, and visible attachment have separate ownership, navigation becomes predictable: switching sections changes what the user sees without quietly rebuilding the tool they were using.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Developer disclosure:&lt;/strong&gt; I work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://nexusshell.app/?utm_source=coderlegion&amp;amp;utm_medium=technical_article&amp;amp;utm_campaign=terminal_view_lifetime&quot;&gt;Nexus Shell&lt;/a&gt;, a native macOS SSH workspace. This article is based on implementation and debugging lessons from that work, not a benchmark against other clients.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Editorial note:&lt;/strong&gt; This draft was prepared with AI assistance and reviewed against the current SwiftUI, AppKit, WebKit, and SSH implementation before publication.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24308/keeping-a-web-backed-ssh-terminal-alive-across-swiftui-navigation</guid>
<pubDate>Sun, 09 Aug 2026 04:53:28 +0000</pubDate>
</item>
<item>
<title>Now That You've Purchased Your VPS, Let's Make It More Secure...</title>
<link>https://coderlegion.com/21729/now-that-youve-purchased-your-vps-lets-make-it-more-secure</link>
<description>&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Having purchased our VPS, we're well on our way to setup Coolify. I'm getting excited and I hope you're excited about that, but before we actually get to that installation there is another step we must take. We must secure our VPS. My Linux OS is Ubuntu.&lt;/p&gt;
&lt;p&gt;In this post we're going to look at 4 easy steps we can take to make our VPS a harder target to hit. There, will be other measures we can take in the future but these will get us prepped, to start setting up Coolify. The four basic steps we will look at are:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* Create Sudo User/ Disable Root Login
* Enable UFW Firewall
* Change the default SSH Port Number
* Install Fail2Ban
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let's get Started.&lt;/p&gt;
&lt;h3 id=&quot;create-a-sudo-user-and-disable-root-login&quot;&gt;Create a Sudo User and Disable Root Login&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;The Root Username is Public Knowledge :&lt;/strong&gt;  Every single Linux server in the world has an account named root. Because hackers already know this username, they only have to guess your password or exploit one single vulnerability to take total control of your server. Creating a custom username forces hackers to guess both a unique username and a password, which is significantly harder to break.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Guardrails Against Accidental Destruction : &lt;/strong&gt; The root user operates without any safety nets. If you run a destructive command by accident while logged in as root, the server will execute it instantly without asking questions.    &lt;/p&gt;
&lt;p&gt;Example: Running rm -rf / as root will delete your entire operating system instantly. A normal user requires you to type sudo before administrative tasks. This extra step acts as a warning prompt, forcing you to type your password and think twice before executing dangerous commands. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Protection Against Automated Bots :&lt;/strong&gt; The moment your VPS goes online, malicious automated bots will start scanning its IP address. These bots constantly bombard the root user with thousands of automated password guesses per minute (known as a brute-force attack). By disabling root login entirely, you immediately render all of those automated attacks completely useless.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Better Accountability and Logs :&lt;/strong&gt; If multiple people ever need access to your server, sharing the root account makes it impossible to know who did what. When everyone uses their own unique standard account and uses sudo for admin tasks, Ubuntu keeps a clean history log /var/log/auth.log). This log shows exactly which specific user executed which command, giving you a clear audit trail.&lt;/p&gt;
&lt;h3 id=&quot;commands&quot;&gt;Commands&lt;/h3&gt;
&lt;h4 id=&quot;creating-a-sudo-superuser-do-user&quot;&gt;Creating a Sudo (Superuser Do) User&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;Create the new user&lt;/em&gt;&lt;br&gt;
 your-command-prompt:~$ &lt;code&gt;adduser your_user_name_goes_here&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Add the user to the sudo group&lt;/em&gt;&lt;br&gt;
 your-command-prompt:~$ &lt;code&gt;usermod -aG sudo your_user_name_goes_here&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note :&lt;/strong&gt; Log out of the root user and log in with the sudo user then disable the root account&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;Lock the Root Password (optional)&lt;/em&gt;&lt;br&gt;
your-command-prompt:~$ &lt;code&gt;sudo passwd -l root&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Verify Root Account is locked&lt;/em&gt;&lt;br&gt;
your-command-prompt:~$ &lt;code&gt;sudo passwd -S root&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note :&lt;/strong&gt; Your output will look like this - &lt;em&gt;root L 05/12/2026 0 99999 7 -1&lt;/em&gt; or this - &lt;em&gt;root LK 2026-05-12 0 99999 7 -1&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;Disable Root Login via sshd_config file&lt;/em&gt;&lt;br&gt;
your-command-prompt:~$ sudo nano /etc/ssh/sshd_config&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;In your &lt;em&gt;sshd_config&lt;/em&gt; file look for the line that says &lt;strong&gt;PermitRootLogin yes&lt;/strong&gt;. Its most likely at the bottom of the file. Change yes to no, then press &lt;strong&gt;ctrl O&lt;/strong&gt; to apply the change, then press &lt;strong&gt;enter&lt;/strong&gt; to confirm, &lt;strong&gt;ctrl X&lt;/strong&gt; to exit. &lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;Restart the sshd Daemon&lt;/em&gt;&lt;br&gt;
your-command-prompt:~$ &lt;code&gt;sudo systemctl restart sshd&lt;/code&gt;  &lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; a and G are what you call flags in a linux operating system. Flags are used to modify the behaviour of a command.&lt;br&gt;
-G: The uppercase -G stands for Groups. This flag tells the system that you want to add the user to the group name that follows it (in this case, the sudo group).&lt;br&gt;
-a: If you use -G by itself (without the -a), it will overwrite your user's existing groups. The system will remove the user from every other group they belong to, leaving them only in the new group you specified. Instead of wiping out your user's existing groups and replacing them, it tells the system to simply add (append) the new group to the user's current list.&lt;br&gt;
-aG: By combining them into -aG, you are telling Ubuntu: &quot;Add this user to the sudo group, but keep all of their existing group memberships exactly as they are.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;Test your new User&lt;/em&gt;&lt;br&gt;
your-command-prompt:~$ &lt;code&gt;groups your_user_name_goes_here&lt;/code&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-ufw-uncomplicated-firewall-firewall&quot;&gt;The UFW (Uncomplicated Firewall) Firewall&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Blocks Brute-Force Attacks :&lt;/strong&gt; Automations constantly guess passwords on exposed ports; a firewall shuts down unneeded entry points.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Restricts Open Ports :&lt;/strong&gt; Database engines (like MySQL) or internal tools stay hidden from the public internet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Limits Protocol Access :&lt;/strong&gt; You can restrict sensitive services like SSH to only accept connections from your specific home or office IP address.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mitigates Malicious Traffic :&lt;/strong&gt; It helps drop unexpected incoming traffic, reducing the impact of basic denial-of-service attempts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prevents Exploitation :&lt;/strong&gt; If an application you run has a hidden security flaw, a firewall can stop hackers from connecting to it. &lt;/p&gt;
&lt;h3 id=&quot;commands-1&quot;&gt;Commands&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;Allow SSH connections so you don't lock yourself out&lt;/em&gt;&lt;br&gt;
  your-command-prompt:~$ &lt;code&gt;sudo ufw allow OpenSSH or sudo ufw allow 22/tcp&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Allow web traffic if you plan to host a website&lt;/em&gt;&lt;br&gt;
your-command-prompt:~$ &lt;code&gt;sudo ufw allow http or sudo ufw allow 80/tcp&lt;/code&gt;&lt;br&gt;
your-command-prompt:~$ &lt;code&gt;sudo ufw allow https or sudo ufw allow 443/tcp&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Enable the Firewall&lt;/em&gt;&lt;br&gt;
your-command-prompt:~$ &lt;code&gt;sudo ufw enable&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Check the Firewall Status&lt;/em&gt;&lt;br&gt;
your-command-prompt:~$ &lt;code&gt;sudo ufw status verbose&lt;/code&gt;&lt;/p&gt;
&lt;h3 id=&quot;change-the-default-ssh-port-from-22-to-something-obscure&quot;&gt;Change the default SSH port from 22 to something obscure&lt;/h3&gt;
&lt;p&gt;The reality is that the moment a VPS gets a public IP, automated bots start hammering port 22 within minutes. They're not targeted attacks — just scripts scanning the entire internet looking for default SSH ports to brute-force. Moving to port 54322 (or any non-standard port) makes you invisible to those dumb scanners.&lt;/p&gt;
&lt;p&gt;The security community calls this, &lt;strong&gt;&lt;em&gt;security through obscurity&lt;/em&gt;&lt;/strong&gt; and generally considers it a weak standalone measure. But combined with the real hardening (SSH keys, no password auth, Fail2ban) it's a cheap quality-of-life improvement that keeps your logs clean.&lt;br&gt;
The honest framing: it's not on the list because it makes you safer — it's on the list because it stops your logs from being flooded with bot noise, which makes it easier to spot actual suspicious activity.&lt;/p&gt;
&lt;h3 id=&quot;commands-2&quot;&gt;Commands&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;Change the Port on the Server&lt;/em&gt;&lt;br&gt;
your-command-prompt:~$ &lt;code&gt;sudo nano /etc/ssh/sshd_config&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;In your &lt;em&gt;sshd_config&lt;/em&gt; file look for the line that says &lt;strong&gt;#Port 22&lt;/strong&gt;. Delete the hash and change the number to the port you want, then press &lt;strong&gt;ctrl O&lt;/strong&gt; to apply the change, then press &lt;strong&gt;enter&lt;/strong&gt; to confirm, &lt;strong&gt;ctrl X&lt;/strong&gt; to exit. &lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;Add your new SSH Port Number&lt;/em&gt;&lt;br&gt;
your-command-prompt:~$ &lt;code&gt;ufw allow 54322/tcp&lt;/code&gt;&lt;br&gt;
your-command-prompt:~$ &lt;code&gt;sudo ufw reload&lt;/code&gt;&lt;br&gt;
your-command-prompt:~$ &lt;code&gt;sudo systemctl restart sshd&lt;/code&gt;&lt;/p&gt;
&lt;h3 id=&quot;protect-your-ubuntu-server-from-automated-brute-force-password-guesses-fail2ban&quot;&gt;Protect your Ubuntu server from automated brute-force password guesses: Fail2Ban&lt;/h3&gt;
&lt;h4 id=&quot;what-is-fail2ban&quot;&gt;What is Fail2Ban&lt;/h4&gt;
&lt;p&gt;Fail2ban is a tool that scans log files for suspicious activity, like excessive failed login attempts, and blocks potentially harmful IP addresses. This free service protects Linux systems from brute force attacks by temporarily rejecting IPs exhibiting suspicious behavior, thereby reducing the risk of unauthorized access.&lt;/p&gt;
&lt;h4 id=&quot;why-consider-fail2ban&quot;&gt;Why Consider Fail2Ban&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Easy Setup &amp;amp; Free : &lt;/strong&gt; Fail2ban is easy to install and configure, requiring minimal expertise. As a free, open-source tool, it provides an accessible and cost-effective way to enhance server security without added software costs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Customizable Configuration :&lt;/strong&gt; he tool provides extensive customization options. You can tailor it to monitor specific services, set the ban duration, or even whitelist trusted IPs, allowing you to adapt it to your unique security needs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Brute Force Protection :&lt;/strong&gt; It’s particularly effective at preventing brute force attacks by automatically banning IP addresses that show malicious behavior, such as repeated login failures.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Efficient Resource Usage :&lt;/strong&gt; Fail2ban bans attackers at the firewall level, which helps reduce the load on your server by preventing malicious traffic from consuming resources.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Alerts and Notifications :&lt;/strong&gt; Integration with notification services means you can get real-time alerts when suspicious activity is detected, helping you stay informed and responsive.&lt;/p&gt;
&lt;h4 id=&quot;limitations-of-fail2ban&quot;&gt;Limitations of Fail2Ban&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Reactive Nature :&lt;/strong&gt; It only blocks an IP after the attacker has already made a set number of failed attempts. It does not prevent the initial probe. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;False Positives :&lt;/strong&gt; Legitimate users can be temporarily locked out if they mistype a password a few times in quick succession. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ineffective Against Distributed Attacks (DDoS) :&lt;/strong&gt; Because it operates on IP addresses, it is easily bypassed by botnets that rotate through thousands of different IPs, making it a poor tool for stopping distributed traffic floods.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;IP Spoofing :&lt;/strong&gt; Attackers can spoof IP addresses, potentially tricking Fail2Ban into blocking innocent third-party IPs. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Docker Conflicts :&lt;/strong&gt; When run on a Docker host, Fail2Ban's default iptables rules frequently conflict with Docker's internal networking, causing bans to fail entirely or impact the wrong containers&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;There is a work around for the Docker Conflicts but I just thought I should mention it so you would be aware.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;commands-3&quot;&gt;Commands&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;Update your Package List&lt;/em&gt;&lt;br&gt;
your-command-prompt:~$ &lt;code&gt;sudo apt update&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Install Fail2Ban&lt;/em&gt;&lt;br&gt;
your-command-prompt:~$ &lt;code&gt;sudo apt install fail2ban -y&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note :&lt;/strong&gt; Why the -y flag? It tells the package manager to automatically answer &quot;yes&quot; to all prompts and confirmations during the installation process, allowing the command to run completely unattended. &lt;br&gt;
Warning!!! Only use the -y flag when you are 100% sure about the package you are installing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;Start and Enable the Service to Run on Boot&lt;/em&gt;&lt;br&gt;
your-command-prompt:~$ &lt;code&gt;sudo systemctl start fail2ban&lt;/code&gt;&lt;br&gt;
your-command-prompt:~$ &lt;code&gt;sudo systemctl enable fail2ban&lt;/code&gt;&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;That's it for now. There is always more that can be done but we have started with the basics. What is most important is not just implementing security measures but that we must have an attitude that defaults to a secure development environment. &lt;/p&gt;
&lt;p&gt;Many security breaches starts in the mind, because we do things with out considering the consequences. This has got to stop. We can even translate this into how we live, where we go and what we do. &lt;/p&gt;
&lt;p&gt;Always look for ways to make you VPS more secure and still allow it to perform efficiently. &lt;/p&gt;
&lt;p&gt;Keep up the good Work!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21729/now-that-youve-purchased-your-vps-lets-make-it-more-secure</guid>
<pubDate>Thu, 02 Jul 2026 03:18:27 +0000</pubDate>
</item>
<item>
<title>How to Expose Localhost to the Public Using Nginx and SSH Tunnels</title>
<link>https://coderlegion.com/16117/how-to-expose-localhost-to-the-public-using-nginx-and-ssh-tunnels</link>
<description>
&lt;p&gt;Hello, I'm Ganesh. I'm building &lt;em&gt;git-lrc&lt;/em&gt;, an AI code reviewer that runs on every commit. It is free, unlimited, and source-available on Github. &lt;a href=&quot;https://github.com/HexmosTech/git-lrc&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;Star Us&lt;/a&gt; to help devs discover the project. Do give it a try and share your feedback for improving the product.&lt;/p&gt;

&lt;p&gt;As a developer, you&amp;rsquo;ve likely faced this hurdle: You are building a complex application on your local machine, and you need to show it to a client or test a webhook of your application. &lt;/p&gt;

&lt;p&gt;You could deploy it to a staging server, but that's slow. &lt;br&gt;
You could use Ngrok, but the free tier has limits and dynamic URLs. &lt;/p&gt;

&lt;p&gt;Today, I&amp;rsquo;ll show you how to build your own &lt;strong&gt;&quot;Unlimited&quot; Reverse Proxy&lt;/strong&gt; using &lt;strong&gt;Nginx&lt;/strong&gt;, &lt;strong&gt;Autossh&lt;/strong&gt;, and a small VPS.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;The goal is to route traffic from a professional URL (like &lt;code&gt;test1.yourdomain.com&lt;/code&gt;) directly to a port on your local machine (like localhost:3000).&lt;/p&gt;

&lt;p&gt;We use three layers of technology to make this happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Nginx:&lt;/strong&gt; Acts as the entry point and handles SSL (HTTPS).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;SSH Reverse Tunnel:&lt;/strong&gt; Creates a secure &quot;pipe&quot; from your VPS back to your laptop.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Autossh:&lt;/strong&gt; Ensures that if your internet flickers, the &quot;pipe&quot; automatically reconnects.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Step 1: The Remote Server (Nginx)
&lt;/h2&gt;

&lt;p&gt;On your server, you need an Nginx configuration that listens for HTTPS traffic and passes it to a local port.&lt;br&gt;
&lt;/p&gt;

&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight nginx&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;server&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kn&quot;&gt;listen&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;443&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ssl&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;http2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;kn&quot;&gt;server_name&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;test1.yourdomain.com&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;kn&quot;&gt;ssl_certificate&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;/etc/letsencrypt/live/your-domain/fullchain.pem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;kn&quot;&gt;ssl_certificate_key&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;/etc/letsencrypt/live/your-domain/privkey.pem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;kn&quot;&gt;location&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# Proxy traffic to the SSH-forwarded port&lt;/span&gt;
        &lt;span class=&quot;kn&quot;&gt;proxy_pass&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&lt;a href=&quot;http://127.0.0.1:6540&quot; rel=&quot;nofollow&quot;&gt;http://127.0.0.1:6540&lt;/a&gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# Keep the original user's info&lt;/span&gt;
        &lt;span class=&quot;kn&quot;&gt;proxy_set_header&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Host&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$host&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;kn&quot;&gt;proxy_set_header&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;X-Real-IP&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$remote_addr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;kn&quot;&gt;proxy_set_header&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;X-Forwarded-For&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;kn&quot;&gt;proxy_set_header&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;https&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: The Local Bridge
&lt;/h2&gt;

&lt;p&gt;On your local machine, you need to open the tunnel.&lt;/p&gt;

&lt;p&gt;Using a &lt;code&gt;Makefile&lt;/code&gt; or a shell script makes this repeatable. The core command uses the &lt;code&gt;-R&lt;/code&gt; flag, which stands for &lt;strong&gt;Remote Forward&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight shell&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# -R [Remote Port]:[Local Destination]:[Local Port]&lt;/span&gt;
autossh &lt;span class=&quot;nt&quot;&gt;-M&lt;/span&gt; 20000 &lt;span class=&quot;nt&quot;&gt;-R&lt;/span&gt; 6540:localhost:3000 root@your-server &lt;span class=&quot;nt&quot;&gt;-N&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this is &quot;Unlimited&quot;
&lt;/h2&gt;

&lt;p&gt;Unlike third-party services, this setup gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total Control:&lt;/strong&gt; No timeout limits or bandwidth caps (other than your own ISP's speed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Professionalism:&lt;/strong&gt; You use your own domain and your own SSL certificates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; Your local machine is not truly &quot;exposed.&quot; Only the specific port you choose (8081) is bridged through the encrypted SSH tunnel.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting Common Gotchas
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;502 Bad Gateway:&lt;/strong&gt; This usually means Nginx is working, but your SSH tunnel isn't running or is on the wrong port.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Port Already in Use:&lt;/strong&gt; If you restart your tunnel quickly, the server might still think the old one is active. 
Use a cleanup script to &lt;code&gt;kill -9 &amp;lt;PID&amp;gt;&lt;/code&gt; any process currently sitting on your remote port before starting a new one.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Firewalls:&lt;/strong&gt; Ensure your VPS allows traffic on ports 80 and 443!&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By combining the stability of Nginx with the flexibility of SSH tunneling, you've created a production-grade development environment. &lt;/p&gt;

&lt;p&gt;You now have a permanent bridge between your local setup and the rest of the world.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/HexmosTech/git-lrc&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;&lt;img src=&quot;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%2Fabn6nl3iehrohehnxgje.png&quot; alt=&quot;git-lrc&quot; width=&quot;800&quot; height=&quot;373&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Any feedback or contributors are welcome! It&amp;rsquo;s online, source-available, and ready for anyone to use. &lt;br&gt;
&amp;#11088; Star it on GitHub: &lt;a href=&quot;https://github.com/HexmosTech/git-lrc&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;https://github.com/HexmosTech/git-lrc&lt;/a&gt;&lt;/p&gt;


</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/16117/how-to-expose-localhost-to-the-public-using-nginx-and-ssh-tunnels</guid>
<pubDate>Sun, 03 May 2026 09:35:44 +0000</pubDate>
</item>
<item>
<title>How does SSH work and how can it be set up?</title>
<link>https://coderlegion.com/16119/how-does-ssh-work-and-how-can-it-be-set-up</link>
<description>
&lt;p&gt;Hello, I'm Ganesh. I'm building &lt;em&gt;git-lrc&lt;/em&gt;, an AI code reviewer that runs on every commit. It is free, unlimited, and source-available on Github. &lt;a href=&quot;https://github.com/HexmosTech/git-lrc&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;Star Us&lt;/a&gt; to help devs discover the project. Do give it a try and share your feedback for improving the product.&lt;/p&gt;

&lt;p&gt;Secure Shell (SSH) is a cryptographic network protocol that allows you to operate network services securely over an unsecured network.&lt;/p&gt;

&lt;p&gt;It is most commonly used for logging into a remote terminal and executing commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  How SSH Works
&lt;/h2&gt;

&lt;p&gt;SSH uses a &lt;strong&gt;client-server model&lt;/strong&gt; and relies on a suite of encryption technologies to ensure that data remains confidential and untampered.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Handshake
&lt;/h3&gt;

&lt;p&gt;When we connect to a server, the two machines exchange supported encryption protocols and settle on a mutually compatible version. &lt;/p&gt;

&lt;p&gt;They also establish a session key using a process like the Diffie-Hellman algorithm, which allows them to create a shared secret without actually sending the key across the network.&lt;/p&gt;

&lt;p&gt;Here is the video explaining this process:&lt;br&gt;
&lt;a href=&quot;https://youtu.be/aoag03mSuXQ?t=729&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;Link&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Authentication
&lt;/h3&gt;

&lt;p&gt;Once the encrypted channel is established, the server needs to verify who you are. This usually happens in one of two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Password Authentication:&lt;/strong&gt; You send your password through the encrypted tunnel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key-based Authentication:&lt;/strong&gt; The most secure method, using a &lt;strong&gt;Public Key&lt;/strong&gt; (stored on the server) and a &lt;strong&gt;Private Key&lt;/strong&gt; (stored on your machine).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  3. Encryption Layers
&lt;/h3&gt;

&lt;p&gt;SSH utilizes three different types of encryption:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symmetrical Encryption:&lt;/strong&gt; Uses one key for both encryption and decryption of the entire session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asymmetrical Encryption:&lt;/strong&gt; Uses a public/private key pair (used primarily during the handshake and for authentication).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hashing:&lt;/strong&gt; Uses algorithms (like HMAC) to ensure data integrity, proving that the packets haven't been altered during transit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For let's setup simple RSA key pair for ubuntu server.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to Setup SSH
&lt;/h2&gt;

&lt;p&gt;Since we are using ubuntu server, we need to install the SSH server on the server.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: Install the SSH Server
&lt;/h3&gt;

&lt;p&gt;Assuming you are in cloud console want to setup ssh on ubuntu.&lt;/p&gt;

&lt;p&gt;On the server, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight shell&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt update
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;openssh-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To check if it&amp;rsquo;s running:&lt;br&gt;
&lt;/p&gt;

&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight shell&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;systemctl status ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Generate SSH Keys (Recommended)
&lt;/h3&gt;

&lt;p&gt;On your &lt;strong&gt;local machine&lt;/strong&gt;, generate a key pair. This is much safer than using a password.&lt;br&gt;
&lt;/p&gt;

&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight shell&quot;&gt;&lt;code&gt;ssh-keygen &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; PEM &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; rsa &lt;span class=&quot;nt&quot;&gt;-b&lt;/span&gt; 4096 &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ~/.ssh/private_key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press Enter to save it in the default location. &lt;/p&gt;

&lt;p&gt;You can add a passphrase for extra security.&lt;br&gt;
&lt;/p&gt;

&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight shell&quot;&gt;&lt;code&gt;gk@jarvis:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ssh-keygen &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; PEM &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; rsa &lt;span class=&quot;nt&quot;&gt;-b&lt;/span&gt; 4096 &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ~/.ssh/private_key
Generating public/private rsa key pair.
Enter passphrase &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;empty &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;no passphrase&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;: 
Enter same passphrase again: 
Your identification has been saved &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; /home/gk/.ssh/private_key
Your public key has been saved &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; /home/gk/.ssh/private_key.pub
The key fingerprint is:
SHA256:i3qqpQpyRkjDAdnbe++rN+ZCzak/4Ryzsn5vGJYEsVg gk@jarvis
The key&lt;span class=&quot;s1&quot;&gt;'s randomart image is:
+---[RSA 4096]----+
|o+    E.         |
|o o  o..         |
| + o. ..         |
|..o .   .        |
|. .  . +So       |
| .  . o.@.       |
|o o .o.*.B       |
|oo o .= % .      |
|..o.o++%=*.      |
+----[SHA256]-----+
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the above output, we have generated a private key and public key.&lt;/p&gt;

&lt;p&gt;we can copy public key and save it in server&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Copy the Key to the Server
&lt;/h3&gt;

&lt;p&gt;Copy public key from local machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight shell&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; ~/.ssh/private_key.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you are inside server either in temperary console or using password.&lt;/p&gt;

&lt;p&gt;Change directory to .ssh&lt;/p&gt;

&lt;p&gt;Add public key to authorized_keys file.&lt;/p&gt;

&lt;p&gt;This way server will trust your public key and you can login without a password.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Connect
&lt;/h3&gt;

&lt;p&gt;Now you can log in without a password:&lt;br&gt;
&lt;/p&gt;

&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight shell&quot;&gt;&lt;code&gt;ssh username@remote_host_ip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We understood how ssh works and how to setup ssh on server.&lt;/p&gt;

&lt;p&gt;I hope this article helps you to understand ssh and how to setup ssh on server.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/HexmosTech/git-lrc&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;&lt;img src=&quot;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%2Fabn6nl3iehrohehnxgje.png&quot; alt=&quot;git-lrc&quot; width=&quot;800&quot; height=&quot;373&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Any feedback or contributors are welcome! It&amp;rsquo;s online, source-available, and ready for anyone to use. &lt;br&gt;
&amp;#11088; Star it on GitHub: &lt;a href=&quot;https://github.com/HexmosTech/git-lrc&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;https://github.com/HexmosTech/git-lrc&lt;/a&gt;&lt;/p&gt;


</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/16119/how-does-ssh-work-and-how-can-it-be-set-up</guid>
<pubDate>Wed, 29 Apr 2026 05:42:27 +0000</pubDate>
</item>
<item>
<title>Simple backups using Rsync with SSH</title>
<link>https://coderlegion.com/11514/simple-backups-using-rsync-with-ssh</link>
<description>&lt;p&gt;This post explains how to SSH with Rsync to make backup of your system efficiently and securely.&lt;/p&gt;
&lt;p&gt;The only requisite to start here is an access to SSH somewhere.&lt;/p&gt;
&lt;h3 id=&quot;create-your-ssh-keys&quot;&gt;Create your ssh keys&lt;/h3&gt;
&lt;p&gt;Here, i just create a keypair called “rsync”, ssh will create a private and a public key.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t ed25519 -o -a 100 -f ~/.ssh/rsync
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After, just send the public key to the server with scp targeting your server.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; scp ~/.ssh/rsync.pub backup@SERVER
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;create-a-backup-script&quot;&gt;Create a backup script&lt;/h3&gt;
&lt;p&gt;Create a shell script named rsync-backup.sh with the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#/usr/bin/env sh

set -o errexit

# All variables
SERVER=&quot;192.168.1.11&quot;
BACKUP_FILE=&quot;/tmp/backup.filter&quot;

# More efficient than playing with --include,--exclude
cat &amp;lt;&amp;lt; EOF &amp;gt; &quot;$BACKUP_FILE&quot;
# Include patterns
+ /documents/***
+ /Downloads/***
+ /downloads/***
+ /images/***
+ /musics/***

# Exclude everything else
- /**
EOF

# And finally the rsync command
rsync -avP --delete -e &quot;ssh -i $HOME/.ssh/rsync&quot; \
    --bwlimit=400 \
    --filter=&quot;merge $BACKUP_FILE&quot; \
    &quot;$HOME/&quot; &quot;backup@${SERVER}:backups/&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The script here will backup your $HOME with only certain directory in the “include pattern list”. Just ensure the server address is correct.&lt;/p&gt;
&lt;p&gt;I also limit the bandwidth because it break my wifi card (bad hardware…)&lt;/p&gt;
&lt;h3 id=&quot;execute-the-script&quot;&gt;Execute the script&lt;/h3&gt;
&lt;p&gt;Just give permission and launch the script.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; chmod u+x rsync-backup.sh
./rsync-backup.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Using SSH with Rsync is an efficient and secure method for backing up your system. By generating SSH keys and creating a simple shell script, you can automate the backup process, ensuring that your important files are safely stored on a remote server.&lt;/p&gt;
&lt;p&gt;Adjusting settings like bandwidth limitation can help optimize performance, especially on networks with limited resources. This approach not only provides a reliable backup solution but also enhances data security through encrypted connections.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/11514/simple-backups-using-rsync-with-ssh</guid>
<pubDate>Thu, 12 Feb 2026 13:35:25 +0000</pubDate>
</item>
<item>
<title>The Quest Continues: Porting the Word Game With AsyncSSH</title>
<link>https://coderlegion.com/2855/the-quest-continues-porting-the-word-game-with-asyncssh</link>
<description>&lt;p&gt;Last week, we reimplemented our &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/leveling-up-llm-game-development-with-dspy-356c&quot;&gt;20-questions variant&lt;/a&gt;, and it received quite a bit of attention. So far we built a web experience, as well as a command line interface for the game. However, unlike the web version, the command line interface is strictly local. There’s no way to share the game with a fellow friend unless we walk them through the setup. Coincidentally, a former coworker recently told me about hosting applications over secure shell protocol in Golang. That makes me wonder, how easily can we replicate it in Python?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*7iiy33yB4geyLDaTTY3zRw.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;A cute illustration from Copilot on the topic.&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-spark-of-an-idea&quot;&gt;The Spark of an idea&lt;/h4&gt;
&lt;p&gt;The secure shell protocol (SSH) is usually what we use to log into another machine for work. Yet, there are a lot of interesting applications hosted over SSH. I remember there was one that would play back a &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/gabe565/ascii-movie&quot;&gt;Star Wars movie in ASCII art&lt;/a&gt; upon connecting successfully. Late last year, my coworker told me about &lt;a rel=&quot;nofollow&quot; href=&quot;https://terminal.shop/&quot;&gt;a coffee shop selling freshly brewed coffee&lt;/a&gt; over SSH. We both found that idea intriguing.&lt;/p&gt;
&lt;p&gt;He suspected the application was &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/charmbracelet/wish&quot;&gt;made with wish&lt;/a&gt;, a Golang library. That prompted me to see if there is an equivalent for Python, but I only found &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.paramiko.org/&quot;&gt;Paramiko&lt;/a&gt; at the time. After a quick glance at the documentation, I decided it was too much work and moved on. It does seem like it is capable of achieving the goal — serving a CLI application over SSH, though.&lt;/p&gt;
&lt;p&gt;Yes, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rollingstone.com/culture/culture-features/chatgpt-hypen-em-dash-ai-writing-1235314945/&quot;&gt;em-dash is intentional&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*v3oPBEIrGmyfjgZB&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Vitaly Gariev on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Keeping in touch as usual, and he would bring up the wish project from time-to-time. Then fast-forward to last week, after I was done with the reimplementation of our little word game, I stumbled upon another library called &lt;a rel=&quot;nofollow&quot; href=&quot;https://asyncssh.readthedocs.io/en/latest/&quot;&gt;AsyncSSH&lt;/a&gt;. Like Paramiko, it can be used to create an SSH server as well. The relatively more extensive examples also seemed helpful.&lt;/p&gt;
&lt;p&gt;After some exchanges with &lt;a rel=&quot;nofollow&quot; href=&quot;https://gemini.google.com/&quot;&gt;Gemini&lt;/a&gt;, I received a generated proof-of-concept code that looked workable. After completing and publishing the article on the game, I finally had time to properly explore the topic. Read on to join the ride together in building an interactive application served over SSH.&lt;/p&gt;
&lt;h4 id=&quot;finding-the-right-tool-paramiko-vs-asyncssh&quot;&gt;Finding the Right Tool: Paramiko vs AsyncSSH&lt;/h4&gt;
&lt;p&gt;Like we briefly mentioned previously, Paramiko and AsyncSSH are libraries we can use to build SSH Server applications (they are also excellent as SSH client libraries). Though the former garnered more stars on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/paramiko/paramiko&quot;&gt;GitHub&lt;/a&gt;, the amount of information on how to build an SSH Server application is surprisingly sparse. From what I read, it also required quite extensive setup.&lt;/p&gt;
&lt;p&gt;The discovery of AsyncSSH was completely unintentional. It was buried deep in the search result when I was looking for a cohesive tutorial on Paramiko. While the design of the documentation site looked as if it was built in Python 2.0 era, the extensive collection of examples is extremely helpful. I particularly liked the simplicity offered by AsyncSSH in comparison.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*3qSBi8rTUc4s1NQF&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Agence Olloweb on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Our goal for this week is a fairly simple application sporting a simple REPL-like experience. The key of this exercise is to learn, hence efficiency isn’t a deciding factor. Therefore, the simplicity and developer friendly documentation make the library a sensible choice for our project this week.&lt;/p&gt;
&lt;h4 id=&quot;two-paths-and-a-relearning&quot;&gt;Two Paths and a Relearning&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*yCF1_agka4PSzxN_&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Ross Sneddon on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The quick proof-of-concept code I came out with the help of Gemini, by referring to &lt;a rel=&quot;nofollow&quot; href=&quot;https://asyncssh.readthedocs.io/en/latest/#simple-server-with-input&quot;&gt;the documentation&lt;/a&gt;, consists of 3 components. Firstly, we have the actual SSH Server class, subclassing asyncssh.SSHServer.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncssh

class MySSHServer(asyncssh.SSHServer):
    def begin_auth(self, username):
        logger.info(&quot;logging in user:%s&quot;, username)
        return False

    def session_requested(self):
        return MyREPLSession()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This class is where we define all the authentication rules. For the purpose of this article, we are skipping all of them to maintain simplicity. Thus, we are returning False in begin_auth to indicate no authentication is needed. Do refer to &lt;a rel=&quot;nofollow&quot; href=&quot;https://asyncssh.readthedocs.io/en/latest/api.html#asyncssh.SSHServer&quot;&gt;the documentation&lt;/a&gt; for information on the topic before publishing applications in production.&lt;/p&gt;
&lt;p&gt;The MyREPLSession is a class inheriting asyncssh.SSHServerSession, which defines the actual application logic we intend to serve over the server. The skeleton is shown as below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyREPLSession(asyncssh.SSHServerSession):
    _chan = None
    _buffer = &quot;&quot;

    def connection_made(self, chan: asyncssh.SSHLineEditorChannel) -&amp;gt; None:
        self._chan = chan

    def pty_requested(
        self,
        term_type: str,
        term_size: tuple[int, int, int, int],
        term_modes: dict[int, int],
    ) -&amp;gt; bool:
        logger.info(&quot;pty received&quot;)
        return True

    def shell_requested(self) -&amp;gt; bool:
        logger.info(&quot;shell received&quot;)
        return True

    def data_received(self, data: str, datatype: int | None) -&amp;gt; None:
        # TODO The actual application
        pass

    def eof_received(self):
        assert isinstance(self._chan, asyncssh.SSHLineEditorChannel)

        self._chan.exit(0)
        return False

    def signal_received(self, signal):
        assert isinstance(self._chan, asyncssh.SSHLineEditorChannel)

        self._chan.exit(0)

    def break_received(self, signal):
        assert isinstance(self._chan, asyncssh.SSHLineEditorChannel)

        self._chan.exit(0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are a couple of key methods we need to implement. The first is the connection_made, in our setup we are expecting a asyncssh.SSHLineEditorChannel object. All the input and output through SSH would be handled by the channel object, hence it is crucial to keep a reference of it. Methods ending with the _received suffix are event handlers, and in our toy project, we simply close the channel when no more input is sent (upon EOF or on terminate/kill signals).&lt;/p&gt;
&lt;p&gt;We will discuss data_received event handler later.&lt;/p&gt;
&lt;p&gt;Lastly, we need to tie everything together through asyncssh.create_server. As implied by the library name, the function returns a coroutine object, and hence we would need to properly set up through AsyncIO. Feel free to revisit our previous article on &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/asyncio-task-management-a-hands-on-scheduler-project-2e54&quot;&gt;how to write an asynchronous program&lt;/a&gt;. But for this example, we are adapting the example shown in the documentation.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/asyncio-task-management-a-hands-on-scheduler-project-2e54&quot;&gt;AsyncIO Task Management: A Hands-On Scheduler Project&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import asyncio

async def main() -&amp;gt; None:
    try:
        # Ensure ssh-host key exists before running
        await asyncssh.create_server(
            MySSHServer,
            &quot;0.0.0.0&quot;,
            8022,
            server_host_keys=[&quot;./ssh-host&quot;],
            line_editor=True
        )
        print(&quot;SSH server listening on port 8022...&quot;)
    except Exception as exc:
        print(f&quot;Error starting server: {exc}&quot;)

if __name__ == &quot; __main__&quot;:
    loop = asyncio.new_event_loop()

    loop.run_until_complete(main())
    loop.run_forever()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We previously used asyncio.run to run the main coroutine, and set up a ShutdownHandler working with an exit event object to end a program. On the other hand, this approach was likely written before asyncio.run was introduced, with the manual management of event loop being the telling sign. The run_forever() here also eliminates the hack where people put an infinite loop after asyncssh.create_server.&lt;/p&gt;
&lt;p&gt;Essentially, the code snippet creates the server, where the server behaviour is defined by the MySSHServer, and we set line_editor=True to be explicit (as we are expecting user interaction in our session). Remember to create an SSH key for the application.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ssh-keygen -q -N &quot;&quot; -t ed25519 -f ./ssh-host
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As mentioned earlier, we are building a very simple REPL-like application. Just as a refresher, REPL stands for read-evaluate-print-loop, which aptly fits our case here. We read user input, get DSPy to process it with the LLM in background, print the generated response, rinse-and-repeat. The skeleton of the flow is shown as below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def main() -&amp;gt; None:
    game_setup_and_welcome_speech()

    while True:
        match input(&quot;&amp;gt; &quot;):
            case command if command == &quot;quit&quot;:
                break
            case attempt:
                process_and_print_reply(attempt)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can populate our data_received handler. This method is called whenever our user is sending input through the SSH session. Unfortunately, we cannot assume the data is always ending with a newline (user pressing enter). Sometimes, the handler may have accumulated a couple of questions and send them in one go. The function may also get called while the user hasn’t done typing. For this, we would have to handle both cases, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyREPLSession(asyncssh.SSHServerSession):
    _buffer = &quot;&quot;

    def data_received(self, data: str, datatype: int | None) -&amp;gt; None:
        assert isinstance(self._chan, asyncssh.SSHLineEditorChannel)

        self._chan.write(f&quot;Welcome to my SSH server, {username}!\n\n&quot;)
        self._buffer += data

        lines = self._buffer.split(&quot;\n&quot;)
        for line in lines[:-1]:
            match line:
                case command if command == &quot;quit&quot;:
                    self._chan.exit(0)
                    break

                case attempt if attempt:
                    process_and_print_reply(attempt)

        self._buffer = lines[-1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Remember we talked about handling user input and output through the SSHLineEditingChannel object we received in connection_made? Output of our application is done through its write() method, just like how we output to stdout (to be displayed on the terminal emulator) through print() usually.&lt;/p&gt;
&lt;p&gt;Let’s test the application.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*nzSnTP5-lU5fShc3HDLN3A.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;A seemingly stalled game session&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;While the application worked, the experience wasn’t ideal. My modest GPU took quite a bit of time to generate a response for every user input, and somehow the cursor stalled and wouldn’t move to another line whenever the user pressed enter. It somehow gave an impression of the application just died after every enter.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*5p_SWLXidmWYQ065&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Josh Redd on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I was ready to move on, and start preparing to write, until I came across &lt;a rel=&quot;nofollow&quot; href=&quot;https://asyncssh.readthedocs.io/en/latest/#line-editing&quot;&gt;another example&lt;/a&gt; that fits the use-case. Now we have a code that looked a lot like the original skeleton.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def handle_client(process: asyncssh.SSHServerProcess) -&amp;gt; None:
    username = process.get_extra_info(&quot;username&quot;)
    process.stdout.write(f&quot;Welcome to my SSH server, {username}!\n\n&quot;)

    game_setup_and_welcome_speech()

    while True:
        match await process.stdin.readline():
            case command if command == &quot;quit&quot;:
                break

            case attempt:
                process_and_print_reply(attempt)

    process.exit(0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Previously we returned output through the SSHLineEditingChannel object, here we are doing it through the SSHServerProcess object. On the other hand, instead of input(), we read it through the readline() method in the stdin (an SSHReader object) property.&lt;/p&gt;
&lt;p&gt;Then, we need to remove the session_requested method from MySSHServer, and define the process_factory argument in asyncssh.create_server to point to our new coroutine.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;await asyncssh.create_server(
    MySSHServer,
    &quot;0.0.0.0&quot;,
    8022,
    server_host_keys=[&quot;./ssh-host&quot;],
    line_editor=True,
    process_factory=handle_client,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Re-run the application again, and to my surprise, it worked almost exactly like the original CLI version. For our project, the second approach definitely worked better. Nonetheless, the first attempt was also crucial, as it gives an idea on how input is being sent in a SSH session.&lt;/p&gt;
&lt;p&gt;The choice of naming the property holding a SSHReader object as stdin, and the fact it reads input through a method named readline piqued my curiosity. I knew we could reach sys.stdin to read content redirected from the previous command (via the pipe | operator), or file content via the &amp;lt; operator (in Unix-like environments). That said, I didn’t know reading user input interactively in a running application is done through stdin as well.&lt;/p&gt;
&lt;p&gt;My first experience writing a production CLI application started with PHP (very strange, yes). That was my first time learning about the relationship between the operators and stdin, stdout, and stderr through my supervisor. The scripts didn’t really need to collect input interactively, and given the similarity of PHP and C, I couldn’t find a scanf equivalent so I assumed PHP doesn’t have such feature.&lt;/p&gt;
&lt;p&gt;I was wrong.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*QGYcCYtRbatdkQ4B&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Mr. Bochelly on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;After some exchanges with Gemini, I realized I could use &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.php.net/manual/en/function.readline.php&quot;&gt;readline()&lt;/a&gt; in PHP for the task. Which itself is similar to scanf and input in Python. Alternatively, I could also use fgets(STDIN) to do the job, just that I need to figure out a way to print a prompt prior to the input collection. On the other hand, if I redirect some content into a Python application, if I call input(), it would read the content in stdin, until it encounters a newline character. Essentially, it roughly equates&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import sys

sys.stdin.readline().strip()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Back to the topic, the toy example skipped many other aspects besides the authentication work we mentioned earlier. Unfortunately, due to the sheer scope and complexity, it is almost impossible for the developer to cover all use cases in the examples. Fortunately, the API documentation is very well-written, and I assume it could be useful for people who know more about the protocol.&lt;/p&gt;
&lt;p&gt;We are lucky to live in an era where a LLM chatbot like Gemini is readily available, to provide help in navigating the documentation. Besides that, it also serves as a valuable companion to help gathering complementary information, like how I relearned stdin. Even so, critical thinking is still crucial while evaluating the response to avoid falling victim to AI hallucination.&lt;/p&gt;
&lt;h4 id=&quot;bridging-game-and-server&quot;&gt;Bridging Game and Server&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*B-C31lKKu6WXm70m&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Phil Hearing on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We finally assembled all the pieces together and ported the game to be served over SSH. The concrete implementation can be found at the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Jeffrey04/guessme-llm/&quot;&gt;GitHub repository&lt;/a&gt; for those who are interested. Both the CLI version and SSH version (the handle_client variant) are very similar, as shown below&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def handle_client(process: asyncssh.SSHServerProcess) -&amp;gt; None:
    username = process.get_extra_info(&quot;username&quot;)
    process.stdout.write(f&quot;Welcome to my SSH server, {username}!\n\n&quot;) # vs print()

    game_setup_and_welcome_speech()
    while True:
        match await process.stdin.readline(): # vs input()
            case command if command == &quot;quit&quot;:
                break
            case command if command == &quot;end&quot; and answer is not None:
                answer = None
            case attempt:
                process_and_print_reply(attempt)

    process.exit(0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The major difference between the two, is the use of process.stdout.write() to replace print(), and process.stdin.readline() to replace input (though a new coroutine called prompt_input is written to compose a process.stdout.write() to display prompt text).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def prompt_input(process: asyncssh.SSHServerProcess, prompt: str) -&amp;gt; str:
    process.stdout.write(prompt)
    return await process.stdin.readline()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The REPL-like experience makes the word game a very synchronous experience. A response is only generated upon user submission of input. Therefore the await is necessary while expecting user input is needed to ensure order. Other than that, the asynchronous nature of AsyncSSH does not affect the game’s logic much. As each game session is independent, each time a new user connects, the library will run handle_client separately for each of them.&lt;/p&gt;
&lt;p&gt;Once the log message shows “SSH server is listening …” after executing ssh.py, it is ready to be connected to with the following command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ssh -p 8022 localhost
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Considering we didn’t define any authentication methods in MySSHServer (which is definitely a bad practice), it should display a welcome message and the game is started right away.&lt;/p&gt;
&lt;p&gt;In this article, we explored two different strategies to host our little word game over SSH. The two approaches have their own pros and cons, but it does show how AsyncSSH makes the development experience quite intuitive. Even though I do not have much experience doing this in Golang with wish (the documentation situation is also quite bad there), I enjoy the time spent doing this with AsyncSSH.&lt;/p&gt;
&lt;h4 id=&quot;the-game-goes-live&quot;&gt;The Game Goes Live&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*yEHAu6wSvV2IeEN1&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Alessandro Erbetta on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The point of the exercise is mainly on learning and discovery. If our game were developed with a text-based UI through &lt;a rel=&quot;nofollow&quot; href=&quot;https://textual.textualize.io/&quot;&gt;Textual&lt;/a&gt;, or even &lt;a rel=&quot;nofollow&quot; href=&quot;https://typer.tiangolo.com/&quot;&gt;Typer&lt;/a&gt;, some extra work is needed to ensure I/O is redirected properly.&lt;/p&gt;
&lt;p&gt;The experience of porting the library to SSH is a series of happy coincidences. It started with a spark from a discussion with my former coworker, and it led to a series of discoveries. This article is a culmination of the whole journey, sharing what I learned throughout. It turns out building an application to be served over SSH just needed a change in how I/O is done. Gemini also helped immensely throughout, deepening my understanding.&lt;/p&gt;
&lt;p&gt;We went through the whole process by configuring a server by subclassing a asyncssh.SSHServer, exploring two different ways to serve applications through process_factory and inheriting asyncssh.SSHServerSession, then getting it running via asyncssh.create_server.&lt;/p&gt;
&lt;p&gt;Hopefully this article sparks an interest and we see more interesting applications hosted over SSH.&lt;/p&gt;
&lt;p&gt;This article was written with assistance from Gemini, a large language model by Google. Gemini provided editorial help, fixing language errors and checking for article flow. However, the code and voice are entirely my own. If you’d like to collaborate or discuss job opportunities, please connect with me on &lt;a rel=&quot;nofollow&quot; href=&quot;https://linkedin.com/in/jeffrey04/&quot;&gt;LinkedIn&lt;/a&gt; or via &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/&quot;&gt;Medium&lt;/a&gt;.&lt;/p&gt;
</description>
<category>More Libraries</category>
<guid isPermaLink="true">https://coderlegion.com/2855/the-quest-continues-porting-the-word-game-with-asyncssh</guid>
<pubDate>Sun, 04 May 2025 23:33:09 +0000</pubDate>
</item>
</channel>
</rss>