diary @ telent

Secret Service#

Mon 26 Aug 2024 09:05:39 AM UTC

Topics: liminix

What I have lately been working on in Liminix is adding the ability to have it use an external source for secrets instead of baking them into the image.

Up until now, the secrets needed by various services (such as wifi passphrases, PPP username/password, ssh keys, etc) are provided as literal values in the configuration.nix (or some other file imported by it) and woven into config files/scripts at buil time. This has consequences:

As of now

Currently we have this: last night I added an SSH public key to a JSON file on my development machine, and five minutes later - without my having to do anything else - it was installed as an authorized key for the root user on my Liminix test device.

So, how does it work?

Services, of course. (always_has_been_meme.gif)

Services in Liminix can have outputs. This is a convention we adopted for "discovery" services which provide data that other dependent services might need. For example, PPP and DHCP services discover information like IP addresses, peer addresses, DNS information etc, Other services might use this information for example to create a route to the peer, or to configure upstreams in a stub DNS resolver. Outputs are stored in an ephemeral filesystem (/run/services/outputs) where they can be read using shell in the startup script of another service, or in Fennel with the anoia.svc module that notifies whenever an output changes.

It turns out that (this might have been anticipated a while ago) this is exactly what we need for retrieving external secrets. We write a service that periodically fetches a JSON file-of-all-the-secrets - such as you could export from SOPS, for example - using HTTPS, and writes all the values therein as outputs. Dependent services refer to those outputs.

Job done? There is a little more to it...

First, the dependent services need to be able to actually use that information. We're not storing the entire hostapd.conf in a JSON file, just the wpa passphrase, so the service that starts hostapd first needs to create a suitable configuration file with the secrets embedded in it. To help with this there's output-template which (rather like ERB in Ruby) reads an input file and evaluates delimited sections of Lua code embedded within it. You can see how we use it in the ppp service to make a ppp-options file before starting pppd.

(Some services were easier to adapt than others. pppd took about ten minutes, but for sshd I wrote/adapted a source code patch for dropbear to make it look for its authorized_keys elsewhere than $HOME/.ssh/authorized_keys - because /home is immutable in Liminix and I thought it was cleaner and more robust than a symlink farm.)

Second, when the secrets change then we need to rewrite the config file and (in some/many cases) restart the process. You can see an example of this in the hostapd service - it wraps the actual hostapd in a subscriber service instance that watches the secret service. The subscriber is implemented with watch-outputs, a Fennel script using anoia.svc to get notified whenever the watched service changes.

What's next?

It's not entirely finished, but there's enough of it to post about. Still to do:

In other news

Liminix is broken under Nixpkgs unstable right now, due to changes in how the Lua packaging works. Yes, I plan to fix that. No, I haven't fixed it yet.

(I generally develop Liminix against the latest stable release of nixpkgs, so I'd always suggest trying that first. There is a CI job that builds against unstable but it's not always the first to get fixed.)