Repository navigation
SSH hosts, release binaries and an installer, and a live demo on GitHub Pages - #52
Merged
Merged
Conversation
A new plugin, ssh, reads the Linux machines named in plugins.ssh.hosts (an address and a user each) and shows them on a Hosts page: live CPU, memory, network and disk charts, temperatures and fans, file systems, disks, ZFS pools, containers and systemd services. A full file system, failed units, a pool that is not online and a host that went away raise attention items. An administrator adds hosts in Settings, and an Overview with nothing connected points there. dash opens one connection per host and runs one command, /bin/sh, with a constant script on its standard input. The script prints /proc, /sys and a few read-only command outputs in marked sections and sleeps; every figure is parsed and computed in dash. Only two integers and a random marker are substituted into it, so no setting reaches a shell. dash never writes to a host, never uses sudo and holds no password. - dash signs in with one ed25519 key pair, made at first need and sealed in credentials.yaml with DASH_SECRET_KEY, as the VAPID key is. Without the key or a writable data directory the plugin does not start and says so. - A host's key is pinned in hostKey. Without one dash reads the key the host presents, ends the handshake before it authenticates and shows the fingerprint; trusting it is a settings write with its audit record. A host that presents another key is refused. - plugin.Host gains Dial, the HTTP plugins' dial guard for a plugin that speaks no HTTP. A list leaf tagged dial counts for FromUI, so hosts saved from the settings page cannot reach loopback. - The topic carries names, states in fixed words, fingerprints, the inventory and the samples; never an address, a user or an error's text. An hour of samples per host stays in memory for the charts. - A host whose key waits to be trusted makes the plugin degraded, not error: that step is the administrator's, not a failure. - The demo twin publishes three made-up hosts, so the screenshots are retaken with the new rail entry and the Hosts page. - internal/jsonschema names two types of the same name by the shortest unique tail of their packages; the full import path it used before would have put the repository's owner in the OpenAPI document. ADR 0003 records the decisions, with those for the release binaries and the static demo that the next commits add. An older release starts with the new credentials.yaml and drops the key at its next write (checked against the v0.16.1 image); docs/upgrading.md says so.
dash shipped as an image and a chart only, so a server without Docker had no way to run the "one Go binary". scripts/release/build-binaries.sh cross-compiles dash for linux and darwin on amd64 and arm64 into reproducible archives with a checksums file (make dist). A binaries job builds them on every run; on a tag the release job attests them and github-release attaches them to the release. The release job needs that job, so no image is pushed for a version whose binaries do not build. site/install.sh, published with the landing page, installs the Linux binary as a systemd service: it checks the archive's SHA-256, writes /etc/dash once (the public URL, and a secret key the installer makes, since dash never generates one), installs a hardened unit with a dynamic user and the key as a credential, starts dash and prints the setup link. Run again it upgrades; --uninstall removes it. systemd-analyze security rates the unit 1.1. scripts/ci/install-smoke.sh installs, upgrades, refuses a bad checksum and uninstalls against a real systemd: in the smoke job on a hosted runner, and with --docker in a throwaway container. docs/deploy/ binary.md has the manual steps and how to verify a download. The hygiene allowlist admits the landing site's address in the files that link the installer and the demo.
Seeing dash needed a clone and a build, or a container. pages.yml now publishes the SPA itself under /demo/, with no server behind it. The SPA's two network entry points go through api/transport.ts. The demo entry replaces them with a player of a recording before any app module runs: a GET answers from a recorded file, the event stream replays 24 recorded moments of every topic in a loop, and a write answers a fixed error that says the demo is read-only. A Date shim in that entry makes the page's clock follow the recording. The production build contains none of it: check-chunks still passes, and the demo's strings are absent from web/dist. The demo is built with a relative base and hash routing, so it works at any path. web/scripts/demo-export.mjs records it from dash in demo mode at a fixed clock by crawling the real SPA: every page a visited page links, every object route the topics name, every tab, every chart range, and each "Copy context for AI" once. Two recordings are byte-identical. demo-smoke.mjs crawls the static build under a sub-path the same way and fails on a request the recording lacks. make demo does all of it. A demo job runs it on every pull request, and pages.yml runs it before it publishes, on every push to main.
The README's quick start began with a build from source. It now starts with the live demo, the demo container and the one-line install, and names SSH hosts first among the features. The landing page gets the same: a demo button in the hero, a section on adding a host over SSH, a Linux server card and the install command. Its screenshots are the retaken ones. CONTRIBUTING.md says how a change gets in and how a plugin is added. The changelog's Unreleased section holds the notes of the three changes before this one.
The notes of SSH hosts, the release binaries with their installer and the static demo move from Unreleased to v0.17.0, the first release with binaries: site/install.sh refuses v0.16.1 and older by name.
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Three things that make dash easier to reach, in one pull request because they lean on each other: the installer is how a server without Docker gets dash, SSH hosts are what a fresh install connects first, and the live demo shows both before anyone installs anything. Five commits: one per part, the docs, and the version.
1. Hosts over SSH (
sshplugin). An administrator adds a Linux machine by address and user in Settings → SSH hosts. dash signs in with its own ed25519 key, runs one fixed read-onlyshscript in one session, and shows the machine on a new Hosts page: live CPU, memory, network and disk charts, temperatures and fans, file systems, disks, ZFS pools, containers and systemd services. Nothing is installed on the host.credentials.yamlwithDASH_SECRET_KEY, like the VAPID key. dash holds no password and never writes to a host.Host.Dial: link-local is always refused, and a host saved from the settings page cannot reach loopback.2. Easy to run. Every release gets
dash_<version>_<os>_<arch>.tar.gz(Linux and macOS, amd64 and arm64), a checksums file and a provenance attestation.site/install.shinstalls the Linux binary as a hardened systemd service, creates the secret key and prints the setup link. Thereleasejob now needs the binaries, so no image ships for a version whose binaries do not build.3. Live demo on GitHub Pages.
pages.ymlbuilds the real SPA for a sub-path and publishes it under/demo/with a recording made from the demo binary. A small transport seam (web/src/api/transport.ts) lets the demo entry replacefetchandEventSourcewith a player; the production bundle contains none of the demo code.4. Docs. ADR 0003 records the decisions. The security model,
SECURITY.md, the upgrade notes, the README quick start, the landing page, a newCONTRIBUTING.mdand the changelog are updated. An Overview with nothing connected now points administrators at adding a host.Decisions worth a look
DASH_SECRET_KEYis required for SSH hosts, and dash still never generates it. The installer and the Compose example create it.authorized_keysline; dash never handles a password.## v0.17.0in the changelog. Tagv0.17.0right after merging:pages.ymlpublishes the landing page and its installer on the push tomain, and the installer has nothing to install until the release with binaries exists.motionlibrary, which used to sit in a shared chunk the entry loaded anyway (177 kB → 43 kB). Measured against a build ofmain, the JavaScript of a first load (the entry and everything it imports statically) goes from 1104.2 kB to 1106.3 kB, 343.3 kB to 344.0 kB gzipped. Nosshpage code and no demo code is in it; the two new pages are lazy chunks of 22.5 kB and 19.4 kB.How it was tested
make check: exit 0 (50 Go packages, the web suite, the production build,check-chunks).go test -raceoninternal/plugins/ssh,internal/pluginandinternal/credentials.Stop, and history across a restart. The collector script was run and parsed undersh, busyboxash(Alpine) and Debian'sdash.DASH_SECRET_KEYand a data directory: Add host → fingerprint shown and equal to the host's own → Trust → the pastedauthorized_keysline → up in 5 seconds with real inventory and charts. sshd's log shows no authentication attempt before the key was trusted. Regenerating the host's keys gavekey_changed, a refused connection and a critical item; replacing the pin in the UI brought it back. A host saved from the UI at a loopback address was refused by the dial guard. The topic held neither the address nor the user;credentials.yamlheld one sealed value and no private key in clear; the audit log had onesettings.setper change.plugins.sshsetting, and dropped thessh:section at its next write, asdocs/upgrading.mdsays.scripts/ci/install-smoke.sh --dockerin a Debian container with systemd: install, upgrade (key untouched), a wrong checksum refused, a missing archive refused, uninstall.systemd-analyze security: 1.1.shellcheckclean;actionlintandzizmorclean on the workflows.dash,LICENSEandTHIRD_PARTY_NOTICES.md.make demopasses: 133 routes crawled, 24 frames, 761 recorded GETs, 41 contexts copied, about 11 MB; two recordings are byte-identical; the smoke crawls the build under/demo/with no miss, and a write shows the read-only message. The production build contains none of the demo's strings.binariesanddemojobs and of the install smoke on a GitHub-hosted runner.maintriggers (attesting and attaching the binaries inreleaseandgithub-release, andpages.ymlpublishing the demo), and thedarwinbinaries beyond compiling.Checklist
make checkpasses.make generateand committed.