<?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: tyxak</title>
    <description>The latest articles on DEV Community by tyxak (@tyxak).</description>
    <link>https://dev.to/tyxak</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%2F3941080%2F6762ff41-dc6c-43b1-ab36-aca5ac1d16a5.png</url>
      <title>DEV Community: tyxak</title>
      <link>https://dev.to/tyxak</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tyxak"/>
    <language>en</language>
    <item>
      <title>RemotePower 8.0.0: ten setup wizards and a security review that fixed 19 findings</title>
      <dc:creator>tyxak</dc:creator>
      <pubDate>Wed, 11 Nov 2026 14:31:56 +0000</pubDate>
      <link>https://dev.to/tyxak/remotepower-800-ten-setup-wizards-and-a-security-review-that-fixed-19-findings-f7e</link>
      <guid>https://dev.to/tyxak/remotepower-800-ten-setup-wizards-and-a-security-review-that-fixed-19-findings-f7e</guid>
      <description>&lt;p&gt;RemotePower 8.0.0 is out. It is the first release of a new major version, published on 11 November, and it comes with ten setup wizards, a text size that goes up to 300% and fixes for 19 findings from a security review, one of them rated High. Every release gets a codename ending in "Matters"; this one is Pil0tMatters.&lt;/p&gt;

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

&lt;p&gt;If you haven't met RemotePower yet, it is a self-hosted tool for monitoring and managing a fleet of Linux, Windows and macOS hosts. A server and a small agent on each host give you a web UI with monitoring and alerting, a CMDB, CVE scanning, patching, compliance reports, a helpdesk and remote access. The agents poll outbound only, so nothing listens on the managed host. It started as a web page with one button that shut a machine down, which I wrote in Python one afternoon. Scope creep did the rest, and these days Claude Code writes most of the code while I decide what goes in. It is MIT licensed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup wizards
&lt;/h2&gt;

&lt;p&gt;Several things in RemotePower take more than one screen to set up, and 8.0.0 adds a guided setup for ten of them, usually behind a &lt;strong&gt;Guide me&lt;/strong&gt; button: a host, a notification destination, an integration, e-mail, single sign-on, receiving syslog, receiving network flows, a relay satellite or scan worker, the agent push channel and the SSH gateway.&lt;/p&gt;

&lt;p&gt;The OIDC card is seven boxes, and what goes in them depends on what you created in your identity provider. E-mail is switched on per event, so a form that is filled in and tested can still send nothing. A wizard asks for one thing at a time, in the order the work happens, and checks as it goes. The dialogs it sits next to are still there, and nothing changes until you press a button.&lt;/p&gt;

&lt;p&gt;The host wizard waits for the host to check in and then shows what it got: name, group, tags, OS and address. Before anything real goes out, the notification wizard saves the destination switched off, sends one message and shows the answer, with the status code and what it usually means when delivery failed. E-mail and sign-on are tested with the settings you typed, before anything is saved, so a wrong password or port never replaces settings that work. The push wizard won't continue until the push daemon answers, and the SSH gateway wizard waits for the gateway service to connect.&lt;/p&gt;

&lt;p&gt;The sign-on guide knows the quirks of Authentik, Authelia, Keycloak, Pocket ID and Zitadel. Zitadel, for example, sends no &lt;code&gt;groups&lt;/code&gt; claim by itself, so everyone stays a viewer until an Action adds one, and the guide says so on the step where you register the client.&lt;/p&gt;

&lt;p&gt;The e-mail, sign-on and integration wizards save only on their last button, so closing one halfway changes nothing. Six long dialogs (maintenance window, monitor target, backup job, auto-patch policy, rollout and host profile) also got a &lt;strong&gt;Step by step&lt;/strong&gt; switch that shows the same form a few fields at a time, says at each step what the form would refuse at the end, and finishes on a review of your choices.&lt;/p&gt;

&lt;p&gt;Testing the wizards and the step-by-step form turned up two bugs. One was an old bug in the New backup job dialog. It remembered which hosts you had ticked, but its save read only the boxes on screen, so a host you had ticked and then filtered out of the list was left out of the job without a word. The other showed only when you typed fast. A wizard moved the focus to its first box a frame after it drew a step, and keystrokes for a box you had just chosen landed in the wrong one. That happened in 12 of 120 tries on a loaded machine and in none of 200 after the fix.&lt;/p&gt;

&lt;p&gt;Escape closes a wizard and returns the focus to where it was, Tab stays inside the dialog, every step is announced by name, and Ctrl or Cmd with Enter presses the primary button.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interface
&lt;/h2&gt;

&lt;p&gt;Text size now runs from 80% to 300% in ten steps, from the user menu or under My Account → Appearance. A window that is too narrow for a step holds the largest ones back: 200% at most on a window 1,440 pixels wide and 110% on a phone. A dialog still fits the window at 300%, and a menu opens next to its button.&lt;/p&gt;

&lt;p&gt;Thirteen more pages, among them Server Status, Patches, Tickets and Containers, show one group at a time as tabs. If you prefer the long scroll, choose &lt;strong&gt;One long page&lt;/strong&gt; under My Account → Appearance → Page sections; the choice is kept in your browser.&lt;/p&gt;

&lt;p&gt;Home keeps up to eight named layouts. You can start one from a copy, from an empty page or from one of four ready-made ones (Fleet overview, Security, Patching and Capacity), and a wall display link cycles through them. Smaller changes include a heading on every page for screen readers, controls at least 24 pixels across, and cron schedules written out as sentences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apps, monitors and connectors
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App discovery.&lt;/strong&gt; The Linux agent reads listening sockets, containers and running services and says which of 63 well-known apps it found. Each one gets Monitor and Connect buttons, and Connect opens the integration wizard with the address and port filled in. Nothing is probed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handshake monitors.&lt;/strong&gt; SMTP, IMAP, SSH banner and NTP monitors check that a service answers as it should: the greeting, the server's abilities, an encrypted session, the clock's offset. They send no mail and log nobody in, and they run from the server, a Linux host's agent or a satellite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connectors.&lt;/strong&gt; Tailscale, Headscale and Cloudflare Tunnel are new, Apprise is a destination of its own, and several existing connectors (Pi-hole, Immich, Proxmox Backup Server, UniFi) read more of what their products report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A read-only MCP endpoint.&lt;/strong&gt; &lt;code&gt;POST /api/mcp&lt;/code&gt; speaks the Model Context Protocol from the server itself, so a client that can send a header connects without a local file. It offers 16 read tools, and every call runs as the API key that made it, so that key's scope, addresses, expiry and rate limit apply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image updates with a rollback.&lt;/strong&gt; The Image updates tab can update the images you list, in shadow mode first, where it records what it would do and queues nothing. An option rolls a project back to its previous images when the check after an update finds an unhealthy container.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic.&lt;/strong&gt; Each Linux agent reads its established TCP connections with &lt;code&gt;ss&lt;/code&gt; once a minute by default, and Threat Intel shows which addresses each host talks to, which programs open the connections and how much each moved. A watch list raises an alert on a match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detections.&lt;/strong&gt; A sweep every five minutes runs six rules over what the agents already send, such as a shell or &lt;code&gt;nc&lt;/code&gt; holding a connection to a public address, or a new SSH key followed by a new destination. Each finding raises one alert, so it can be acknowledged, muted and routed like any other.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Security
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Method
&lt;/h3&gt;

&lt;p&gt;The 8.0.0 review is published in full as &lt;a href="https://github.com/tyxak/remotepower/blob/v8.0.0/docs/security-review-8.0.0.md" rel="noopener noreferrer"&gt;security-review-8.0.0.md&lt;/a&gt;. It asked two questions. What can each new feature be made to do that it should not? And where does the code follow a rule in most places and miss it in one or two?&lt;/p&gt;

&lt;p&gt;My rule for it was that anything rated Medium or higher gets fixed, even when the fix is big. Claude Code did most of the finding and fixing. Every finding was reproduced and fixed on a seeded copy of the product, never on anyone's installation, and each fix has tests that were also run against the code with the fix taken out.&lt;/p&gt;

&lt;p&gt;The second question found most of the problems, so it was answered with lists generated by script, and the ratio was printed each time. The command queue, for example, has 30 writers: 12 ask for approval or call a gateway that does, 18 are listed with a reason, and none is neither. A 4-of-8 ratio found a bug in host deletion, which cleared four of the eight settings kept by host id and left the other four behind.&lt;/p&gt;

&lt;p&gt;Scanners ran on the final tree: CodeQL, bandit, semgrep, gitleaks, shellcheck, pip-audit, nuclei and wapiti. Where a scanner could be given something it ought to find, it was. gitleaks got three planted tokens and found all three, and semgrep got a known-bad file and flagged it.&lt;/p&gt;

&lt;p&gt;The authorization matrix called every routed operation (1,106 of them, taken from the real route tables) as 26 kinds of caller, from the platform operator and scoped roles to roles of two organisations, API keys, host tokens, a wrong token and no token. Each caller went against the objects of its own organisation, of the other one and of the default one, and every answer was judged against the check the handler itself names. That came to 86,805 requests in 60 runs, on the final tree. The review found no way for a route added in 8.0.0 to reach another organisation's data.&lt;/p&gt;

&lt;p&gt;Generated tests (29 Hypothesis properties) found five of the 19, one of them a Medium. A run of 1,904 hostile requests to the routes added since 7.5.0, with every optional module off and then on, produced no 500, no exception and no unanswered request. The release also went through the full suite of about 22,000 tests on the JSON and SQLite storage backends before it was tagged.&lt;/p&gt;

&lt;h3&gt;
  
  
  Findings
&lt;/h3&gt;

&lt;p&gt;One High, nine Medium, eight Low or hardening items and one informational item, 19 in all, and every one is fixed. The review lists each with the test that covers its fix.&lt;/p&gt;

&lt;p&gt;The High was in the web page. The settings form in a host's drawer read nine fields from a list that never carried them. If you changed only a host's group, the page posted those nine fields empty, and the server treats an empty list as "clear this". The host lost its command allowlist, its watched units and files and its log rules, and an agentless host lost its SSH mode, user and port. The form now asks the server for the host's own settings first, and stays read-only, with a line that says why, if the answer doesn't come.&lt;/p&gt;

&lt;p&gt;Three of the Mediums were leaks between organisations on an installation with tenants. Two matched a host to your organisation by display name, so another organisation's host with the same name put its events into your compliance evidence and its held image updates on your page. The third was the Home page, which builds its summaries from stores that are not keyed by host: a tenant's users got figures for the whole installation, including open alerts by severity, monitors, VPN totals, backup jobs and the list of integrations with the addresses in their error texts.&lt;/p&gt;

&lt;p&gt;One finding came out of a scanner being wrong. wapiti reported an SQL injection in the &lt;code&gt;error_description&lt;/code&gt; parameter of the SSO callback. It was not one. The answer was the same redirect for every value, and the scanner had read the server's rate limit, a 429, as a difference between its true and false probes. But the report pointed at that handler, and reading the handler showed that the callback wrote &lt;code&gt;error&lt;/code&gt; and &lt;code&gt;error_description&lt;/code&gt; to the error log as they came. A line break in either started a log line of its own, so anyone who could reach the server could add a &lt;code&gt;[remotepower] ...&lt;/code&gt; entry. Both values now go through the log helpers, which turn control characters and line separators into &lt;code&gt;?&lt;/code&gt; and cut the code to 80 characters and the description to 240.&lt;/p&gt;

&lt;p&gt;Python's &lt;code&gt;isdigit()&lt;/code&gt; accepts characters that &lt;code&gt;int()&lt;/code&gt; refuses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="s2"&gt;"²"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# True&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"²"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# ValueError: invalid literal for int() with base 10: '²'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code checked text with &lt;code&gt;isdigit()&lt;/code&gt; and then called &lt;code&gt;int()&lt;/code&gt;, so a superscript two in a port, a CIDR, a cron field or an object id turned a 400 into a 500 in 17 places. The worst case was a container report that listed &lt;code&gt;²&lt;/code&gt; as a published port, which made three fleet views answer 500 for every user.&lt;/p&gt;

&lt;p&gt;One defect turned up by accident. A system update moved the development machine to OpenSSL 4.1.0 in the middle of the pre-release test run, and the test that runs a real agent against a real HTTPS server began failing every time. The cause was a defect in the product. Server certificates issued by the built-in CA (&lt;code&gt;tools/gen-ca.sh&lt;/code&gt; and the Settings button that generates a CA and a server certificate) had no authority key identifier, which RFC 5280 asks for in every certificate a CA signs. OpenSSL 4.1 and Python 3.13 or later refuse such a certificate with &lt;code&gt;Missing Authority Key Identifier&lt;/code&gt;, so an agent pinned to a generated CA could not connect from a host that has both. Both generators add the extension now, and &lt;code&gt;gen-ca.sh&lt;/code&gt; checks its own output in strict mode. The tests meant to cover this had run &lt;code&gt;openssl verify&lt;/code&gt; without strict mode, which accepts the old certificate.&lt;/p&gt;

&lt;p&gt;Three more Mediums:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A host name that started with &lt;code&gt;=&lt;/code&gt; ran as a formula when a CSV export was opened in a spreadsheet.&lt;/li&gt;
&lt;li&gt;The declarative export, which is meant to be committed to a repository, held webhook header values (an &lt;code&gt;Authorization&lt;/code&gt; or &lt;code&gt;X-API-Key&lt;/code&gt; credential, say) in plain text. An earlier fix had masked them in the support bundle and the backup and missed this third place.&lt;/li&gt;
&lt;li&gt;Run command on an SSH host, the Ansible runner and the web terminal ignored quarantine and maintenance mode, which the command queue and the SSH gateway already honoured. All three ask the same question as the queue now, and a test finds the command channels in the syntax tree and checks that each one asks it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Guardrails on the new features
&lt;/h3&gt;

&lt;p&gt;The MCP endpoint, Traffic, Detections and automatic image updates are all off until you switch them on.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The MCP endpoint can only read. A tool call is a request for one of the server's own read routes, made with the caller's own token, and it passes through every check the web server's front door makes. A test asserts that this list of checks equals the list in the request handler, so a check added there and missing here fails the build.&lt;/li&gt;
&lt;li&gt;The handshake monitors connect to the address they are given, so each passes an address rule when it connects. Link-local and cloud-metadata addresses are always refused and loopback only where allowed, and the address is checked when the name is resolved and again when the connection is made. The agent and the satellite carry a copy of the server's probe code that a script writes and a test checks is identical.&lt;/li&gt;
&lt;li&gt;Traffic captures no packets and reads no payload. The agent reads its connection table with &lt;code&gt;ss&lt;/code&gt;, adds up what each connection moved by remote address, port and program, and sends the 100 busiest rows.&lt;/li&gt;
&lt;li&gt;Detections runs over data the server already holds. It opens no connection and runs no command on a host, and one sweep raises at most 100 alerts for the whole fleet, worst first, so a single address that turns bad cannot flood an inbox.&lt;/li&gt;
&lt;li&gt;Automatic image updates put commands on a host through the same gateway as every other command, so maintenance mode, quarantine, audit mode and four-eyes approval apply. Turning the mode on needs a fresh sign-in.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Not covered
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A live probe of a running installation: every probe ran against a seeded copy.&lt;/li&gt;
&lt;li&gt;Real Windows and macOS hosts. Traffic, the app finder and the rollback are Linux-only.&lt;/li&gt;
&lt;li&gt;Real services behind the connectors. The UniFi, Tailscale, Headscale and Cloudflare connectors ran against stand-ins written from the vendors' published descriptions, and no Apprise server was run.&lt;/li&gt;
&lt;li&gt;Safari itself. WebKit 26.4 as built by Playwright ran on Linux, next to Firefox and Chromium.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two limits are by design. The agent runs as root, so a fully compromised server is root on its hosts. And a role scoped to groups, tags or sites limits what a user can change but does not hide the other hosts, so give anyone who must not see them a tenant. If you find something the review missed, please report it privately through GitHub's &lt;a href="https://github.com/tyxak/remotepower/security/advisories/new" rel="noopener noreferrer"&gt;Report a vulnerability&lt;/a&gt; form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Update the agents. The agent version is 8.0.0, and finding apps, the new monitor types and the image-update rollback all run on the host.&lt;/li&gt;
&lt;li&gt;Hard-reload the dashboard once so the new front end loads.&lt;/li&gt;
&lt;li&gt;Long pages open as tabs now. &lt;strong&gt;One long page&lt;/strong&gt; under My Account → Appearance → Page sections gets the old layout back.&lt;/li&gt;
&lt;li&gt;A host in quarantine or maintenance mode refuses Run command, an Ansible run and the web terminal. All three ignored it before.&lt;/li&gt;
&lt;li&gt;Deleting a host takes more with it: its CMDB record (notes, documents and credentials), runbook, schedules, mutes, backup jobs and scan schedules, and its place in groups, services and links. Tickets, time entries, alerts, the audit trail and command history stay.&lt;/li&gt;
&lt;li&gt;Server certificates from the built-in CA name their issuer now. An older certificate keeps working where it did; to replace it, run &lt;code&gt;tools/gen-ca.sh&lt;/code&gt; again or press &lt;strong&gt;Generate certificate&lt;/strong&gt; in Settings. The CA is reused, so agents keep their trust.&lt;/li&gt;
&lt;li&gt;Settings → Alerts inbox has a new switch, "Let every signed-in user acknowledge, resolve and assign alerts". It is on, as it was; turn it off to limit those actions to administrators.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting it
&lt;/h2&gt;

&lt;p&gt;The release page has the tarball, a detached signature and a SHA-256 file, and the tag is signed. The container images (&lt;code&gt;remotepower&lt;/code&gt; and &lt;code&gt;remotepower-agent&lt;/code&gt;) are on GitHub's container registry for amd64 and arm64, and the install guide shows how to verify their cosign signature before you run them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull ghcr.io/tyxak/remotepower:8.0.0
cosign verify ghcr.io/tyxak/remotepower:8.0.0 \
  --certificate-identity-regexp &lt;span class="s1"&gt;'^https://github.com/tyxak/remotepower/\.github/workflows/release\.yml@'&lt;/span&gt; \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/tyxak/remotepower/releases/tag/v8.0.0" rel="noopener noreferrer"&gt;release page&lt;/a&gt; and the &lt;a href="https://github.com/tyxak/remotepower/blob/v8.0.0/docs/wizards.md" rel="noopener noreferrer"&gt;wizards guide&lt;/a&gt; are in the repo, the &lt;a href="https://github.com/tyxak/remotepower/blob/v8.0.0/docs/install.md" rel="noopener noreferrer"&gt;install guide&lt;/a&gt; covers the rest, and a read-only &lt;a href="https://demoremote.tvipper.com" rel="noopener noreferrer"&gt;demo&lt;/a&gt; runs 8.0.0.&lt;/p&gt;

&lt;p&gt;The text this release added is available in Chinese, Hindi, Spanish, Arabic, German and French, and no native speaker has read it yet. If a line reads wrong in your language, open an issue or start a discussion and tell me.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I drafted this post with Claude Code. Its figures come from the 8.0.0 release notes and the security review in the repo.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>selfhosted</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>I let my servers fix themselves. The hard part was teaching them to say no</title>
      <dc:creator>tyxak</dc:creator>
      <pubDate>Sat, 15 Aug 2026 23:01:44 +0000</pubDate>
      <link>https://dev.to/tyxak/i-let-my-servers-fix-themselves-the-hard-part-was-teaching-them-to-say-no-5g01</link>
      <guid>https://dev.to/tyxak/i-let-my-servers-fix-themselves-the-hard-part-was-teaching-them-to-say-no-5g01</guid>
      <description>&lt;p&gt;RemotePower is a free, MIT licensed, self-hosted fleet manager: monitoring, alerting, a CMDB, CVE scanning, patching and remote management in one web UI. It exists because I got tired of running six dashboards for one rack. Nginx, Python and plain JavaScript, no build step, no Kubernetes, and agents that poll out over HTTPS so nothing opens on your hosts.&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%2Foudwfgu148vr0lhm0h2v.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%2Foudwfgu148vr0lhm0h2v.png" alt=" " width="690" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One thing up front, since I'd want to know: I did not type most of this code. An AI wrote it with me steering, reviewing and arguing with it. The bugs are still mine. (The lines I did type were typed in vi, obviously.)&lt;/p&gt;

&lt;p&gt;7.0 "Aut0nomyMatters" just shipped, and it is the first release where the fleet can act on its own alerts. I know how that sounds. It made me nervous too, and I'm the one who shipped it. Nothing rests on a model promising to behave. The safety is the parts you already trusted: signed commands, four-eyes approval, maintenance windows, tenant isolation, an audit log, and the checks that already define what healthy means, wired into a loop.&lt;/p&gt;

&lt;p&gt;Everything starts off. Move one tenant to shadow mode and the loop reaches a verdict per alert, writes it down, and touches nothing. An intern with a diary and no sudo. Read the receipts for a while, then grant specific actions. It only acts on precedent from your own fleet: same signature closed at least twice before, 70% success or better. No precedent, no action, which is a higher bar than I hold myself to at 3am. Before acting it works out what goes dark, and that "what breaks if I reboot this?" preview works even with autonomy off. Reboots, patches and the rest of the destructive set need a backup that was actually restored and verified in the last month, and they escalate to a human instead of firing.&lt;/p&gt;

&lt;p&gt;Every decision leaves a receipt, and 15 minutes later the host's checks are sampled again. If more are failing, you hear about it. There is no rollback, and 7.0 stops implying one. You cannot un-reboot a host.&lt;/p&gt;

&lt;p&gt;Also new: an All pages map of the UI (89 pages, and yes, I was losing them too), 45 queryable host fields in the Data Explorer instead of 14, tenant admin in the UI, Arch/CachyOS/Manjaro patching unstuck, and a security pass that closed seven gaps. The changelog is long because I write down what broke, including six of my own test gates that reported success while measuring nothing. When an AI writes your code, you learn fast to trust measurements over prose.&lt;/p&gt;

&lt;p&gt;Try it with docker compose up -d, or clone and sudo bash install.sh. Read-only demo at &lt;a href="https://demoremote.tvipper.com" rel="noopener noreferrer"&gt;https://demoremote.tvipper.com&lt;/a&gt; (demo / demo). Code: &lt;a href="https://github.com/tyxak/remotepower" rel="noopener noreferrer"&gt;https://github.com/tyxak/remotepower&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If a shadow-mode receipt ever makes you think "I would not have done that", tell me. That is the bug report I want most.&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%2F7pz8q0l5lv5thq61g877.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%2F7pz8q0l5lv5thq61g877.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ai</category>
      <category>opensource</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>My NAS kept its encryption keys on itself — so I built a KMIP server into my open-source monitoring tool</title>
      <dc:creator>tyxak</dc:creator>
      <pubDate>Thu, 30 Jul 2026 14:14:16 +0000</pubDate>
      <link>https://dev.to/tyxak/my-nas-kept-its-encryption-keys-on-itself-so-i-built-a-kmip-server-into-my-open-source-monitoring-8o7</link>
      <guid>https://dev.to/tyxak/my-nas-kept-its-encryption-keys-on-itself-so-i-built-a-kmip-server-into-my-open-source-monitoring-8o7</guid>
      <description>&lt;p&gt;I maintain &lt;a href="https://github.com/tyxak/remotepower" rel="noopener noreferrer"&gt;RemotePower&lt;/a&gt;, an open-source (MIT)&lt;br&gt;
self-hosted control plane for a small fleet or a homelab — monitoring, alerting,&lt;br&gt;
CVE scanning, patching, a CMDB. It started as a tool for my own machines and&lt;br&gt;
grew from there. Version 6.4.1 just shipped, and the headline feature came out&lt;br&gt;
of a realisation that had been quietly bothering me for a while.&lt;/p&gt;
&lt;h2&gt;
  
  
  The itch
&lt;/h2&gt;

&lt;p&gt;My Synology encrypts its volumes. Good. It stores the encryption keys for those&lt;br&gt;
volumes... on the Synology. Same box, same disks, same burglar. Whoever walks&lt;br&gt;
off with the appliance gets the lock and the key together, which defeats a lot&lt;br&gt;
of the point of encrypting at all.&lt;/p&gt;

&lt;p&gt;The industry already has an answer: &lt;strong&gt;KMIP&lt;/strong&gt; (Key Management Interoperability&lt;br&gt;
Protocol), the OASIS standard that lets an appliance fetch its keys from an&lt;br&gt;
external key manager at mount time. Synology DSM, TrueNAS and VMware vSphere&lt;br&gt;
all speak it natively. The problem is the other end of the conversation —&lt;br&gt;
everything I found that implements the &lt;em&gt;server&lt;/em&gt; side is an enterprise appliance&lt;br&gt;
or a heavyweight commercial suite. There was no small, self-hostable KMIP&lt;br&gt;
server a homelab could point a NAS at.&lt;/p&gt;

&lt;p&gt;So I built one into the tool that was already monitoring those boxes.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;

&lt;p&gt;It's deliberately conservative in design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Off by default.&lt;/strong&gt; Nothing listens until you enable it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A separate, sandboxed sidecar&lt;/strong&gt; (&lt;code&gt;remotepower-kmipd&lt;/code&gt; on tcp/5696) that
terminates mutual TLS and parses the KMIP wire format — and holds &lt;em&gt;no&lt;/em&gt; keys,
no database access, no master key. It runs under systemd &lt;code&gt;DynamicUser=yes&lt;/code&gt;
with no access to the data directory. All the material that matters (the CA,
the key objects, encryption at rest, the activity log) lives behind the main
API, reached over loopback with a shared secret.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutual TLS only.&lt;/strong&gt; Enabling it generates a private CA; each appliance gets
its own client certificate through a three-step wizard, and the private key
is shown once and never stored server-side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An encrypted recovery bundle&lt;/strong&gt;, because key custody without a way back
isn't a feature — it's a liability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Setup is one installer flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; ./install-server.sh &lt;span class="nt"&gt;--with-kmip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The supported operation set is deliberately matched to what DSM actually calls&lt;br&gt;
in practice (an appliance that hits an unimplemented KMIP operation tends to&lt;br&gt;
abort its whole setup with a generic error, so partial support is worse than&lt;br&gt;
none).&lt;/p&gt;

&lt;p&gt;The footgun, stated plainly&lt;/p&gt;

&lt;p&gt;External key custody has a real trade-off, and I'd rather write it here than&lt;br&gt;
have someone discover it at 2am: once an appliance stores its keys here, it&lt;br&gt;
needs this server reachable at its own boot time to mount encrypted storage.&lt;br&gt;
A NAS that keeps its keys on a VM hosted by that same NAS will not come back&lt;br&gt;
from a reboot. That circular dependency is the classic self-hosted key-server&lt;br&gt;
mistake, and the docs lead with it.&lt;/p&gt;

&lt;p&gt;Two things in 6.4.1 exist specifically to soften the sharp edges: the key&lt;br&gt;
server now has its own row on the server-status page (it was the one sidecar&lt;br&gt;
without one, despite being the one whose outage stops volumes mounting), and it&lt;br&gt;
warns ahead of its own certificate expiry — because an expired client cert&lt;br&gt;
silently ends an appliance's access to its keys, and the failure surfaces at&lt;br&gt;
the next reboot, which is the worst possible moment.&lt;/p&gt;

&lt;p&gt;The rest of the release&lt;/p&gt;

&lt;p&gt;Less glamorous, but honest work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A per-destination minimum severity filter that had never filtered — the
saved values sat in a range below every event's actual priority, so all four
choices behaved identically. It works now, and existing settings keep exactly
the behaviour they had, so nobody's alerting changes without asking.&lt;/li&gt;
&lt;li&gt;Laptops on Windows and macOS report like laptops. Three signals were
Linux-only, which left the laptop features dead on precisely the hosts they
were designed for: a closed lid was treated like a dead server instead of
getting the grace period, and battery health never reached the alerts.&lt;/li&gt;
&lt;li&gt;A UI pass for the small stuff — one body size, one icon size, buttons that
sit in a row, panels that scroll instead of growing without limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it&lt;br&gt;
                                                                                                                                                                                                                    There's a read-only demo seeded with synthetic devices, alerts and CVEs —&lt;br&gt;
no install needed: &lt;a href="https://demoremote.tvipper.com" rel="noopener noreferrer"&gt;https://demoremote.tvipper.com&lt;/a&gt;                                                                                                                                                                   (login demo / demo, resets every few hours).&lt;br&gt;
                                                                                                                                                                                                                    Code and docs: &lt;a href="https://github.com/tyxak/remotepower" rel="noopener noreferrer"&gt;https://github.com/tyxak/remotepower&lt;/a&gt;&lt;br&gt;
                                                                                                                                                                                                                    It's a one-person project and I have no illusions about that. If you've run key&lt;br&gt;
servers in production — or you can poke a hole in the KMIP design — I would                                                                                                                                         genuinely rather hear it from you than from an incident. Issues and discussions&lt;br&gt;
are open.&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%2F6veh4r8ebpzqxcrzveby.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%2F6veh4r8ebpzqxcrzveby.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>security</category>
      <category>homelab</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Making Windows a first-class citizen in my self-hosted fleet manager (RemotePower 6.2)</title>
      <dc:creator>tyxak</dc:creator>
      <pubDate>Tue, 14 Jul 2026 19:42:51 +0000</pubDate>
      <link>https://dev.to/tyxak/making-windows-a-first-class-citizen-in-my-self-hosted-fleet-manager-remotepower-62-l5i</link>
      <guid>https://dev.to/tyxak/making-windows-a-first-class-citizen-in-my-self-hosted-fleet-manager-remotepower-62-l5i</guid>
      <description>&lt;p&gt;For the last few releases, RemotePower's Windows support was… let's be honest, "kind of there." The agent ran, it reported in, but it did so as a scheduled task that quietly died if the box hiccuped, and half the things that were effortless on Linux needed hand-holding on Windows.&lt;/p&gt;

&lt;p&gt;6.2 — codename "Daem0nMatters" — is the release where I stopped apologizing for that.&lt;/p&gt;

&lt;p&gt;The agent is now a real service, everywhere&lt;/p&gt;

&lt;p&gt;The headline change is boring in the best way: the agent runs as a proper supervised service on every OS it supports.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Windows — an actual Windows service (via pywin32), registered with the Service Control Manager. It shows up in services.msc, and the SCM restarts it if it crashes. No more scheduled-task-that-silently-stops. (If you've ever fought "Error 1053: the service did not respond in time" — that self-heals now.)&lt;/li&gt;
&lt;li&gt;macOS — a launchd job with KeepAlive.&lt;/li&gt;
&lt;li&gt;Linux — systemd, as before.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical upshot: the agent survives reboots and crashes on its own, on the OS where that was least true before.&lt;/p&gt;

&lt;p&gt;Onboarding a Windows box is now a one-liner&lt;/p&gt;

&lt;p&gt;Just like Linux. You paste one elevated-PowerShell line:&lt;/p&gt;

&lt;p&gt;iwr /install.ps1?t= | iex&lt;/p&gt;

&lt;p&gt;…and it downloads the agent, verifies its SHA-256, installs Python if it's missing (winget on modern Windows, the official python.org silent installer otherwise), enrolls with a one-time token, and registers the service. Nothing else to configure on a bare box.&lt;/p&gt;

&lt;p&gt;It actually understands Windows now&lt;/p&gt;

&lt;p&gt;A Windows host isn't just "a Linux host with worse paths," so the agent stopped pretending. New this release:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Security posture as first-class checks — BitLocker volume status, Windows Defender real-time protection and signature age, the Windows Firewall per profile, and the Windows Update service all render as Checks rows, drive alerts, and now show at-a-glance in the device drawer. "Real-time protection is off on WEB03" is a thing you find out before it matters.&lt;/li&gt;
&lt;li&gt;A windows_service check type — the analogue of systemd_unit: "is service X running?" via Get-Service.&lt;/li&gt;
&lt;li&gt;Real Event Log reading — a persistent RecordId cursor (so you don't re-ingest or miss events across restarts), the Security channel, and Event IDs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And two genuinely embarrassing bugs got fixed along the way: the self-updater used to report success even when it failed (rc=0 unconditionally), and it resolved helper binaries by bare name — a SYSTEM PATH-hijack waiting to happen. Both closed, and the self-update is signature-checked when you enable signed updates.&lt;/p&gt;

&lt;p&gt;The rest of 6.2, briefly&lt;/p&gt;

&lt;p&gt;Windows was the theme, but it rode in with a batch of gap-closing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A governed AI executor — you can ask the assistant to remediate a host, but it can only pick one action from a catalog you wrote, by exact id. It can't author a command, so there's no prompt-injection path to a shell.&lt;/li&gt;
&lt;li&gt;A PII scan (cards by Luhn, national IDs, IBANs, emails), EDR coverage reporting (cross-references Wazuh/CrowdStrike/SentinelOne against the real fleet to find hosts with no endpoint protection), and DNS-blocker control (pause Pi-hole/AdGuard for a bounded, self-re-enabling window).&lt;/li&gt;
&lt;li&gt;Three new tripwires: privileged-group changes (sudo/wheel/Administrators), Defender-off, and USB physical-access.&lt;/li&gt;
&lt;li&gt;A host-wide disk-usage explorer for when a mount is 94% and you need to know of what, plus a composite device-reliability prediction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the boring-but-important stuff&lt;/p&gt;

&lt;p&gt;It's still self-hosted, MIT-licensed, Python + nginx (gunicorn/Flask), with JSON / SQLite / PostgreSQL backends. Every release goes through CodeQL, Bandit and gitleaks, plus a code-level security review; nothing Critical, High or Medium ships. The 6.2 review is in the repo (&lt;a href="https://github.com/tyxak/remotepower/blob/main/docs/security-review-6.2.0.md" rel="noopener noreferrer"&gt;https://github.com/tyxak/remotepower/blob/main/docs/security-review-6.2.0.md&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;If you've been running Linux boxes on it and eyeing the Windows column with suspicion — this is the release to give it another look.&lt;/p&gt;

&lt;p&gt;Repo + docs: &lt;a href="https://github.com/tyxak/remotepower" rel="noopener noreferrer"&gt;https://github.com/tyxak/remotepower&lt;/a&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>sysadmin</category>
      <category>microsoft</category>
      <category>python</category>
    </item>
    <item>
      <title>Five fixes from a stranger, and the release that finally got a community</title>
      <dc:creator>tyxak</dc:creator>
      <pubDate>Thu, 25 Jun 2026 20:14:16 +0000</pubDate>
      <link>https://dev.to/tyxak/five-fixes-from-a-stranger-and-the-release-that-finally-got-a-community-1nhi</link>
      <guid>https://dev.to/tyxak/five-fixes-from-a-stranger-and-the-release-that-finally-got-a-community-1nhi</guid>
      <description>&lt;p&gt;RemotePower started as a web page with a single button that turned a machine off. I wrote it in Python one afternoon, and then scope creep did the rest, with a lot of help from AI to speed things up (sorry, AI snobs :-)). That one button is now a self-hosted control plane for a whole Linux fleet: monitoring and alerting, a CMDB, CVE scanning, patching, browser-based SSH, Proxmox, drift detection. Underneath it's still deliberately boring — nginx, Python CGI, flat JSON files — and the agents poll out over HTTPS, so there are &lt;strong&gt;no inbound ports open on the clients, ever&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For most of its life it's been a one-person project. The architectural ideas are mine, the code is mostly written by AI, and I'd rather be upfront about that than pretend otherwise.&lt;/p&gt;

&lt;p&gt;Then, last week, something happened that I genuinely didn't expect: someone I've never met opened a pull request. Then another. By the end of the week it was &lt;strong&gt;five fixes and a really sharp bug report&lt;/strong&gt; — all from one first-time contributor, none of whom I know.&lt;/p&gt;

&lt;h2&gt;
  
  
  The release named itself
&lt;/h2&gt;

&lt;p&gt;A little habit I picked up along the way: every RemotePower release gets a codename ending in "Matters." VisualMatters, TrustMatters, FortifyMatters, OnboardingMatters, PerimeterMatters, CTRLMatters. It started as a bit of fun and stuck around, and it turned into a nice forcing function — each release gets one thing to care about most.&lt;/p&gt;

&lt;p&gt;This one named itself. The thing that mattered most about it wasn't a feature I built. It was that, for the first time, the "ours" in "the tool is yours, ours really" had someone else in it. So: &lt;strong&gt;UnityMatters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here are three of the fixes that came with it, because honestly they're the good kind of bug — the boring-looking ones that are quietly nasty.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The device record that deleted itself
&lt;/h2&gt;

&lt;p&gt;This is the bug report that made me sit up. A handful of API handlers were doing an unlocked read-modify-write of the device store: load the whole set, change one field, save the whole set back. Classic, and fine until two writes overlap.&lt;/p&gt;

&lt;p&gt;On the flat-JSON backend that's the usual lost-update — a dropped field, annoying but survivable. But on the SQL backend it's worse than that. The save reconciles the &lt;em&gt;entire&lt;/em&gt; device set and deletes any row that isn't in the payload. So picture a slow admin edit that loaded the device list a moment before a brand-new agent enrolled. When that edit saves its now-stale snapshot, the freshly-enrolled device — and its auth token — simply &lt;strong&gt;vanishes&lt;/strong&gt;. The agent's enrollment, gone, because an unrelated edit happened to be in flight.&lt;/p&gt;

&lt;p&gt;The fix is the boring, correct one: every device-write now does its read-modify-write under a single lock (&lt;code&gt;_LockedUpdate(DEVICES_FILE)&lt;/code&gt;), so the load and the save can't be split by another writer. There's a guardrail test now too, so the next person who adds a handler can't quietly reintroduce it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The backend that recompiled itself 50,000 times a day
&lt;/h2&gt;

&lt;p&gt;This one's pure "deliberately boring architecture" coming back to bite. RemotePower's backend is a single ~50,000-line Python file, run as a CGI script via fcgiwrap. Turns out CPython &lt;strong&gt;never uses the cached &lt;code&gt;.pyc&lt;/code&gt; for a script you run as the main program&lt;/strong&gt; — it recompiles the source every single time. So every request, even a health check, was paying ~0.9s to recompile fifty thousand lines before doing any work.&lt;/p&gt;

&lt;p&gt;The contributed fix is a four-line shim: a tiny entry script that &lt;em&gt;imports&lt;/em&gt; the big module (so CPython loads its cached bytecode) and runs it as &lt;code&gt;__main__&lt;/code&gt;. The big file is untouched. Per-request time went from &lt;strong&gt;~0.9s to ~0.15s&lt;/strong&gt; — about 6× — for the cost of a file that does basically nothing. My favourite kind of patch.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Proxmox, but the whole cluster this time
&lt;/h2&gt;

&lt;p&gt;The Proxmox integration only ever asked the one node it was configured against, so on a cluster you saw one node's guests and nothing else. It now lists guests cluster-wide and tags each one with its owning node, and resolves that node per guest — so start/stop/snapshot/migrate hit the right host even for a guest living somewhere else.&lt;/p&gt;

&lt;p&gt;The part I appreciated: node names come back from the cluster, and they flow into request paths. So they're validated as hostnames before they can ever reach a &lt;code&gt;/nodes/&amp;lt;node&amp;gt;/…&lt;/code&gt; URL — a hostile or misconfigured cluster can't steer an authenticated call somewhere it shouldn't go. Someone thought about the threat model, not just the happy path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm writing this down
&lt;/h2&gt;

&lt;p&gt;There's no "buy me a coffee" button on RemotePower. It's MIT licensed, and it stays that way — homelab niceties and enterprise functions, all of it, free. Instead of a tip jar there's a pull request, an issue tracker, and a security workflow. The tool is yours. Ours, really.&lt;/p&gt;

&lt;p&gt;This release is the first time that last sentence had receipts. If you run a Linux fleet or a homelab and you want to kick the tyres, it installs in about five minutes. And if you hit a bug, or have an idea, or just want to say hi — that's all very welcome. Clearly it goes somewhere now. :-)&lt;/p&gt;

&lt;p&gt;Under all the AI, there's still a friendly human being behind this.&lt;/p&gt;

&lt;p&gt;/jake&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;RemotePower is self-hosted, MIT-licensed, Linux/Windows/macOS agents, no inbound ports. Repo + install docs: &lt;a href="https://github.com/tyxak/remotepower" rel="noopener noreferrer"&gt;https://github.com/tyxak/remotepower&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>opensource</category>
      <category>showdev</category>
      <category>selfhosted</category>
      <category>devops</category>
    </item>
    <item>
      <title>RemotePower – self-hosted remote power management</title>
      <dc:creator>tyxak</dc:creator>
      <pubDate>Fri, 05 Jun 2026 21:20:24 +0000</pubDate>
      <link>https://dev.to/tyxak/remotepower-self-hosted-remote-power-management-2f5h</link>
      <guid>https://dev.to/tyxak/remotepower-self-hosted-remote-power-management-2f5h</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpuhgrspjxmsntv0b277g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpuhgrspjxmsntv0b277g.png" alt=" " width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I built RemotePower because I wanted to power devices on and off across my own infrastructure without depending on a cloud service or some vendor's phone-home dashboard. Everything I run is self-hosted, and remote power was the one annoying gap I kept working around with SSH scripts and cron hacks. So I wrote a proper tool for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The itch
&lt;/h2&gt;

&lt;p&gt;If you run your own hardware, you know the pattern: a box needs rebooting, a device needs cycling, and you're either physically there or you've cobbled together a one-off SSH script that you half-remember how to use. Every commercial answer to this wants you to route your power management through &lt;em&gt;their&lt;/em&gt; cloud, install &lt;em&gt;their&lt;/em&gt; agent that phones home to &lt;em&gt;their&lt;/em&gt; dashboard, and trust that the company still exists next year. For something as sensitive as "turn my machines on and off," that never sat right with me.&lt;/p&gt;

&lt;p&gt;I wanted something I fully own, that runs entirely on infrastructure I control, and that I can read top to bottom when something breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;RemotePower is a polling-agent setup. Clients phone home to a small server, so you don't have to expose anything inbound on the machines you're managing. The agent reaches out; the server never has to reach in. That means no inbound firewall holes on your managed boxes, which is the whole point when the thing you're controlling is power state.&lt;/p&gt;

&lt;p&gt;The flow is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An agent runs on (or alongside) each managed device.&lt;/li&gt;
&lt;li&gt;On an interval, the agent polls the server: "anything for me?"&lt;/li&gt;
&lt;li&gt;The server hands back any queued commands.&lt;/li&gt;
&lt;li&gt;The agent executes and reports back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because the agent initiates every connection, the managed machines can sit behind NAT, behind a restrictive firewall, or on a network you don't fully control, and it still works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The backend is boring on purpose
&lt;/h2&gt;

&lt;p&gt;This is the part I'm actually proud of. The backend is deliberately, aggressively boring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python CGI behind nginx&lt;/strong&gt; — each request is a short-lived process. No long-running app server to leak memory or wedge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flat JSON files for storage&lt;/strong&gt; — no database to babysit, no migrations, no connection pool, no separate service to keep alive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An HMAC token per device&lt;/strong&gt; for authentication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes it trivial to deploy on a cheap VPS or a box in your closet, and easy to reason about when something breaks. When your storage layer is &lt;code&gt;cat packages.json&lt;/code&gt;, debugging is a very different experience than chasing a query planner.&lt;/p&gt;

&lt;p&gt;A rough sense of the layout on the server side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;/&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;www&lt;/span&gt;/&lt;span class="n"&gt;remotepower&lt;/span&gt;/&lt;span class="n"&gt;cgi&lt;/span&gt;-&lt;span class="n"&gt;bin&lt;/span&gt;/   &lt;span class="c"&gt;# the CGI handlers (api.py and friends)
&lt;/span&gt;/&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;lib&lt;/span&gt;/&lt;span class="n"&gt;remotepower&lt;/span&gt;/           &lt;span class="c"&gt;# flat JSON state files
&lt;/span&gt;/&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;remotepower&lt;/span&gt;/               &lt;span class="c"&gt;# config
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is flat-file storage the "right" choice at scale? No. But for the scale most self-hosters actually operate at — a handful to a few dozen devices — it removes an entire category of operational pain, and that tradeoff has been worth it every single time so far.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent does beyond power
&lt;/h2&gt;

&lt;p&gt;Once you have an agent phoning home anyway, it's a natural place to hang a few more useful things. The Linux agent also does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Package enumeration&lt;/strong&gt; — it reads installed packages via &lt;code&gt;dpkg-query&lt;/code&gt; / &lt;code&gt;rpm&lt;/code&gt; / &lt;code&gt;pacman&lt;/code&gt; / &lt;code&gt;apk&lt;/code&gt;, so the server has an inventory of what's actually on each box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A CVE scan against &lt;a href="https://osv.dev/" rel="noopener noreferrer"&gt;OSV.dev&lt;/a&gt;&lt;/strong&gt; — it cross-references those installed packages against known vulnerabilities and surfaces findings per device, with a dashboard view and an ignore-list for the noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Prometheus &lt;code&gt;/metrics&lt;/code&gt; endpoint&lt;/strong&gt; — so if you already run Prometheus + Grafana (and if you're self-hosting, you probably do), you can graph it alongside everything else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CVE piece in particular turned a power tool into something I check more often than I expected — knowing which of my boxes are carrying a vulnerable package, without standing up a separate scanner, is genuinely handy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm posting this
&lt;/h2&gt;

&lt;p&gt;It's still very much a personal project that grew. There are rough edges. A few I already know about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The flat-JSON storage works great until it doesn't — there's no row-level locking, so very high concurrency isn't its strength (it's not trying to be).&lt;/li&gt;
&lt;li&gt;CGI-per-request is simple but not the fastest model under heavy polling.&lt;/li&gt;
&lt;li&gt;The auth model (HMAC per device) is solid for the threat model I designed for, but I'd love more eyes on it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is really why I'm writing this. I'd genuinely like to hear where the design choices look wrong to people who run more than I do. If you've operated fleets, managed power at scale, or just have strong opinions about polling-vs-push or flat-files-vs-database, I want the pushback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it / tear it apart
&lt;/h2&gt;

&lt;p&gt;The repo is here: &lt;strong&gt;&lt;a href="https://github.com/tyxak/remotepower" rel="noopener noreferrer"&gt;github.com/tyxak/remotepower&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer anything about how it's put together — the architecture decisions, the agent protocol, why CGI in 2026, any of it. And if you spot something I got wrong, that's exactly the feedback I'm here for.&lt;/p&gt;

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

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

</description>
      <category>monitoring</category>
      <category>devops</category>
      <category>security</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
