== Configuration There are many things you can specify in a configuration, but most commonly you need to change: * which services (processes) to run * what packages to install * permitted users and groups * Linux kernel configuration options * Busybox applets * filesystem layout === Modules *Modules* are a means of abstraction which allow "bundling" of configuration options related to a common purpose or theme. For example, the `+dnsmasq+` module defines a template for a dnsmasq service, ensures that the dnsmasq package is installed, and provides a dnsmasq user and group for the service to run as. The `+ppp+` module defines a service template and also enables various PPP-related kernel configuration. Not all modules are included in the configuration by default, because that would mean that the kernel (and the Busybox binary providing common CLI tools) was compiled with many unnecessary bells and whistles and therefore be bigger than needed. (This is not purely an academic concern if your device has little flash storage). Therefore, specifying a service is usually a two-step process. For example, to add an NTP service you first add `+modules/ntp+` to your `+imports+` list, then you create a service by calling `+config.system.service.ntp.build { .... }+` with the appropriate service-dependent configuration parameters. [source,nix] ---- let svc = config.system.service; in { # ... imports = [ ./modules/ntp # .... ]; config.services.ntp = svc.ntp.build { pools = { "pool.ntp.org" = ["iburst"]; }; makestep = { threshold = 1.0; limit = 3; }; }; ---- Merely including the module won't define the service on its own: it only creates the template in `+config.system.service.foo+` and you have to create an actual service using the template. This is an intentional choice to allow the creation of multiple differently-configured services based on the same template - perhaps e.g. when you have multiple networks (VPNs etc) in different trust domains, or you want to run two SSH daemons on different ports. (For the background to this, please refer to the `+architecture decision record +`) [TIP] ==== Liminix modules should be quite familiar (but also different) if you already know how to use NixOS modules. We use the NixOS module infrastructure code, meaning that you should recognise the syntax, the type system, the rules for combining configuration values from different sources. We don't use the NixOS modules themselves, because the underlying system is not similar enough for them to work. ==== [[configuration-services]] === Services In Liminix a service is any kind of long-running task or process on the system, that is managed (started, stopped, and monitored) by a service supervisor. A typical SOHO router might have services to * answer DHCP and DNS requests from the LAN * provide a wireless access point * connect using PPPoE or L2TP to an upstream network * start/stop the firewall * enable/disable IP packet forwarding * mount filesystems (Some of these might not be considered services using other definitions of the term: for example, this L2TP process would be a "client" in the client/server classification; and enabling packet forwarding doesn't require any long-lived process - just a setting to be toggled. However, there is value in being able to use the same abstractions for all the things to manage them and specify their dependency relationships - so in Liminix "everything is a service") The service supervision system enables service health monitoring, restart of unhealthy services, and failover to "backup" services when a primary service fails or its dependencies are unavailable. The intention is that you have a framework in which you can specify policy requirements like "ethernet wan dhcp-client should be restarted if it crashes, but if it can't start because the hardware link is down, then 4G ppp service should be started instead". Any attribute in [.title-ref]#config.services# will become part of the default set of services that s6-rc will try to bring up. Services are usually started at boot time, but *controlled services* are those that are required only in particular contexts. For example, a service to mount a USB backup drive should run only when the drive is attached to the system. Liminix currently implements three kinds of controlled service: * "uevent-rule" service controllers use sysfs/uevent to identify when particular hardware devices are present, and start/stop a controlled service appropriately. * the "round-robin" service controller is used for service failover: it allows you to specify a list of services and runs each of them in turn until it exits, then runs the next. * the "health-check" service wraps another service, and runs a "health check" command at regular intervals. When the health check fails, indicating that the wrapped service is not working, it is terminated and allowed to restart. === Runtime secrets (external vault) Secrets (such as wifi passphrases, PPP username/password, SSH keys, etc) that you provide as literal values in `+configuration.nix+` are processed into into config files and scripts at build time, and eventually end up in various files in the (world-readable) `+/nix/store+` before being baked into a flashable image. To change a secret - whether due to a compromise, or just as part of to a routine key rotation - you need to rebuild the configuration and potentially reflash the affected devices. To avoid this, you may instead use a "secrets service", which is a mechanism for your device to fetch secrets from a source external to the Nix store, and create at runtime the configuration files and scripts that start the services which require them. Not every possible parameter to every possible service is configurable using a secrets service. Parameters which can be configured this way are those with the type `+liminix.lib.types.replacable+`. At the time this document was written, these include: * ppp (pppoe and l2tp): `+username+`, `+password+` * ssh: `+authorizedKeys+` * hostapd: all parameters (most likely to be useful for `+wpa_passphrase+`) To use a runtime secret for any of these parameters: * create a secrets service to specify the source of truth for secrets * use the `+outputRef+` function in the service parameter to specify the secrets service and path For example, given you had an HTTPS server hosting a JSON file with the structure [source,json] ---- "ssh": { "authorizedKeys": { "root": [ "ssh-rsa ....", "ssh-rsa ....", ... ] "guest": [ "ssh-rsa ....", "ssh-rsa ....", ... ] } } ---- you could use a `+configuration.nix+` fragment something like this to make those keys visible to ssh: [source,nix] ---- services.secrets = svc.secrets.outboard.build { name = "secret-service"; url = "http://10.0.0.1/secrets.json"; username = "secrets"; password = "liminix"; interval = 30; # minutes dependencies = [ config.services.lan ]; }; services.sshd = svc.ssh.build { authorizedKeys = outputRef config.services.secrets "ssh/authorizedKeys"; }; ---- There are presently two implementations of a secrets service: ===== Outboard secrets (HTTPS) This service expects a URL to a JSON file containing all the secrets. You may specify a username and password along with the URL, which are used if the file is password-protected (HTTP Basic authentication). Note that this is not a protection against a malicious local user: the username and password are normal build-time parameters so will be readable in the Nix store. This is a mitigation against the URL being accidentally discovered due to e.g. a log file or error message on the server leaking. ===== Tang secrets (encrypted local file) Aternatively, secrets may be stored locally on the device, in a file that has been encrypted using https://github.com/latchset/tang[Tang]. ____ Tang is a server for binding data to network presence. This sounds fancy, but the concept is simple. You have some data, but you only want it to be available when the system containing the data is on a certain, usually secure, network. ____ [source,nix] ---- services.secrets = svc.secrets.tang.build { name = "secret-service"; path = "/run/mnt/usbstick/secrets.json.jwe"; interval = 30; # minutes dependencies = [ config.services.mount-usbstick ]; }; ---- The encryption uses the same scheme/algorithm as https://github.com/latchset/clevis[Clevis] : you may use the https://github.com/latchset/clevis?tab=readme-ov-file#pin-tang[Clevis instructions] to encrypt the file on another host and then copy it to your Liminix device, or you can use `+tangc encrypt+` to encrypt directly on the device. (That latter approach may pose a chicken/egg problem if the device needs secrets to boot up and run the services you are relying on in order to login).