meta, magick#
Fri 11 Sep 2026 03:25:23 PM UTC
I ran cripslock through PageSpeed
Insights a few weeks back and was aghast
to see how slow it was. No small part of this was due to having been
uploading 4096x3072 phone photos directly onto the site and letting
Boxy deal with presenting them at a
suitable size - resizing client-side does nothing for the transfer
size. Obviously.
So this is just a short blog post about blogging software: I have
added luagraphicsmagick to the build, and use it to scale images to
1024 pixels
before serving them, which should reduce their size approximately
ninefold for little loss of quality.
(I also recently added a search box, which is in the sidebar)
When I went back to PageSpeed after deploying that, it said I should
use srcset, and a better image format, and better compression, and
do a bunch of other not-necessarily image-related stuff I'm also not
doing (setting sensible cache headers would be a good one), and I am
putting off doing almost any of it because I feel I should be
spending the time on things I can write about instead of on polishing
the platform where I write it. It also said I score 0/2 on "AI
readiness", and my only question there is whether (and how) it's
possible to get a negative score for that. OK, that's two questions.
Things I could be writing about but am not include
-
diagnosing an electrical problem and replacing the reg/rect on my
motorbike (unwritten as I haven't actually replaced it yet, because
the wires on the new one are too short)
-
diagnosing a why-is-it-spitting-fluid-out-of-the-overflow problem
and replacing the oil/coolant on my motorbike (unwritten until I can
find out why it's refusing to take the full volume of new coolant)
-
rigging up the Liminix wireless extender in the shed to use SOPS for
its secrets (unwritten because I ... well, mostly because I just haven't written it)
Secret dogfood#
Sun 13 Sep 2026 06:11:23 PM UTC
Having admitted the other day that I could have written about
rigging up the Liminix wireless extender in the shed to use SOPS for its secrets (unwritten because I ... well, mostly because I just haven't written it)
but hadn't, I felt compelled to sort it out.
I've been working on this on and off for longer than I can remember,
because the whole thing still feels brittle and hard to test, and is
for that reason unsatisfying. But the other device providing wifi in
the shed seems to have entirely ceased working, and it's where I
perform $dayjob when I'm not in the office, so I needed to do
something as it's embarrassing to have my Zoom calls drop out.
(Every other important device in the shed has wired ethernet, but my
work Mac is too locked down and corporate to make that an option)
There are a couple of things I want the new generation of Liminix
devices to do that the old ones don't:
-
send their logs somewhere that won't disappear on reboot. I have talked about this previously, concluding on that occasion that it was "basically finished and ... just needs installing on some real devices"
-
get their "secrets" from a server somewhere instead of baking them
into the image at build time, so that secrets can be rotated and
configuration changed without reflashing the device. "Secrets" here
is currently mostly meaning things like the wifi password, allowed ssh keys etc,
which are technically either not secret at all or not especially
sensitive but let's at least try hey?
As the first of them was "basically finished", I have been spending
hours/months (actual/elapsed) on the second.
In essence it's simple: Liminix already has code to get secrets from a
server,
which is basically anything that can serve a JSON file over HTTP(S).
A service on the client gets the JSON file at configurable intervals,
and unpacks it into a directory under
/run/services/outputs/service-name where it can be referenced by any
other service that needs it
In essence it would be simple, so of course I have accidentally it
more complicated, by deciding to keep secrets in
SOPS and to use HTTPS client certificates for
authentication.
-
I wrote an HTTPS server (it's called
Undercroft) that does mTLS
(could have just used nginx for this bit) and on each request runs sops decrypt
(probably couldn't have just used nginx for this bit) to decrypt my secrets file on the fly and transform it
from yaml - which is human-readable but not easily machine parseable -
to json, which is the opposite.
-
On the client side, the Lminix secrets fetcher was previously only
capable of HTTP basic auth, but luckily it was dead simple to add mTLS
because fetch-freebsd already consults environment variables
SSL_CLIENT_CERT_FILE, SSL_CLIENT_KEY_FILE, SSL_CA_CERT_FILE
The threat modelling here might be a little weird, to be
honest. using SOPS means that our secrets are encrypted at rest and we
can back them up, put them in git servers etc with impunity, etc but
instead of having a decryption key per device we decrypt using a
single key owned by the https server, and then re-encrypt (courtesy of
TLS) for the wire. The more sympathetic way to use SOPS would probably
be to use ssh host keys as age identities, but that requires getting
the host keys off each of the devices after they've been created at
first boot, then re-encrypting the file for all the recipients.
How the client gets its certificate in the first place is the part
that's still a bit unsatisfying, because it's automated and the
authentication uses a shared secret that's baked into the Liminix
image - so anyone with a copy of one of your devices' Liminix images
and binwalk could make themselves a TLS cert and fire off a request
to your Certifix service to
sign it. But when the machine seeking a signature has no identifying features
that can't be faked, and no i/o capabilities, how does it attest that
it really is the machine it claims to be, and not an imposter?
-
ssh host keys are generated on first boot. We don't want to
create them on the build machine and bake them into the image, but
this means that to see what key was generated we'd have to connect
to the box after it booted, meaning (a) that it can't be depending
on any of those secrets to configure networking, (b) how do we know
we're connecting to the right place?
-
the machines are physical computers that can't be certified by their
hypervisor (as in AWS, Azure etc)
-
the mac address is easy to guess/fake
-
we could write some secret into u-boot env, but only on devices where
u-boot has been configured with a working persistent environment, and only
if we can get to the u-boot prompt (probably needs serial console)
-
if the device has controllable LEDs, the server could send it a
pattern and it could blink the LEDs in that pattern. I've only just
thought of this idea and it might be a good one.
-
we could change how images are written and flashed so that a
per-device secret that authenticates the machine to certifix is
appended/prepended/merged into the image before it's flashed. This
might be possible but will require some thought if we want to keep
the secret out of the nix store. Ideally we'd make the secret some
kind of time-limited, single-use token.
Anyway, I've done none of those things, even the ones that are
possible. But it's up and running and I flashed it using
Levitate using a chunk of
code like this in its configuration
defaultProfile.packages = with pkgs; [
(levitate.override {
config = {
imports = [ "${modulesPath}/ssh" ];
programs.busybox.applets = [ "flashcp" ];
services = {
# dhcp on wired ethernet only instead of trying to bring up
# all the wifi stuff
dhcpc4 = svc.dhcp4c.client.build {
interface = config.hardware.networkInterfaces.lan;
dependencies = [ config.services.hostname ];
};
# for recovery image we will just embed static keys instead of
# running the full secrets management doodad
sshd = svc.ssh.build {
authorizedKeys.root = import ../users/dan/authorized-keys.nix;
};
};
};
})
];
There is a gotcha here that I need to address: you have to re-import
the ssh module inside levitate's config, otherwise although it finds
svc.ssh OK, it doesn't set config.programs.busybox.options.FEATURE_FANCY_ECHO and as a result
the authorizedKeys file is garbage.
Anyway, it's up and running and doesn't seem to have crashed
yet. There is something weird (but, I think, basically cosmetic) going
on with the log shipping forwarder which keeps stopping and restarting
whenever it does a DHCP renew, but I will write about that another
time - hopefully, after I have found out what the hell is happening.
In other news, Liminix is currently not building against nixpkgs unstable
because of a cross-compilation execline build failure. Check the Hydra for the last known working version.