<?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: Alex Sashin</title>
    <description>The latest articles on DEV Community by Alex Sashin (@alex_sashin).</description>
    <link>https://dev.to/alex_sashin</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%2F4142747%2Fd144782a-5bef-4b23-8c19-c57043e0431d.png</url>
      <title>DEV Community: Alex Sashin</title>
      <link>https://dev.to/alex_sashin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alex_sashin"/>
    <language>en</language>
    <item>
      <title>Stop SSH-ing into 50 Servers: Why We Rewrote Our Python Infra Tool in Go (And Open-Sourced It)</title>
      <dc:creator>Alex Sashin</dc:creator>
      <pubDate>Wed, 30 Sep 2026 12:48:52 +0000</pubDate>
      <link>https://dev.to/alex_sashin/stop-ssh-ing-into-50-servers-why-we-rewrote-our-python-infra-tool-in-go-and-open-sourced-it-159k</link>
      <guid>https://dev.to/alex_sashin/stop-ssh-ing-into-50-servers-why-we-rewrote-our-python-infra-tool-in-go-and-open-sourced-it-159k</guid>
      <description>&lt;p&gt;Hey folks. I work in infrastructure and infosec, and today I want to share the story behind an internal project we recently open-sourced: nkt (NetKnownsThat).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; It’s an all-in-one control plane for Linux hosts. It parses nginx/haproxy configs, manages certs, controls firewalls, handles Docker/Podman/LXD/K8s, and gives you a web terminal and centralized vulnerability scanning.&lt;/p&gt;

&lt;p&gt;But the tech stack isn't the interesting part. The evolution is.&lt;/p&gt;

&lt;p&gt;Originally, nkt was a monstrous Python project. We eventually rewrote it entirely in Go. And for once, the "never rewrite from scratch" rule didn't apply. Rewriting was the only way to survive and build something we weren't embarrassed to run in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Python Era: Dependency Hell and 200MB Binaries&lt;/strong&gt;&lt;br&gt;
nkt started as a few audit scripts. Over time, it morphed into a massive Swiss Army knife. But deploying Python to dozens of heterogeneous VPS instances (from beefy bare-metal to low-end ARM routers) is a nightmare.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependency Hell:&lt;/strong&gt; Every server needed a venv. System Python varied between Ubuntu versions, pip occasionally nuked system packages, and the cryptography lib demanded Rust compilers on machines that didn't have them.&lt;br&gt;
&lt;strong&gt;Deployment:&lt;/strong&gt; Try packing a complex Python project into a single static binary for ARM. PyInstaller spat out 200MB binaries that crashed with obscure glibc errors. Dragging an interpreter and all dependencies to every host was madness.&lt;br&gt;
&lt;strong&gt;Performance:&lt;/strong&gt; Parsing hundreds of configs and analyzing iptables trees via psutil ate RAM and crawled.&lt;br&gt;
After debugging yet another expired cert incident on prod, I snapped: "Enough. We're rewriting this in Go." We kept the React/Ant Design frontend but moved the backend and TUI to Go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Go Rewrite: One Binary to Rule Them All&lt;/strong&gt;&lt;br&gt;
Moving to Go gave us the holy grail: go build and a single static binary (~30MB) that runs anywhere. We just shoved the frontend inside using go:embed. No Python, no Node, no separate files on the host.&lt;/p&gt;

&lt;p&gt;Here is what nkt actually does now, and why we built it this way.&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%2F9w0fdpsabky661tu6b6b.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%2F9w0fdpsabky661tu6b6b.png" alt="The host overview dashboard." width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Deep Audits and "Findings"&lt;/strong&gt;&lt;br&gt;
Most tools just list open ports. nkt parses your actual configs (nginx, haproxy, caddy, docker-compose, ufw, firewalld) and cross-checks them against the live host state (ss output, live containers, real TLS sockets).&lt;/p&gt;

&lt;p&gt;If your nginx config says port 80 is open, but the firewall blocks it, or the container isn't running, it flags it as a Finding. It tells you the exact file, line number, and how to fix it.&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%2F54txv0r7ppijfi6b50k3.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%2F54txv0r7ppijfi6b50k3.png" alt="Findings show exactly what's broken and where." width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Configs That Won't Brick Your Prod&lt;/strong&gt;&lt;br&gt;
Editing configs via a web UI is terrifying. What if you miss a semicolon and drop the network?&lt;/p&gt;

&lt;p&gt;We implemented Optimistic Rollbacks. When you save an nginx or haproxy config, the backend runs nginx -t or haproxy -c before applying. If it fails, the file automatically rolls back to the previous version. You literally cannot break prod with a typo. Plus, it checks if you're about to lock yourself out of SSH.&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%2Fc4tb7vzlanee6th8qd5p.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%2Fc4tb7vzlanee6th8qd5p.png" alt="The block editor for nginx/haproxy configs." width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Resource Topology&lt;/strong&gt;&lt;br&gt;
Instead of staring at raw data, we built a resource map. It visualizes the chain: External Network → Service → Listener → Pool → Backend → Container/VM. If a backend goes down, you see exactly where the chain broke.&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%2Fmwqt2edwog0oxk5swf4m.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%2Fmwqt2edwog0oxk5swf4m.png" alt="Visualizing how traffic flows through your infrastructure." width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Hub: Managing 50+ Servers&lt;/strong&gt;&lt;br&gt;
When you have 2 servers, SSH is fine. When you have 50, you need a Hub (nkt hub).&lt;/p&gt;

&lt;p&gt;The Hub is a central VPS that connects to your fleet over SSH. When you add a new host, the Hub checks the architecture (uname), cross-compiles the nkt binary on the fly (GOOS=linux GOARCH=arm64), uploads it via SFTP, and sets up a systemd unit.&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%2F41l1rgtywm2my5qp5gbh.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%2F41l1rgtywm2my5qp5gbh.png" alt="Managing a fleet of hosts from a single pane of glass." width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Centralized Security &amp;amp; Fail2ban&lt;/strong&gt;&lt;br&gt;
Managing fail2ban across 50 nodes manually is a joke. With the Hub, if an IP attacks one server, you can ban it across your entire fleet from the Hub dashboard in one click.&lt;/p&gt;

&lt;p&gt;The Hub automatically injects its own IP into the ignoreip list of all nodes so it never accidentally locks itself out.&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%2Fv55q3tih778fenyijmnl.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%2Fv55q3tih778fenyijmnl.png" alt="Centralized fail2ban management." width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We also added heuristic malware scanning. We don't just rely on ClamAV. We check for crypto-miners: processes running from /tmp, weird ld.so.preload entries, or cron jobs piping curl to sh.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fallback Channel:&lt;/strong&gt;&lt;br&gt;
Imagine you (or a junior dev) accidentally block port 22 via ufw, or sshd crashes. A normal tool would lose the server forever. The Hub opens a separate reverse TLS tunnel (fallback channel). If SSH is dead, the dashboard, web terminal, and binary updates keep working through this tunnel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Launching as a Hub&lt;/strong&gt;&lt;br&gt;
Prebuilt binaries for linux/amd64, linux/arm64 and linux/arm are in Releases together with SHA256SUMS:&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;# substitute your architecture; V is the latest version&lt;/span&gt;
&lt;span class="nv"&gt;V&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://api.github.com/repos/piqab/nkt/releases/latest | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s/.*"tag_name": *"\(.*\)".*/\1/p'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSLO&lt;/span&gt; https://github.com/piqab/nkt/releases/download/&lt;span class="nv"&gt;$V&lt;/span&gt;/nkt-linux-amd64
curl &lt;span class="nt"&gt;-fsSLO&lt;/span&gt; https://github.com/piqab/nkt/releases/download/&lt;span class="nv"&gt;$V&lt;/span&gt;/SHA256SUMS
&lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; SHA256SUMS &lt;span class="nt"&gt;--ignore-missing&lt;/span&gt;
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x nkt-linux-amd64
&lt;span class="nb"&gt;sudo mv &lt;/span&gt;nkt-linux-amd64 /usr/local/bin/nkt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From source you only need make: it decides whether to build in Docker or on the bare host (then, if Go or Node is missing, it offers to install them into ~/.local without sudo):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/piqab/nkt.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;nkt
make build                  &lt;span class="c"&gt;# dist/nkt&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;make &lt;span class="nb"&gt;install&lt;/span&gt;           &lt;span class="c"&gt;# binary, unit and nkt.env (an existing nkt.env is kept)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the Hub is incredibly straightforward. You just need Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSLO&lt;/span&gt; https://raw.githubusercontent.com/piqab/nkt/main/deploy/docker-compose.hub.release.yml
docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.hub.release.yml up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.hub.release.yml logs hub    &lt;span class="c"&gt;# grab the initial admin password&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once it's up, you just paste your target servers' SSH credentials (or keys), and the Hub takes over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitOps &amp;amp; Deployments (Even Behind NAT) Beta&lt;/strong&gt;&lt;br&gt;
We needed a way to deploy Compose stacks and K8s manifests straight from Git. You can set up pipelines triggered by Git webhooks, registry tag polling, or manual buttons.&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%2Ftgrncf8kgy2mq0rupoa8.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%2Ftgrncf8kgy2mq0rupoa8.png" alt="Deploy Compose" width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Deploying Docker Compose stacks straight from Git.&lt;br&gt;
The NAT Problem: What if your management Hub is behind a corporate NAT and can't receive GitHub webhooks?&lt;/p&gt;

&lt;p&gt;We built nkt-edge. It’s a tiny 8MB binary you put on a public VPS. It catches the webhook and tunnels it back to your Hub over a pinned TLS connection. Your Hub stays completely hidden from the internet, but still gets CI/CD triggers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes on Steroids Beta&lt;/strong&gt;&lt;br&gt;
Managing K8s via kubectl over SSH tunnels gets old fast. You can provision multi-node clusters across different VPS providers. nkt spins up the VMs via libvirt, installs k3s/kubeadm, and sets up a WireGuard mesh between the nodes.&lt;/p&gt;

&lt;p&gt;Need to debug a pod? You can open the pod's application directly in your browser via kubectl port-forward proxied through the Hub. No Ingress or NodePort required.&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%2Fk9o4mhwqwxlarqy1cc4z.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%2Fk9o4mhwqwxlarqy1cc4z.png" alt="Opening a K8s pod's app directly in the browser." width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try It Without Breaking Anything&lt;/strong&gt;&lt;br&gt;
I know installing random infra tools from the internet is sketchy. That’s why we built a fixtures mode. It runs a synthetic snapshot of a real production server with intentionally planted problems (open Redis, port conflicts, expired TLS certs, rogue containers).&lt;/p&gt;

&lt;p&gt;You can click around the UI, test the config rollbacks, and view the resource map without touching your actual machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/piqab/nkt.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;nkt
make build-dev &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;NKT_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fixtures ./nkt     
&lt;span class="c"&gt;# Opens at http://127.0.0.1:8077&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to run it on a real host locally, it’s just a curl away:&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;V&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://api.github.com/repos/piqab/nkt/releases/latest | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s/.*"tag_name": *"\(.*\)".*/\1/p'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; nkt https://github.com/piqab/nkt/releases/download/&lt;span class="nv"&gt;$V&lt;/span&gt;/nkt-linux-amd64 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 nkt /usr/local/bin/nkt
&lt;span class="nb"&gt;sudo &lt;/span&gt;nkt scan &lt;span class="c"&gt;# or tui&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Wrapping Up&lt;/strong&gt;&lt;br&gt;
Rewriting from Python to Go took time, but it eliminated dependency hell, gave us instant cross-compilation, and dropped memory usage to basically zero. nkt is now a ~25MB binary that acts as a local agent, a centralized Hub, and a TUI tool.&lt;/p&gt;

&lt;p&gt;Check out the repo, drop a star if it looks useful, and feel free to open issues or PRs. We’re actively using it in production and adding features based on what we actually need.&lt;/p&gt;

&lt;p&gt;👉 GitHub: &lt;a href="https://dev.tourl"&gt;github.com/piqab/nkt&lt;/a&gt;&lt;br&gt;
📚 Docs: &lt;a href="https://dev.tourl"&gt;piqab.github.io/nkt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What’s your go-to tool for auditing Linux hosts? Let me know in the comments!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>opensource</category>
      <category>security</category>
      <category>go</category>
    </item>
  </channel>
</rss>
