Skip to content

Device Setup

What turns a freshly deployed device into a working system, and how people and tools authenticate on it afterwards. Deploying the image itself is covered in Production; everything here assumes the bus runs with authentication on (--auth, RFC-034).

The algol command on the device

Today, algol on the device is a docker exec wrapper into the running nats container. That has a consequence worth stating plainly: inside the container the CLI is root and /run/algol/nats is mounted, so D2's fallback presents the boot seed - every invocation, from any user who can run docker, acts as the boot keymaster, and algol key init has nowhere to write (the container fs is read-only). For now that is accepted: docker-group membership is root equivalence on the device anyway, and it is how the CLI has always behaved.

The direction is decided, though: the platform will move away from running in docker altogether, and once the modules run natively on the host, algol is a host binary executed by the calling user - which is when D2 does what it says and the per-user enrolment below becomes the device's reality. Nothing in the build blocks that move: /app/algold is fully static (CGO_ENABLED=0).

Until then, the walk-through below describes the target state, and a device-local algol call is simply the boot keymaster whoever runs it. The per-user keys are still worth creating today - the nats CLI is a host binary and does present them - which takes one detour around the wrapper, below.

What the first boot does by itself

No manual step is needed for the modules:

  • Every module generates its own NKey on first start (--nats-key-dir, conventionally /var/algol/<id>/.nats), and its --module name binds to that key on its first connection. The bindings persist across boots in the nats app's registry (--registry-file, default /var/algol/nats/registry.json).
  • The nats app writes the boot's root key seed to /run/algol/nats/root.nkey (mode 0600, readable by host root only). It has role keymaster and is regenerated on every nats start - it is the bootstrap credential for the enrolments below, nothing to keep or copy.

After the first boot the modules are running and talking to each other. No user key is enrolled yet: everything a human does runs on the boot seed (which is all today's wrapper offers anyway - see above).

Provisioning a device

Everything a fresh device needs beyond the modules is one manifest import and one secret set per secret. The import is a deployment act - it persists, so nothing has to be committed afterwards - and it needs no key enrolment: under the wrapper every caller is already the boot keymaster (above).

algol manifest import ret-site.json
algol secret set webui_admin --bcrypt -      # the password, hashed here, never sent as text
algol secret ls                              # every declaration the manifest carried, and which still need a value

A manifest carries declarations, never values (RFC-001), so a config that references one sits in WAIT until the value is set. That is the whole manual step: algol secret ls is the list of what is still missing, and algol config ls shows what is waiting on it.

Authoring the manifest

A manifest is captured from a device that already runs what it should carry, so the authoring is ordinary configuration on a reference device followed by an export.

# the files a RET needs: the board-controller script and its two firmware zips
algol file put bcapp   --data - --cache high --adopt-digest --content-type text/x-shellscript < bcapp.sh
algol file put fw_v390 --data - --cache high --adopt-digest --content-type application/zip < ret_bbc_app_v3.9.0.zip
algol file put fw_v381 --data - --cache high --adopt-digest --content-type application/zip < ret_bbc_app_v3.8.1.zip

# the unit that runs it, binding those three by reference (app/board-controller/README.md)
algol config update sys.systemdscript.bcapp < bcapp-script.toml

# the first web user: a declaration plus the config that references it
algol secret set webui_admin --kind bcrypt --consumer webui.webuser --bcrypt -
algol config update webui.webuser.admin < admin.toml

# the cloud gateway's credentials, piped because the wrapper sees no host path.
# An upgraded device already names cfg.secret.gate_<componentId> and waits for it;
# use that id instead, or point the config elsewhere.
sudo cat /var/algol/secrets/ngs.creds \
  | algol secret set gate_parametric_rar4000 --kind bytes --consumer gate.gate -

algol config commit '*'
algol manifest export > ret-site.json

--data - reads the content from stdin, which is the form to use on a device: the wrapper runs the CLI inside a container that sees no host path, and a pipe crosses that boundary with nothing staged. See Files for the other sources and for the 64 KiB inline cap, which is what decides whether an artifact travels in the manifest or as a url the device downloads.

Two things do not travel: the secret values, which is the point of the manual step above, and anything larger than the cap. Per-target manifests differ by what the hardware needs - a RAR carries no bcapp files at all, because the board-controller script drives a bootloader handshake that only the RET's board has (board-controller).

Creating a key under the docker wrapper

algol key init writes into the container's root home, which is read-only, so under the wrapper it cannot create a key for a host user - and algol key show answers from the same place, reporting the boot seed rather than the caller's key. The way around it is to run the same image as a one-shot container of your own: as the calling user, with only that user's key directory mounted, so the seed is generated in its final place and never exists anywhere else (RFC-034 D1).

# The image the bus is running, by id, so the CLI matches the running platform.
algol_image() {
  docker inspect --format '{{.Image}}' \
    "$(docker ps -q --filter label=com.docker.compose.service=nats)"
}

# This user's public key, once the key exists. stdout is the key alone.
algol_key() {
  docker run --rm --user "$(id -u):$(id -g)" -v ~/.algol:/keys "$(algol_image)" \
    cli --key-file /keys/nats/identity.nkey key show
}

Three things about that form:

  • Create ~/.algol yourself first (the blocks below do). A bind mount whose source is missing is created by docker, owned by root, and the container then cannot write into it.
  • --dir in the key init calls below is what lets them work without a passwd entry. Given no directory the CLI looks the caller's home up by uid, and the image knows nothing about the host's users.
  • key init prints the path and the key, both on stdout, and the path it prints is the container's. key show is the one that splits them - key on stdout, resolved path on stderr - which is why algol_key is what a command substitution calls.

Enrolling stays the wrapper's job, and needs no sudo: inside the container every caller is already the boot keymaster.

The torizon user

Once per device - the enrolment persists in the registry:

algol key init
sudo algol nats enrol --key $(algol key show) --label torizon@<device> --role keymaster
  • algol key init runs as the user, never under sudo. It creates ~/.algol/nats/identity.nkey (0700/0600) and refuses to replace an existing key.
  • The enrol runs under sudo because only root can read the boot seed: as root, algol falls back to /run/algol/nats/root.nkey, the boot's keymaster. $(algol key show) expands before sudo applies, so the key that is enrolled is the torizon user's.
  • Role keymaster may additionally use the algol nats verbs (ls, enrol, revoke); role superuser covers everything else (config, secrets, data). A caller grants a role at most at its own.

Under today's wrapper the first line cannot run and the second needs no sudo, so the device form is:

install -d -m 700 ~/.algol
docker run --rm --user "$(id -u):$(id -g)" -v ~/.algol:/keys "$(algol_image)" \
  cli key init --dir /keys/nats
algol nats enrol --key "$(algol_key)" --label "torizon@$(hostname)" --role keymaster
algol nats ls        # the key is now listed under user keys

From then on the torizon user has a key of its own: the nats CLI presents it today, and every algol command will once the CLI runs on the host (CLI - bus identity).

The root user

Root needs no setup and no key of its own: with none, algol as root falls back to the boot seed and is keymaster, and the nats CLI presents that same file (below). Two rules:

  • Never run algol key init under sudo or as root. A key at /root/.algol/nats/identity.nkey outranks the boot seed in the resolution order and is enrolled nowhere, so every root command fails with a 401 (whose message hints at --key-file /run/algol/nats/root.nkey). Deleting the stray key restores the fallback.
  • Do not enrol or copy the boot seed itself; it changes on every nats restart. Enrol per-user keys instead.

Other machines

A laptop follows the torizon user's pattern, one key per machine: algol key init locally, then enrol its public key in a shell on the device (as root, or as an already enrolled keymaster). The bus port is bound to loopback, so reach it through a tunnel:

ssh -L 4222:localhost:4222 torizon@<device>
algol config ls        # --nats-url defaults to nats://localhost:4222

Using the nats CLI directly

The plain nats binary (natscli) works against the enforcing server with one flag: the key (--nkey takes the seed file as it is on disk). An enrolled user key - root's, or a key enrolled as superuser or keymaster - may subscribe every reply inbox (RFC-034 D8 rule 1), so request/reply and the JetStream API work on natscli's default _INBOX., and nats sub '_INBOX.>' shows the replies between modules next to their requests on local.>. The one request that still needs the user-key inbox prefix is a secret get, which is answered to a platform-shaped reply subject only (rule 4); algol secret get does the same without the flag. A user with a key of its own can save both in a context once and make it the default. The public key comes from algol_key, never from algol key show (see below), and through a variable so that a failure to read it is visible rather than saved:

KEY=$(algol_key)
nats context save algol \
  --server nats://localhost:4222 \
  --nkey ~/.algol/nats/identity.nkey \
  --inbox-prefix "_INBOX.userkey.$KEY" \
  --select

nats context save prints what it stored and ends in Connection: OK once the key is enrolled; after that a bare nats sub 'local.>', nats stream ls or nats micro ls works, and with the prefix in the context so does a secret get.

Root saves no context and needs no key of its own: it presents the boot seed, which is new on every nats start. A function in root's ~/.bashrc makes the seed an everyday identity rather than an ad-hoc one; for a secret get, add --inbox-prefix "_INBOX.userkey.$(algol key show)" to the call, recomputed there because root's public key changes with the seed:

natsroot() {
  /home/torizon/bin/nats --nkey /run/algol/nats/root.nkey "$@"
}

natsroot stream ls
natsroot sub '_INBOX.>'

nats is named by path there because it is installed in the torizon user's ~/bin, which is not on root's PATH. Worth knowing:

  • algol key show is the boot seed's key, which is right here and wrong in a saved context. The wrapper answers from inside the container, where the resolution finds no user key and falls back to that same seed - so it is exactly the key natsroot presents, and exactly not the torizon user's. A context saved from it is pinned to a key that is gone after the next nats restart.
  • What the key gets is its role's template: local.> both ways, the JetStream API, $SRV.>, the secret verbs, every reply inbox; the auth verbs only for role keymaster. Modules and configured roles read their own inbox alone - that confinement is what the secret replies rest on, and an enrolled user key is exempt from it only because it may get every secret anyway.
  • For a configured role (RFC-035 D7) the prefix is not optional for anything that waits for a reply, which is most of natscli: req, stream ls, micro ls, every JetStream call. Only fire-and-forget pub and a plain sub on local.> work without it. The failure is a subscribe denial the server reports asynchronously and a request timeout - so with a context already saved, dropping the flag looks like it still works, because the context is still supplying it.
  • A secret is the one verb that needs the prefix from an enrolled user key, and the one that says so. The configurer refuses a get or a set whose reply subject is not one of the two confined shapes, with 401 reply subject is not a confined inbox, before it reads anything - the prefix is how the answer is released there, not merely how you hear it.
  • Never set --connection-name to a module id: a module name is claimed by connection name, and on a fresh registry an unenrolled key would bind the name to itself.

Reset

Deleting the registry file and restarting the nats service reopens every module name for first-connect binding and drops every enrolled user key (RFC-034 example F). Individual corrections do not need it: algol nats enrol --module <id> --key <U...> rebinds one module (for example after its volume was wiped), enrol --key re-keys a person, and revoke closes either.

A reset leaves the key files in the users' homes alone - it is only the enrolments that go - so recovering the people is one enrol --key per user against the key each file already holds, and no nats context has to be saved again.