Docs ›
Written for Rasputin 2026.09.4
Provisioning & the seed file
Starter — your first node
Your first node runs the control plane (the web UI + API). Flash the OS image, then:
Plug the flashed card or drive into your computer and mount the volume labeled
RASPUTIN-OS— the small FAT seed partition. Go by the label, not size: the Pi image has several FAT partitions.Create a file named
rasputin-seed.envat its root with four lines:RASPUTIN_NODE_ROLE=controlplane RASPUTIN_CLUSTER_ID=rasputin RASPUTIN_NODE_ID=cp-1 RASPUTIN_SSH_AUTHORIZED_KEY="ssh-ed25519 AAAA… you@laptop"(Copy-exact template: rasputin-seed.env.example.)
RASPUTIN_CLUSTER_IDnames the cluster — you browse it athttps://<name>.local, and it is fixed for the life of the installation, so set it now or leave itrasputinforever.RASPUTIN_NODE_IDnames this box within that cluster — any short lowercase name works for either (cp-1,home-cp). Use your own SSH public key, and keep the double quotes — the file is read byshand the key contains spaces.Boot the node and open
http://<your-cluster>.local(http://rasputin.local if you kept the default). The first-run wizard registers a passkey and lands you on the dashboard.
That’s the whole happy path. The first control plane self-initializes against its own embedded NATS — it needs no join token, NATS URL or bus pin, just its role, an id, and your key. It makes its own bus key on first start; see The bus key and pin.
The seed file
rasputin-seed.env lives on the RASPUTIN-OS volume and is read once, on first boot, to pick the node’s role and join the fleet. Leave it blank for an un-provisioned image. A node that boots without a role stops at first boot with an error and its agent does not start; add the role and reboot, and first boot runs again. Every entry is KEY=value, one per line.
| Variable | Applies to | What it does |
|---|---|---|
RASPUTIN_NODE_ROLE | all | controlplane or compute. Required — without it, first boot stops with an error. (The firewall node runs the separate OpenWrt image, not this one.) |
RASPUTIN_SSH_AUTHORIZED_KEY | all | Your SSH public key for root, double-quoted. The image bakes no key, so this is the only way in over the network — leave it blank and SSH is unusable (the local console still works). One key line. |
RASPUTIN_CLUSTER_ID | all | Names the cluster, and with it the name you browse (https://<cluster>.local), the WebAuthn RP ID your passkeys bind to, the NATS URL nodes dial, and the <cluster>.internal DNS zone the control plane serves. Optional; blank → rasputin. Fixed at provision time — renaming a live cluster is not supported, so a new name means re-provisioning every node. Set it if a second Rasputin cluster might ever share the network: two clusters that both take the default collide on rasputin.local. |
RASPUTIN_NODE_ID | all | Names the node on the fleet. Required on every node — a seed without it stops first boot with an error, and nothing on the node makes an id up. On the control plane that is because the control plane’s identity must be stable; on any other node, because its join token is bound to the id it was issued for. The Add-Node flow and rasputin-provision assign it and bind the join token to it. It must be one DNS label: lowercase letters, digits and hyphens, not starting or ending with a hyphen, 63 characters or fewer. |
RASPUTIN_NTP_SERVER | all | Optional NTP server to pin (host or IP; double-quote a space-separated list). Only needed to force a homelab-local time server — see Time sync. |
RASPUTIN_NATS_URL | compute | The control plane’s NATS URL. The first control plane self-inits against its own embedded NATS and doesn’t need this. |
RASPUTIN_CP_JOIN_TOKEN | compute | A join token minted by the control plane. Not needed by the first control plane. |
RASPUTIN_RELEASE_CHANNEL | control plane only | stable or dev — which channel Check-for-Updates tracks. Optional; blank → stable. |
Those are all you need for an ordinary cluster. The seed accepts five more, which the
Add-Node flow or rasputin-provision sets for you where they apply, and which you would only
write by hand in the cases named below:
| Variable | Applies to | What it does |
|---|---|---|
RASPUTIN_BUS_AUTH | control plane only | enforce requires every node to present a join token on the message bus. Blank also means enforce: the control plane requires join tokens unless this is set to off, and a bus running with it off shows as a Security warning under Alerts. A matched set generated by rasputin-provision writes enforce explicitly. |
RASPUTIN_SELF_NODE_ID | control plane only | Written automatically from RASPUTIN_NODE_ID — the control plane needs to recognize itself, so it skips itself during fleet updates and defaults to being its own BMC host. Setting it by hand is not useful. |
RASPUTIN_BUS_PIN | all | The pin of the control plane’s bus key: sha256/ followed by 44 characters of base64, 51 characters in all, unquoted. The node connects to the bus over TLS and accepts only a server holding that key. Not a secret. The Add-Node flow writes it into every enrollment file and rasputin-provision into every seed, the control plane’s included. A malformed pin stops first boot with an error. Leave it out and the node connects without TLS until the control plane sends it the pin — see The bus key and pin. |
RASPUTIN_BUS_KEY | control plane only | The bus private key itself, which rasputin-provision writes into a matched set’s control-plane seed. A secret. First boot writes it to /var/lib/rasputin/bus/bus.key, never over a key already there, then blanks the line in the seed — best-effort, like the join token. A malformed key stops first boot with an error. Never put it in any other node’s seed. Leave it out and the control plane makes its own key on first start. |
RASPUTIN_BMC_HOST | compute | 1 marks this node as driving a BMC bus over its own serial port — only for a rack manager on a BitScope blade rack. It suppresses the login prompt and kernel messages on that port, because there the port is a command channel and stray output is live power traffic. Takes one extra reboot during first boot. Wrong on any other node, and not how you configure a Turing Pi — see the Turing Pi guide. |
That is the complete set. Anything else you may see in /var/lib/rasputin/node.env on a
running node is written by the node itself, not read from the seed.
The SSH key must be double-quoted: the value contains spaces and the file is sourced by sh. First boot appends it to the persistent partition (the rootfs is read-only) and runs once; to rotate or revoke a key later, edit /var/lib/rasputin/dropbear/authorized_keys on the node directly.
The bus key and pin
Every node’s agent holds a connection to the control plane’s message bus. The control plane
serves that bus over TLS with a dedicated bus key, and each node checks the server against
the key’s pin, RASPUTIN_BUS_PIN. A node that has the pin connects only over TLS, and only
to a server holding that key: a server with any other key is refused before the node sends its
join token.
You do not set this up. The Add-Node flow writes the pin into every enrollment file.
rasputin-provision makes a bus key for each matched set, writes its pin into every seed, and
writes the key itself into the control plane’s seed only. A first control plane seeded by hand,
with no key, makes its own on first start.
The bus stops accepting unencrypted connections by itself. A node without the pin — one enrolled before its cluster’s release carried the pin, or a first control plane seeded by hand — connects without TLS at first. The control plane then:
- Waits until its own running build is committed, so that no update still able to roll the control plane back is in progress. A freshly flashed control plane is already committed.
- Sends the pin to each node that is not on TLS. The node saves it and reconnects over TLS.
- Switches the bus to refuse unencrypted connections, once every enrolled node has reported a TLS connection, nothing is connected without TLS, and no task is running.
There is nothing to run and nothing to restart. At the switch, every node drops off the bus for
a moment and reconnects by itself. For that moment, starting a task is refused with an error
saying the control plane is switching its node bus to TLS-only; try again. A cluster whose seeds
all carried the pin from the start, such as a rasputin-provision matched set, has nothing to
send in step 2 and makes the switch soon after the control plane’s own agent first connects.
What it protects. Once the switch is made, the bus refuses any connection that is not TLS. And a node that holds the pin never sends its join token to a server without your cluster’s bus key.
What it does not protect. It does not identify nodes: a node still proves which node it is with its join token, not with a certificate. Until the switch is made, the bus still accepts unencrypted connections. An enrolled node that is powered off, or runs a release too old to report TLS, holds the switch back until it is back online on a current release, or removed.
What you cannot take back. The switch is one-way: the control plane never moves the bus
back to accepting unencrypted connections by itself. After it, a node whose seed has no pin
cannot connect at all, so a seed you write by hand needs the RASPUTIN_BUS_PIN line — the
enrollment files the control plane generates already carry it. A node that holds a pin refuses
a different one, so the bus key has to outlive the control plane’s hardware. It is part of the
identity backup: a control plane re-flashed without restoring that backup makes a new key,
and no node holding the old pin connects to it. See Restore a cluster.
Checking where it stands. While signed in, open
https://<cluster-id>.local/api/bus/tls in the same browser, where <cluster-id> is your
cluster’s name. It answers with a short JSON document:
modereadsoffer(waiting for the control plane’s build to be committed),migrate(sending pins), orrequire(the bus refuses unencrypted connections).- Until
modereadsrequire,blockerslists in words everything holding the next step back. pinis the pin to put in a seed you write by hand.
If the bus key did not load, the address answers with an error instead, and the bus runs without TLS. That, and a switch that could not be made, each raise a Security warning in the alert list — see Respond to an alert.
Time sync
Every node needs a correct clock. The control plane mints its HTTPS certificate against the current time, so a node whose clock is wrong serves a certificate your browser rejects as expired (or not-yet-valid). Nodes with no battery-backed real-time clock — the Raspberry Pi 5 — rely entirely on NTP for this, so if you ever see an expired-certificate warning on a freshly flashed node, the clock is the first thing to check.
You normally don’t configure anything — a node gets its time automatically, in this order of precedence:
- Your DHCP server’s NTP server (option 42), if it advertises one — used automatically.
RASPUTIN_NTP_SERVERfrom the seed, if you set it.- Built-in public fallback — anycast Cloudflare/Google IPs, used only when neither of the above is known. These are numeric, so they work even on a network with no working DNS.
Set RASPUTIN_NTP_SERVER only to pin a specific time server — for example an isolated LAN whose DHCP doesn’t advertise NTP:
RASPUTIN_NTP_SERVER="ntp.homelab.lan"
Double-quote the value if you list more than one server (space-separated), the same way you quote the SSH key.
Adding more nodes
Additional nodes need a node id and a join token minted by the running control plane — set RASPUTIN_NODE_ID, RASPUTIN_CP_JOIN_TOKEN, and RASPUTIN_NATS_URL in that node’s seed. You don’t hand-write these: the control plane’s Add-Node flow and the
rasputin-provision
matched-set CLI (it ships in the control-plane repo) generate a seed bound to each node id. The firewall is a separate x86 image with its own seed — see rasputin-openwrt-firewall.
Something wrong or missing on this page? Tell us — docs bugs count too.