Configure your board for headless use

All Ubuntu pre-installed images, both for cloud and SBCs use cloud-init for their first boot setup, including initial user creation. It is possible to customize the first boot to configure a board for remote usage. This includes securely importing all necessary credentials, and making it possible to locate easily on the local network, removing the need to ever attach a screen or keyboard to the board.

You will need the following:

  • An Ubuntu One (Launchpad) or GitHub user account

  • An SSH public/private key-pair (see ssh-keygen(1) for more information)

  • Your SSH public key registered with Launchpad or GitHub

Cloud-init seeds

The cloud-init “seed” provides the configuration that cloud-init will act upon during the first boot. The location of the seed is image dependent. On the Ubuntu for Raspberry Pi images, it is present on the boot partition (the first FAT partition on the images). On other images, it may be present on a FAT partition labeled “CIDATA”. The location will be documented in the board’s corresponding chapter in these docs.

Wherever it resides, the cloud-init seed always consists of the following files, all in YAML format:

meta-data

While mandatory, this is largely uninteresting for our purposes here. It simply “identifies” the board and specifies whether the seed has a network-based component (this is more useful in a cloud context)

user-data

This defines the bulk of the cloud-init configuration, specifying initial user characteristics, packages to install, files to manipulate, and so on

network-config

Optional to the seed, but required in our case to ensure that cloud-init has a valid network connection from which to retrieve keys and packages

Network configuration

The network-config file contains the netplan compatible configuration for networking cards present on the board. The default network configuration typically attempts connection on available interfaces, but does not require it. This is fine for the default cloud-init configuration, which does not require network access to complete.

However, if your configuration requires network access for anything (installing packages, retrieving SSH keys from an online account, mounting remote file-systems, and so on), you will need to change the default configuration to require a connection. This ensures that cloud-init will wait until the specified connection is fully online before proceeding with the rest of its configuration.

As an example, take the default network-config on the Raspberry Pi images:

version: 2
ethernets:
  eth0:
    dhcp4: true
    optional: true

If your board has an ethernet port which will be connected on first boot, you can simply change this indicate the connection is mandatory:

version: 2
ethernets:
  eth0:
    dhcp4: true
    optional: false

On a board which has ethernet, but which will be using wifi for connectivity you might add a wifis section instead with a mandatory interface:

version: 2
ethernets:
  eth0:
    dhcp4: true
    optional: true
wifis:
  wlan0:
    dhcp4: true
    optional: false
    access-points:
      my-wifi-ssid:
        password: "my very secret wifi password"

Note that there is no specific restriction on the type of network connectivity required. Ethernet or wifi can be used for first boot configuration. That said, ethernet is typically preferred where available as the simpler and more reliable medium.

Customizing the users

On the vast majority of pre-installed Ubuntu images, the default username is “ubuntu” with a default password of “ubuntu”. Obviously this is well known and insecure. For this reason, SSH password-based authentication is disabled by default on all such images, and an initial password change is mandated on login.

All these aspects may be configured with the user-data portion of the cloud-init seed. The user and users keys control the details of the user(s) created on first boot.

user

Specifies the attributes of the “default” user, including:

name

The name of the user to create. Defaults to “ubuntu”

plain_text_passwd

The password for the user. Defaults to “ubuntu”

hashed_passwd

You can also supply a default password as a hash (see below for instructions on generating the hash)

lock_passwd

If set to true (the default), disables password based login

groups

The list of groups to add the user to

homedir

The location of the user’s home directory. Defaults to /home/name

shell

Path to the user’s login shell. Defaults to bash(1) on Ubuntu

ssh_import_id

Import SSH keys from the specified account (see below for more information)

sudo

List of strings containing sudo(8) rules for this user

users

The list of users to be created. Initially, this is just the “default” user defined by the user key above. However, additional entries using the same sub-keys as those under user may also be included in the list. The “default” entry may also be excluded to prevent its creation.

The default user configuration on Ubuntu could be expressed as follows. Note, this configuration is implicit in the cloud-init installation; it doesn’t need to be specified in your user-data, this is simply to give context for the changes below:

user:
  name: ubuntu
  plain_text_passwd: "ubuntu"
  groups: [adm, cdrom, dip, lxd, sudo]
  lock_passwd: true
  shell: /bin/bash
  sudo: ["ALL=(ALL) NOPASSWD:ALL"]
users:
  - default

If you want to rename the default user to “fred” and set a different password that isn’t locked, you can use the following in your user-data:

user:
  name: fred
  plain_text_passwd: "flintst0ne"
  lock_passwd: false

You can also specify a hash of a password:

user:
  name: fred
  hashed_passwd: "$6$rounds=500000$V0fxPRRWCnTWfCIz$dV9YdtDo5MOrOyXPMw6tuHVtV/dxc3EtRzIyl7AaZD.GZvL0nNvdG1VT4xYwvM0e/j70eYsbRpKKB5CxtpGUd1"
  lock_passwd: false

The hash can be generated with the mkpasswd(1) utility from the whois package, like so:

ubuntu@ubuntu:~$ mkpasswd --method=SHA-512 --rounds=500000
Password: # not echoed$6$rounds=500000$V0fxPRRWCnTWfCIz$dV9YdtDo5MOrOyXPMw6tuHVtV/dxc3EtRzIyl7AaZD.GZvL0nNvdG1VT4xYwvM0e/j70eYsbRpKKB5CxtpGUd1

Warning

Be aware that this is barely more secure than a plain text password. In both cases, the password or the hash will typically be world readable after the machine has booted. You are strongly recommended not to rely on this for remote first boot login. See SSH authentication below for a more secure alternative.

To define a user in addition to the default one, add it to the users key. Include “default” to ensure the user defined under user is also created:

user:
  name: fred
  hashed_passwd: "$6$rounds=500000$V0fxPRRWCnTWfCIz$dV9YdtDo5MOrOyXPMw6tuHVtV/dxc3EtRzIyl7AaZD.GZvL0nNvdG1VT4xYwvM0e/j70eYsbRpKKB5CxtpGUd1"
  lock_passwd: false
users:
  - default
  - name: barney
    hashed_passwd: "$6$rounds=500000$TEz/1c9AInDtCeCu$enA9jQEKTDHjypdMXdfTXMN5Khw./J3r0uIzHpktNjxZXw26k22mwcJ68el8GFDSR5i6unmmg/ePm.lVxkfbF0"
    groups: [lxd]
    lock_passwd: false

Alternatively, you can suppress creation of the default user (by not including “default” under users), and simply define all the users directly:

users:
  - name: fred
    hashed_passwd: "$6$rounds=500000$V0fxPRRWCnTWfCIz$dV9YdtDo5MOrOyXPMw6tuHVtV/dxc3EtRzIyl7AaZD.GZvL0nNvdG1VT4xYwvM0e/j70eYsbRpKKB5CxtpGUd1"
    groups: [adm, cdrom, dip, lxd, sudo]
    lock_passwd: false
    sudo: ["ALL=(ALL) NOPASSWD:ALL"]
  - name: barney
    hashed_passwd: "$6$rounds=500000$TEz/1c9AInDtCeCu$enA9jQEKTDHjypdMXdfTXMN5Khw./J3r0uIzHpktNjxZXw26k22mwcJ68el8GFDSR5i6unmmg/ePm.lVxkfbF0"
    groups: [lxd]
    lock_passwd: false

Note

In this case, as neither user is the “default” user, their definitions will not inherit from the cloud-init defaults. Remember to include sudo or administration rights for at least one user in this case.

SSH authentication

By default, password-based authentication for SSH is disabled because the default usernames and passwords are both well known and trivially guessable.

You can control whether SSH password-based authentication is enabled via cloud-init. You can also import SSH keys for public-key authentication from either a GitHub or Launchpad account. The following keys in user-data are used for this:

ssh_pwauth

If set to “true” (it is “false” by default), password-based authentication will be permitted for SSH

ssh_import_id

Defines the list of accounts to request SSH public keys from. May be specified at the top-level, in which case imported keys are assigned to all users created by cloud-init, or under individual user definitions, in which case the keys apply just to that user

We strongly recommend you leave SSH password-based authentication disabled. Importing SSH public keys for your user(s) from GitHub or Launchpad is a much more secure option as at no point will your machine be remotely accessible with a username / password combination defined in a world-readable file. Naturally, this requires an internet connection (see Network configuration above).

As noted, the ssh_import_id value is a list of account names. GitHub accounts are prefixed with gh: and Launchpad accounts with lp:. For example:

ssh_import_id:
  - lp:launchpad_username
  - gh:github_username

For a complete example, consider the following user-data file, which changes the default username to “fred”, the default password to “flintst0ne”, leaves SSH password-based authentication disabled (explicitly), and imports SSH keys from the GitHub user “fred_flintstone”:

user:
  name: fred
  plain_text_passwd: flintst0ne
  lock_passwd: false
ssh_pwauth: false
ssh_import_id:
  - gh:fred_flintstone

Finding your board

A common issue with headless SBCs is how to locate them on the network once they have booted. One method is to configure each with a static IP address, but this involves a certain amount of complexity to ensure all machines have a unique configuration. Another is to have a router configuration which is capable of reporting newly seen machines.

However, a popular alternative is to use mDNS. If you have avahi-daemon(8) active on your system, you can locate machines by their hostname within the .local domain. To accomplish this, you will use the following keys within user-data:

hostname

Sets the machine’s hostname to the specified value

package_update

If set to “true”, causes the local package index to updated. This matters less on Ubuntu cloud images as they are regenerated daily. However, the Ubuntu board images are static once released and thus extremely likely to have an out of date package index on first boot

package_upgrade

If set to “true”, causes the package manager to run an upgrade of installed packages during first boot. Again, this is more important on Ubuntu board images than cloud images

packages

The list of extra packages to install, once any requested update or upgrade is concluded

The following example will set the machine’s hostname to “mypi”, ensure all packages are up to date, and install “avahi-daemon”.

hostname: mypi
package_update: true
package_upgrade: true
packages:
  - avahi-daemon

Be aware this will require an internet connection on first boot (see Network configuration above).

Once cloud-init has finished, you should be able to reach your machine (with ping(1) or ssh(1)) as “mypi.local”, assuming you also have “avahi-daemon” installed on your client machine.

Optimizations

If you are booting many boards with the same release or distribution, it may be beneficial to configure a local apt cache (see apt-cacher-ng(8)). If you have such a cache, you can use it by specifying it in your user-data. Change the highlighted line to the URL for your cache:

apt:
  conf: |
    Acquire::http { Proxy "http://acng.example.com:3142"; }