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:
<a Raspberry Pi, or a UEFI amd64 box — name the exact model>.

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 four things; every prompt has an env-var override, so an agent can drive it deterministically:

VariableMeaning
RASPUTIN_ARCHTarget hardware: arm64 (a Raspberry Pi) or amd64 (any UEFI amd64 box). See supported hardware.
RASPUTIN_CLUSTER_IDCluster name (default rasputin). Becomes https://<name>.local, the WebAuthn RP ID, and the <name>.internal DNS zone — fixed for the life of the installation, so set it deliberately. Short lowercase name: letters, digits, hyphens. Two clusters that both take the default collide on rasputin.local.
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. A release with no manifest.json.sig (anything before 2026-09) is refused.
RASPUTIN_MANIFEST_FILEUse an already-downloaded manifest.json instead of fetching one — the script verifies whatever it is given. Its signature is read from <path>.sig, or from RASPUTIN_MANIFEST_SIG_FILE.
RASPUTIN_ROOT_CA_FILEUse a local copy of the root CA. It must still match the fingerprint the script carries.
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 pins the root CA by the fingerprint in its own source, requires manifest.json.sig to verify against that root, requires the signer to be authorized for OS and firmware images, and only then checks the image’s SHA-256 against that verified manifest. It refuses internal disks by default, writes the seed, and block-level reads it back. Every one of those checks runs during a dry run, before anything is written, so the preflight below is a full verification. 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_CLUSTER_ID=rasputin 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_CLUSTER_ID=rasputin 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.

    That command checks the chain, not the authorization. A signature can verify against the root and still have been made by a leaf that is not allowed to sign OS or firmware images — the Rasputin PKI issues separate purposes, and the one that grants firmware signing is the extended-key-usage OID 1.3.6.1.4.1.66587.1.1.1. To check it, pull the signer out of the signature and look for that OID as a whole token (it is a prefix of future OIDs, so a plain substring match is not enough):

    openssl cms -verify -purpose any -binary -inform DER \
      -in manifest.json.sig -content manifest.json \
      -CAfile rasputin-root-ca.pem -signer signer.pem -out /dev/null
    openssl x509 -in signer.pem -noout -text | grep -A1 'Extended Key Usage' \
      | grep -E '(^|[ ,])1\.3\.6\.1\.4\.1\.66587\.1\.1\.1([ ,]|$)'
    

    bootstrap.sh does all of the above — including pinning the root CA by the fingerprint above, which it carries in its own source — so a dry run (RASPUTIN_DRY_RUN=1) is a complete verification with nothing written. Prefer it to running these by hand: one verifier that decides what gets flashed beats two that can disagree. openssl x509 -ext is not used here on purpose — macOS ships LibreSSL, which has no -ext.

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 stops and the agent never startsThe 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 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.