Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The account, sudo, and SSH keys

Every image carries one account, debian. Four settings decide who can use it, what it costs them to become root, and what hardware they can reach. All four are config, resolved before the build.

The knobs

fieldlayerdefaultwhat it sets
sudobase.tomlpassword/etc/sudoers.d/debian — whether sudo prompts
first_boot_password_lengthbase.toml12length of the generated per-image password
ssh_authorized_keysbase.tomlnone~debian/.ssh/authorized_keys
groupsbase.toml + the hardware layersvideo, renderthe account’s supplementary groups

Each is overridable in a recipe. resolve and doctor additionally take --sudo and --password-length, so you can see what a choice resolves to before writing it down:

boot2deb resolve turing-rk1/forky --sudo password --password-length 16

build takes none of them. An image’s access rules come from the config its lock was resolved against, so changing them means changing base.toml or the recipe. resolve prints the recipe to write, with the keys already filled in.

ssh_authorized_keys and groups have no flag at all, deliberately — see below.

Groups

groups is what the account can reach in hardware. The SoC layers’ udev rules set GROUP= on the V4L2 codecs, the DRM render nodes, the DMA-BUF heaps and the sound cards. A group absent from this list is a device the operator opens as root or not at all. resolve prints the resolved set:

groups       : video, render, audio

It is the one part of the account axis the hardware layers contribute to. A group is a capability the silicon has, rather than a policy about its operator. The resolved set is the union of base.toml, the SoC, the boot method and the device, in that order and de-duplicated. So audio sits on socs/rk3576.toml and socs/rk3288.toml, next to the alsa-utils that is the reason it is wanted. Every board on those SoCs inherits it without restating it.

Why the group and not logind’s uaccess ACL? That ACL reaches only a seated session, so it covers someone at the board’s own keyboard and not one reached over SSH. On a headless board the group is the whole story. Without it aplay -l reports no soundcards to debian, and works only under sudo.

A recipe replaces the resolved set rather than adding to it, so name the whole list:

groups = ["video", "render", "audio"]   # omit -> the layers' union

Replacing is what lets a recipe withhold access the layers would grant. groups = [] leaves the account in its login group and nothing else. Every name must already exist on the target. One no installed package creates fails the build rather than being created, since that is always a typo.

The password

Each built image gets its own randomly generated password for debian, printed on the build’s last lines and stored in the .provenance.toml beside the image. It is expired, so the first login has to replace it.

first-boot pw : 7kQmR3xLpAvB  (user debian, expired — change at first login)

Three facts about the base image decide how much that password is guarding. openssh-server is installed and enabled, and a DHCP client brings the board onto the network before anyone has logged in. The account reaches root through sudo, whose prompt asks for that same password. The printed password is therefore root, on whatever network the board is plugged into, from the moment it powers on.

Expiry does not change that. A login against an expired account is permitted and is then required to set the new password. That protects against a credential nobody ever rotates. It does nothing against someone reaching the board before its owner does, since whoever logs in first chooses the new password and keeps the account. Length is what covers that window, which is why it is a validated range rather than a preference.

Choosing a length

The alphabet is 56 symbols: mixed case and digits, with 0/O/o and 1/l/I removed so the value transcribes cleanly at a console. Each character is therefore about 5.8 bits. The accepted range is 8 to 64, and the default is 12.

There are two different attacks, and they have very different reach:

  • Guessing at the login. Bounded by what an sshd on one of these boards will service — tens of attempts per second, not thousands. Even 8 characters (~46 bits) is far out of reach here. This is why 8 is the floor rather than a recommendation.
  • Attacking the hash offline. /etc/shadow travels inside the image file, and the password appears in the .provenance.toml beside it. Anyone holding a copy of either can attack the hash with no rate limit and no board involved. Against that, 8 characters is merely expensive, and 12 (~70 bits) is out of reach.

The length that matters is the one for an image you might copy, publish, or hand to someone else. That case is what 12 is chosen for. Shortening to 8 suits an image one operator flashes and boots directly, where the hash never leaves the build host.

Before shortening it: the friction here is typing a password at a console, and an authorized key removes that friction entirely without giving anything up. Reach for a key first.

Authorizing an SSH key

List the public key — the .pub file’s contents — in ssh_authorized_keys:

ssh_authorized_keys = [
    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBl5Nn9... operator@workstation",
]

With a key in place you never type the generated password. ssh debian@<board> works on the first boot. The password stays as the console fallback for the day the key is on a laptop you do not have.

The entries are key material, not paths. A path would resolve differently on another machine, and the point of writing a key down is that every build of that recipe carries it. Public keys are not secret, so they belong in config alongside everything else that describes the image.

Three places to put them, depending on scope:

  • A recipe — this build point authorizes these keys.
  • A base.toml overlay — your keys on every image you build, without editing the shipped tree. See Overlays.
  • The shipped base.toml — only if the config root is your own fork.

A fourth is per unit rather than per image. boot2deb press --ssh-key writes the key into the pressed file’s seed partition, and the device appends it to authorized_keys at first boot. That is how six boards from one image get six different keys, or the same key without rebuilding. See Producing images.

A recipe’s list replaces the base list rather than adding to it. A recipe for an image you intend to hand to someone else can therefore authorize nobody, with ssh_authorized_keys = [].

What is checked, and what is not

Each entry is validated at resolve. sshd reports a line it cannot parse only in its own log, on a board that need not have a console. The failure to avoid is an image whose key silently does not work.

An entry must be one line holding a key type, a base64 blob, and an optional comment. The blob is decoded far enough to confirm its own embedded type name agrees with the declared one. That catches the common paste accidents: a key wrapped by a mail client, a truncated copy, or a blob under the wrong type name.

Two rejections are worth knowing about:

  • Private key material is refused by name. A -----BEGIN OPENSSH PRIVATE KEY----- block here means id_ed25519 was reached for instead of id_ed25519.pub. The consequence would be a private key baked into every copy of the image. Authorize the .pub.
  • Options prefixes (restrict, command="…", from="…") are refused. sshd accepts them, but their syntax is quoted and comma-separated. A builder that half understood it would write a weaker restriction than the author wrote. Write a bare key, and add options on the running board if you need them.

ssh-dss is not an accepted type: OpenSSH removed DSA support, so such a line could never authenticate anything.

The file is written with mode 0600 inside a 0700 ~/.ssh, both owned by debian. That is what sshd’s default StrictModes requires before it will read a key at all.

Choosing a sudo policy

sudo = "password" prompts for the account’s own password. sudo = "nopasswd" gives debian root with no prompt.

password is the default, and root then costs one more proof than a login. An open session left at the console is not root on its own. Neither is a login with an authorized key, or a process running as debian.

nopasswd suits a single-operator board that runs nothing reachable from beyond a trusted network. It also suits an unattended first-boot setup script, which has to reach root without a password embedded in it. The tradeoff is narrow but real. Under nopasswd, anything that can log in is root, so the password and the keys above are the whole boundary.

It is also one line to change on a running board, so this is a default rather than a commitment:

sudo sh -c 'echo "debian ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/debian'

Note that passwd root does not change it. The rule belongs to debian, not to root. Root ships locked, and Debian’s convention is to leave it that way and reach root through sudo.

What the provenance records

The .provenance.toml beside each image carries the full access picture in [credentials] — the generated password, the sudo policy, and every authorized key:

[credentials]
user = "debian"
password = "7kQmR3xLpAvB"
note = "expired at first login (passwd -e); unique per built image"
sudo = "password"
authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBl5Nn9... operator@workstation"]

That file holds a live credential, so treat it as sensitive. The image’s own /etc/boot2deb/image.toml deliberately carries none of this. It is readable by anyone holding the disk. An image that inventoried its own access rules would hand a reader the list of what to go after.

Because the password is fresh per build, it is also the one thing that puts a built image’s /etc/shadow outside the byte-reproducibility claim. The keys and the sudo policy are ordinary resolved config, so they are part of what a rebuild reproduces. They are part of the rootfs cache key too. Adding a key or tightening sudo therefore rebuilds the rootfs, rather than reusing the tree that had the old rules.