Install with an AI agent

Rasputin’s install path is scriptable end to end, and this page is the contract for it — written for AI coding agents (Claude Code, Codex, and friends) and for the humans driving them. Everything here is also how you’d automate an install with plain shell, no AI involved.

If you’re a human who wants an agent to do the work, paste this into it:

Read https://rasputin.geekdojo.com/llms.txt and
https://rasputin.geekdojo.com/docs/agents/index.md, then help me install
Rasputin. Plan the flash with RASPUTIN_DRY_RUN=1 first and show me the plan;
only write to a disk after I confirm. My first node's hardware is:
<Raspberry Pi 4/5/CM5, or an Intel N100 / amd64 box>.

Or skip the prompt: this contract ships packaged as an agent skill in geekdojo/rasputin-agents.

  • Claude Code: /plugin marketplace add geekdojo/rasputin-agents, then /plugin install rasputin@geekdojo
  • Codex (or any Agent Skills client): git clone https://github.com/geekdojo/rasputin-agents && cd rasputin-agents && codex — the skill and AGENTS.md are auto-discovered

Machine-readable endpoints

URLWhat it is
/llms.txtIndex of everything on this page’s level — docs, manifests, install one-liner.
/releases.jsonLatest stable versions + image download URLs. Regenerated daily and on every release.
OS manifestAuthoritative per-artifact SHA-256 checksums, sizes, and signer for the newest stable OS release. Stable URL — no API, no auth, no HTML scraping.
Firewall manifestSame, for the firewall image.
/bootstrap.shThe flasher itself — plain bash, commented, and the source of truth for the env contract below.
/rasputin-root-ca.pemThe public Rasputin root CA — the trust anchor for release signatures, and the same cert baked into every image at /etc/rasputin/trust/root-ca.pem.
Docs as markdownEvery docs page has a raw-markdown mirror at index.md — e.g. /docs/provisioning/index.md. Skip the HTML entirely.

Pin a specific release by swapping latest/download for download/<tag> in the manifest and image URLs.

Non-interactive install

bootstrap.sh prompts a human for three things; every prompt has an env-var override, so an agent can drive it deterministically:

VariableMeaning
RASPUTIN_ARCHTarget hardware: arm64 (Raspberry Pi 4/5/CM5) or amd64 (Intel N100 / any amd64 box).
RASPUTIN_NODE_IDControl-plane node id (default cp-1). Short lowercase name: letters, digits, hyphens.
RASPUTIN_SSH_AUTHORIZED_KEYYour SSH public key line. Or set RASPUTIN_SSH_KEY_FILE to a .pub path instead.
RASPUTIN_RELEASEPin a release tag. Default: latest stable.
RASPUTIN_DISKTarget device (e.g. /dev/disk4, /dev/sdb). Skips the disk-picker prompt.
RASPUTIN_ASSUME_YES=1 skips the typed flash confirmation.
RASPUTIN_DRY_RUN=1 prints the resolved plan (disk, image URL) and stops before any write.
RASPUTIN_ALLOW_INTERNAL=1 also offers internal disks. Dangerous; leave unset.

The script verifies the image’s SHA-256 against the release manifest, refuses internal disks by default, writes the seed, and block-level reads it back. The recommended agent flow, in order:

# 1. Preflight — no writes, prove the plan to the user first.
curl -fsSL https://rasputin.geekdojo.com/bootstrap.sh | sudo \
  RASPUTIN_ARCH=arm64 RASPUTIN_NODE_ID=cp-1 \
  RASPUTIN_SSH_KEY_FILE=$HOME/.ssh/id_ed25519.pub \
  RASPUTIN_DRY_RUN=1 bash

# 2. Flash — same command, dry-run swapped for the confirmed target disk.
curl -fsSL https://rasputin.geekdojo.com/bootstrap.sh | sudo \
  RASPUTIN_ARCH=arm64 RASPUTIN_NODE_ID=cp-1 \
  RASPUTIN_SSH_KEY_FILE=$HOME/.ssh/id_ed25519.pub \
  RASPUTIN_DISK=/dev/disk4 RASPUTIN_ASSUME_YES=1 bash

Agents: always dry-run first and show the human the plan before flashing. Writing a disk image is destructive, and picking the wrong disk is the one mistake this page can’t undo for you.

Flashing by hand instead (Windows, or no script): the Download page has the manual steps, and a copy-exact seed template lives at /rasputin-seed.env.example. The full seed reference is Provisioning (markdown).

Verifying the install

After the node boots (first boot takes a few minutes — the browser refusing to connect during it is normal, not a failure):

curl -fsS http://rasputin.local/healthz
# → {"status":"ok"}

/healthz answers unauthenticated on plain HTTP as soon as the control plane is up. If rasputin.local doesn’t resolve (routine on Windows without mDNS, and behind some routers), find the host named rasputin in the router’s DHCP lease table and probe the IP directly.

Beyond the probe, the trust chain is fetchable too: the cluster’s CA certificate is at http://rasputin.local/mesh-ca.pem (unauthenticated by design — first-run trust is TOFU on your own LAN), and http://rasputin.local lands on a trust page with per-OS CA-install instructions before HTTPS sign-in.

Verifying signatures

The trust anchor is the Rasputin root CA: /rasputin-root-ca.pemCN=Rasputin Root CA, O=Geekdojo, valid to 2046, SHA-256 fingerprint 67:7E:57:06:13:87:3E:08:A3:2C:F5:F4:52:76:10:33:8D:57:5A:C4:E9:67:5C:CD:91:C4:43:FE:BD:27:C1:B9. Cross-check the fingerprint with openssl x509 -in rasputin-root-ca.pem -noout -fingerprint -sha256.

What’s signed, precisely:

  • Firewall artifacts — the -ab.img.gz initial-flash disk and the .rootfs OTA artifact each ship a detached DER CMS .sig alongside them on the release. The signature embeds the leaf and intermediate certs, so the root alone completes the chain:

    curl -fsSLO https://rasputin.geekdojo.com/rasputin-root-ca.pem
    openssl cms -verify -binary -inform DER \
      -in  rasputin-fw-n100-<version>-ab.img.gz.sig \
      -content rasputin-fw-n100-<version>-ab.img.gz \
      -CAfile rasputin-root-ca.pem -out /dev/null
    
  • OS update bundles (.raucb) are CMS-signed by RAUC; on a Linux box with rauc installed: rauc info --keyring rasputin-root-ca.pem rasputin-os-<sku>-<version>.raucb. Devices do this on every update against the baked-in root — you’re re-running their check.

  • OS flashable images (.img.xz) carry no per-image .sig; they chain to the root through the signed release manifest. manifest.json ships a detached DER CMS signature (manifest.json.sig) on the release — verify it, then check the image’s SHA-256 against the manifest’s imageSha256:

    openssl cms -verify -binary -inform DER \
      -in  manifest.json.sig \
      -content manifest.json \
      -CAfile rasputin-root-ca.pem -out /dev/null
    

    Releases cut before the manifest signature landed have no manifest.json.sig; for those, the HTTPS-fetched imageSha256 is the whole integrity story.

Where the agent must hand off

Two steps are deliberately human-only:

  • Passkey registration. Sign-in is passkey-only — Touch ID, Windows Hello, or a security key at https://rasputin.local/setup. No passwords exist, so there is nothing for an agent to type. Walk the human to the browser and wait.
  • Adding more nodes. The dashboard’s Add-node wizard mints each new node’s one-liner with an id-bound join token baked in. Don’t hand-construct compute seeds (RASPUTIN_NATS_URL, RASPUTIN_CP_JOIN_TOKEN) — run the wizard’s one-liner as given.

Known failure modes

SymptomCause and fix
Browser says the certificate is expired on a fresh nodeThe node’s clock is wrong — common on boards with no battery-backed clock (Pi 5) when NTP is broken. See Time sync.
rasputin.local never resolvesNo mDNS on the client (Windows) or the router blocks it. Use the DHCP-lease IP for host rasputin.
Seed didn’t take; first boot waits foreverThe seed must be named rasputin-seed.env, at the root of the FAT volume labeled RASPUTIN-OS — go by label, not size; the Pi image has several FAT partitions. The SSH key line must be double-quoted.
First hour took more than an hourThat’s a bug by definition. File it — blunt reports are the valuable kind.

It’s pre-alpha: image layouts and update formats still change without notice, and this contract can change with them — re-read this page rather than caching it. Something wrong or missing here blocks every agent-driven install, so docs bugs count double.


Something wrong or missing on this page? Tell us — docs bugs count too.