Skip to content

SSH hosts, release binaries and an installer, and a live demo on GitHub Pages - #52

Merged
TechDufus merged 5 commits into
mainfrom
feat/ssh-hosts-easy-run-pages-demo
Nov 11, 2026
Merged

TechDufus merged 5 commits into
mainfrom
feat/ssh-hosts-easy-run-pages-demo

Conversation

@TechDufus

@TechDufus TechDufus commented Nov 11, 2026 •

Copy link
Copy Markdown
Owner

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 (ssh plugin). 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-only sh script 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.

  • Host keys are pinned. dash sends nothing to a host until an administrator trusts the fingerprint it shows; a changed key is refused and raises a critical attention item.
  • One sealed identity. dash's private key is sealed in credentials.yaml with DASH_SECRET_KEY, like the VAPID key. dash holds no password and never writes to a host.
  • The remote command is a constant. Only two integers and a random marker are substituted into the script; no setting reaches a shell. All parsing is Go, with fixtures.
  • The HTTP plugins' dial guard, now exposed to plugins as Host.Dial: link-local is always refused, and a host saved from the settings page cannot reach loopback.
  • Viewers see names, states, fingerprints and metrics; never addresses, users or error text.
  • Demo twin with three made-up hosts, so the screenshots and the live demo show the page.

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.sh installs the Linux binary as a hardened systemd service, creates the secret key and prints the setup link. The release job now needs the binaries, so no image ships for a version whose binaries do not build.

3. Live demo on GitHub Pages. pages.yml builds 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 replace fetch and EventSource with 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 new CONTRIBUTING.md and the changelog are updated. An Overview with nothing connected now points administrators at adding a host.

Decisions worth a look

  • Agentless over an agent. No binary or daemon on the hosts; ADR 0003 T.1 lists what that costs (Linux only, no SMART or power without root).
  • DASH_SECRET_KEY is required for SSH hosts, and dash still never generates it. The installer and the Compose example create it.
  • No password bootstrap. The administrator pastes one authorized_keys line; dash never handles a password.
  • The version is 0.17.0 (the last commit), with the notes under ## v0.17.0 in the changelog. Tag v0.17.0 right after merging: pages.yml publishes the landing page and its installer on the push to main, and the installer has nothing to install until the release with binaries exists.
  • Entry chunk. The SPA's entry chunk is 944 kB (was 808 kB) because the bundler now inlines the motion library, which used to sit in a shared chunk the entry loaded anyway (177 kB → 43 kB). Measured against a build of main, 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. No ssh page 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 -race on internal/plugins/ssh, internal/plugin and internal/credentials.
  • SSH, unit: an in-process SSH server covers the unverified scan (the server sees no authentication request), trust then up, a changed key, unauthorized then authorized, silence, an oversized frame, a non-Linux host, Stop, and history across a restart. The collector script was run and parsed under sh, busybox ash (Alpine) and Debian's dash.
  • SSH, end to end through the UI against a real OpenSSH server (an Alpine container on a private address), with DASH_SECRET_KEY and a data directory: Add host → fingerprint shown and equal to the host's own → Trust → the pasted authorized_keys line → 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 gave key_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.yaml held one sealed value and no private key in clear; the audit log had one settings.set per change.
  • Rollback: the v0.16.1 image started on that data directory, warned about the unknown plugins.ssh setting, and dropped the ssh: section at its next write, as docs/upgrading.md says.
  • Installer: scripts/ci/install-smoke.sh --docker in a Debian container with systemd: install, upgrade (key untouched), a wrong checksum refused, a missing archive refused, uninstall. systemd-analyze security: 1.1. shellcheck clean; actionlint and zizmor clean on the workflows.
  • Release binaries: two builds are byte-identical; each archive holds exactly dash, LICENSE and THIRD_PARTY_NOTICES.md.
  • Static demo: make demo passes: 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.
  • CI on this pull request: every job is green, including the first runs of the new binaries and demo jobs and of the install smoke on a GitHub-hosted runner.
  • Not run: what only a tag or main triggers (attesting and attaching the binaries in release and github-release, and pages.yml publishing the demo), and the darwin binaries beyond compiling.

Checklist

  • make check passes.
  • Generated files are regenerated with make generate and committed.
  • Commits follow conventional commits and are signed.
  • Tests cover the change; no assertion was weakened.
  • Docs are updated where behaviour, settings or the API changed.
  • No lab identifiers: fixtures and docs use example.com, the documentation IP ranges and neutral names.
  • A new plugin has a demo twin.

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.
@TechDufus
TechDufus merged commit 91ce2c4 into main Nov 11, 2026
11 checks passed

This branch was successfully deployed

2 active deployments
release — 91ce2c46 Deployed Nov 11, 2026 by TechDufus via release #123
github-pages — 91ce2c46 Deployed Nov 11, 2026 by TechDufus via deploy #9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant