224 lines
8.6 KiB
Plaintext
224 lines
8.6 KiB
Plaintext
== 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 <adr/module-system>+`)
|
|
|
|
[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).
|
|
|