Provisioning & the seed file

Starter — your first node

Your first node runs the control plane (the web UI + API). Flash the OS image, then:

  1. 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.

  2. Create a file named rasputin-seed.env at 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_ID names the cluster — you browse it at https://<name>.local, and it is fixed for the life of the installation, so set it now or leave it rasputin forever. RASPUTIN_NODE_ID names 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 by sh and the key contains spaces.

  3. 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.

VariableApplies toWhat it does
RASPUTIN_NODE_ROLEallcontrolplane 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_KEYallYour 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_IDallNames 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_IDallNames 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_SERVERallOptional 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_URLcomputeThe control plane’s NATS URL. The first control plane self-inits against its own embedded NATS and doesn’t need this.
RASPUTIN_CP_JOIN_TOKENcomputeA join token minted by the control plane. Not needed by the first control plane.
RASPUTIN_RELEASE_CHANNELcontrol plane onlystable 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:

VariableApplies toWhat it does
RASPUTIN_BUS_AUTHcontrol plane onlyenforce 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_IDcontrol plane onlyWritten 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_PINallThe 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_KEYcontrol plane onlyThe 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_HOSTcompute1 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:

  1. 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.
  2. Sends the pin to each node that is not on TLS. The node saves it and reconnects over TLS.
  3. 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:

  • mode reads offer (waiting for the control plane’s build to be committed), migrate (sending pins), or require (the bus refuses unencrypted connections).
  • Until mode reads require, blockers lists in words everything holding the next step back.
  • pin is 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:

  1. Your DHCP server’s NTP server (option 42), if it advertises one — used automatically.
  2. RASPUTIN_NTP_SERVER from the seed, if you set it.
  3. 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.