<?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: Ixlosbek Erkinov</title>
    <description>The latest articles on DEV Community by Ixlosbek Erkinov (@ix1osbek).</description>
    <link>https://dev.to/ix1osbek</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%2F3076752%2F872da104-77ea-4a91-918b-d3c3f31bd3c0.jpeg</url>
      <title>DEV Community: Ixlosbek Erkinov</title>
      <link>https://dev.to/ix1osbek</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ix1osbek"/>
    <language>en</language>
    <item>
      <title>I built a Linux server panel with zero npm dependencies</title>
      <dc:creator>Ixlosbek Erkinov</dc:creator>
      <pubDate>Thu, 15 Oct 2026 08:07:01 +0000</pubDate>
      <link>https://dev.to/ix1osbek/i-built-a-linux-server-panel-with-zero-npm-dependencies-34md</link>
      <guid>https://dev.to/ix1osbek/i-built-a-linux-server-panel-with-zero-npm-dependencies-34md</guid>
      <description>&lt;p&gt;I run a few small VPSes. Every time I wanted to know what was listening on a port, what was eating memory or how full the disk was, I opened SSH and typed the same handful of commands: &lt;code&gt;ss -tulpn&lt;/code&gt;, &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;df -h&lt;/code&gt;, &lt;code&gt;free -m&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://github.com/lnxeasy/lnxeasy" rel="noopener noreferrer"&gt;lnxeasy&lt;/a&gt;&lt;/strong&gt;: a small web panel that puts those answers on one page in the browser.&lt;/p&gt;

&lt;p&gt;This post is about what it does, and more about three decisions behind it: no dependencies, no root, and security claims you can verify yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it shows
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System:&lt;/strong&gt; hostname, IP, kernel, uptime, CPU, load average, memory and swap&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; every disk and file system with total, used and free space&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ports:&lt;/strong&gt; listening TCP and UDP ports and the process holding each one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processes:&lt;/strong&gt; every process with CPU, memory, owner and command, CPU use per core, and a details view with parent, children and ports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It only watches for now. Managing services is the next step.&lt;/p&gt;

&lt;p&gt;Installing is one command:&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;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/lnxeasy/lnxeasy/main/install.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prints an address, a username and a random password. If you'd rather read a script before running it as root (you should), download it first. The repository has a &lt;a href="https://github.com/lnxeasy/lnxeasy/blob/main/CODE_TOUR.md" rel="noopener noreferrer"&gt;CODE_TOUR.md&lt;/a&gt; that explains every step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: zero dependencies
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;package.json&lt;/code&gt; has no &lt;code&gt;dependencies&lt;/code&gt; key. The server uses only Node.js built-ins (&lt;code&gt;node:http&lt;/code&gt;, &lt;code&gt;node:https&lt;/code&gt;, &lt;code&gt;node:crypto&lt;/code&gt;, &lt;code&gt;node:fs&lt;/code&gt;, &lt;code&gt;node:child_process&lt;/code&gt;...). The browser side is plain HTML, CSS and JavaScript.&lt;/p&gt;

&lt;p&gt;Why bother? A panel that can see your whole server is a juicy target. Every package you add is code you didn't read, running with the same access. With zero dependencies the whole thing is small enough to read in an evening.&lt;/p&gt;

&lt;p&gt;Things I would normally reach for a library for:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Built with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTTP routing&lt;/td&gt;
&lt;td&gt;a 35-line router on &lt;code&gt;node:http&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password hashing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;crypto.scrypt&lt;/code&gt; with a random salt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two-step codes (TOTP)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;crypto.createHmac&lt;/code&gt; (RFC 6238 is short)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessions&lt;/td&gt;
&lt;td&gt;256-bit random tokens, &lt;code&gt;HttpOnly&lt;/code&gt;, &lt;code&gt;Secure&lt;/code&gt;, &lt;code&gt;SameSite=Strict&lt;/code&gt; cookies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;the built-in &lt;code&gt;node:test&lt;/code&gt; runner (217 tests)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Env files&lt;/td&gt;
&lt;td&gt;&lt;code&gt;node --env-file-if-exists&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Modern Node covers far more than people assume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: it never runs as root
&lt;/h2&gt;

&lt;p&gt;Here is the awkward part: Linux shows which process owns a port only to that process's owner or to root. A monitoring panel wants exactly that information.&lt;/p&gt;

&lt;p&gt;Running the whole web server as root would be the easy fix and the wrong one. Instead the installer adds a tiny systemd socket-activated helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# lnxeasy-helper@.service (simplified)
&lt;/span&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/ss -tulpnH&lt;/span&gt;
&lt;span class="py"&gt;StandardInput&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;systemd listens on &lt;code&gt;/run/lnxeasy/helper.sock&lt;/code&gt;; only root and the &lt;code&gt;lnxeasy&lt;/code&gt; group can open it.&lt;/li&gt;
&lt;li&gt;For each connection it starts one fixed command. Input is thrown away, output goes back through the socket.&lt;/li&gt;
&lt;li&gt;The unit cannot write to the file system and is stopped after ten seconds at most.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The network-facing process has no privileges, and the privileged process cannot be told what to do. If lnxeasy were fully taken over, the most it could get from the helper is the list of listening ports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 3: claims you can check with grep
&lt;/h2&gt;

&lt;p&gt;"Secure" is easy to write in a README. So instead of only listing features, CODE_TOUR.md has a "ten-minute check": commands you run against the repo, each checking one claim.&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;# It never connects out to another machine&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s2"&gt;"fetch&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;|http&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;request|https&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;request|net&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;connect|createConnection"&lt;/span&gt; server.js cli.js src

&lt;span class="c"&gt;# It never runs text as code&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="s2"&gt;eval&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;|new Function|vm&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; server.js cli.js src public

&lt;span class="c"&gt;# The page never turns server text into HTML&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"innerHTML&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;outerHTML&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;insertAdjacentHTML"&lt;/span&gt; public
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected output is written next to each command. If one of them ever prints something unexpected, that's a bug.&lt;/p&gt;

&lt;p&gt;The rest of the security model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Login required everywhere except the login page and a health check&lt;/li&gt;
&lt;li&gt;Refuses to listen on a public address without HTTPS (self-signed by default, or bring your own with &lt;code&gt;--cert&lt;/code&gt;/&lt;code&gt;--key&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Optional TOTP; a code works only once, even across restarts&lt;/li&gt;
&lt;li&gt;Five failed attempts lock an address out for 15 minutes&lt;/li&gt;
&lt;li&gt;Optional IP allowlist (&lt;code&gt;lnxeasy allow 203.0.113.7&lt;/code&gt;); everyone else gets no answer at all&lt;/li&gt;
&lt;li&gt;Strict Content Security Policy, &lt;code&gt;Origin&lt;/code&gt; check on every write&lt;/li&gt;
&lt;li&gt;Commands run with &lt;code&gt;execFile&lt;/code&gt; and fixed arguments, never a shell&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it does not protect against
&lt;/h2&gt;

&lt;p&gt;I think this part matters as much as the list above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It has &lt;strong&gt;not&lt;/strong&gt; had an independent security audit. I tested it myself against a local install (unauthorised access, path traversal, injection, XSS, brute force, flooding) and fixed six low and medium findings in v0.4.1. They're listed in &lt;a href="https://github.com/lnxeasy/lnxeasy/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The first visit with a self-signed certificate trusts whatever answers. Compare the fingerprint the installer prints, or use a real certificate.&lt;/li&gt;
&lt;li&gt;It won't stop a large DDoS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a server that matters: allowlist your own IP, turn on two-step sign-in, or keep the panel behind a VPN.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next, and how to help
&lt;/h2&gt;

&lt;p&gt;lnxeasy is at v0.4.1 and early. Planned: a services view (systemd units), logs, and simple history graphs.&lt;/p&gt;

&lt;p&gt;If you'd like to help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;a href="https://github.com/lnxeasy/lnxeasy" rel="noopener noreferrer"&gt;Star the repo&lt;/a&gt; so others find it&lt;/li&gt;
&lt;li&gt;Try it on a test server and &lt;a href="https://github.com/lnxeasy/lnxeasy/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt; with what broke or what you missed&lt;/li&gt;
&lt;li&gt;Read the code tour and tell me what looks wrong. Security reports go through &lt;a href="https://github.com/lnxeasy/lnxeasy/security/advisories/new" rel="noopener noreferrer"&gt;private reporting&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pick something up: &lt;a href="https://github.com/lnxeasy/lnxeasy/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt; explains the module layout (parser → service → routes), and adding a new view is mostly one folder&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading. I'd love to hear what you'd want on a page like this.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post was drafted with the help of an AI assistant and reviewed by me.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>node</category>
      <category>opensource</category>
      <category>security</category>
    </item>
  </channel>
</rss>
