Docs ›
Written for Rasputin 2026.08.4
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 andAGENTS.mdare auto-discovered
Machine-readable endpoints
| URL | What it is |
|---|---|
| /llms.txt | Index of everything on this page’s level — docs, manifests, install one-liner. |
| /releases.json | Latest stable versions + image download URLs. Regenerated daily and on every release. |
| OS manifest | Authoritative per-artifact SHA-256 checksums, sizes, and signer for the newest stable OS release. Stable URL — no API, no auth, no HTML scraping. |
| Firewall manifest | Same, for the firewall image. |
| /bootstrap.sh | The flasher itself — plain bash, commented, and the source of truth for the env contract below. |
| /rasputin-root-ca.pem | The 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 markdown | Every 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:
| Variable | Meaning |
|---|---|
RASPUTIN_ARCH | Target hardware: arm64 (a Raspberry Pi) or amd64 (any UEFI amd64 box). See supported hardware. |
RASPUTIN_CLUSTER_ID | Cluster 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_ID | Control-plane node id (default cp-1). Short lowercase name: letters, digits, hyphens. |
RASPUTIN_SSH_AUTHORIZED_KEY | Your SSH public key line. Or set RASPUTIN_SSH_KEY_FILE to a .pub path instead. |
RASPUTIN_RELEASE | Pin a release tag. Default: latest stable. A release with no manifest.json.sig (anything before 2026-09) is refused. |
RASPUTIN_MANIFEST_FILE | Use 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_FILE | Use a local copy of the root CA. It must still match the fingerprint the script carries. |
RASPUTIN_DISK | Target 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.pem — CN=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.gzinitial-flash disk and the.rootfsOTA artifact each ship a detached DER CMS.sigalongside 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/nullOS update bundles (
.raucb) are CMS-signed by RAUC; on a Linux box withraucinstalled: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.jsonships a detached DER CMS signature (manifest.json.sig) on the release — verify it, then check the image’s SHA-256 against the manifest’simageSha256:openssl cms -verify -binary -inform DER \ -in manifest.json.sig \ -content manifest.json \ -CAfile rasputin-root-ca.pem -out /dev/nullReleases cut before the manifest signature landed have no
manifest.json.sig; for those, the HTTPS-fetchedimageSha256is 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.shdoes 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 -extis 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
| Symptom | Cause and fix |
|---|---|
| Browser says the certificate is expired on a fresh node | The 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 resolves | No 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 starts | The 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 hour | That’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.