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--modulename 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 rolekeymasterand 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
~/.algolyourself 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. --dirin thekey initcalls 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 initprints the path and the key, both on stdout, and the path it prints is the container's.key showis the one that splits them - key on stdout, resolved path on stderr - which is whyalgol_keyis 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 initruns 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,
algolfalls 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
keymastermay additionally use thealgol natsverbs (ls,enrol,revoke); rolesuperusercovers 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 initunder sudo or as root. A key at/root/.algol/nats/identity.nkeyoutranks 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 showis 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 keynatsrootpresents, 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; theauthverbs 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 maygetevery 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-forgetpuband a plainsubonlocal.>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
getor asetwhose reply subject is not one of the two confined shapes, with401 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-nameto 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.