Update preauthkey and join script
Build Kiosk Image (Raspberry Pi) / rpi (push) Canceled after 0s
Build Kiosk Images (x86) / android (push) Successful in 3m44s
Build Kiosk Images (x86) / commercial (push) Successful in 3m41s
Build Kiosk Images (x86) / kiosk (push) Successful in 3m45s

This commit is contained in:
NeilClack
2026-10-04 20:12:25 -05:00
parent eafa4b6e26
commit 31bc806d01
+60 -5
View File
@@ -13,7 +13,16 @@ let
myKey = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL4qWNZnYU5JaqR9c2qh6oFZktzVNBOx42PfDaGofCL6 cnclack@pm.me macbook" ];
# Headscale preauth key. Paste/replace here.
headscaleAuthKey = "hskey-auth-zZdWiKTwDfTT-tgRKiAHyXabykoARDDyNfYihgVrJQ6y6zK7lwV0FW6xwHTdyWoqa_ZpCf80vgAJC";
#
# This MUST be a *reusable* key with a long expiry (1 year is good), created
# with:
# headscale preauthkeys create --user <user-id> --reusable --expiration 8760h
#
# A single-use key only ever enrolls one node, which would mean a fresh key
# plus a rebuild for every kiosk that joins the fleet. A reusable key lets any
# number of kiosks enroll unattended, and the timer below retries the join so
# a node that misses its window at boot heals on its own.
headscaleAuthKey = "hskey-auth-UfU_eS_sdOu6-CjrqrVdDJFRFsPnaTtK5sFuCiVlffJAS7uQ1wX1_65MNk205WGsF4clOiBD3ZOV-";
in
{
############################################################
@@ -115,7 +124,9 @@ in
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
# Deliberately no RemainAfterExit: the unit has to fall back to inactive
# so the timer below can trigger it again. The script is idempotent, so
# re-running it on an already-joined kiosk is a cheap no-op.
};
path = [ pkgs.tailscale pkgs.coreutils pkgs.gnused pkgs.gnugrep ];
script = ''
@@ -144,7 +155,17 @@ in
| cut -c1-63)"
[ -n "$serial" ] || serial="radiantwave-unknown"
echo "radiantwave-tailscale-auth: joining as '$serial'"
# Already joined? Bail out early. This is what makes the periodic
# re-invocation from the timer below cheap and safe to run forever.
# The JSON is pretty-printed with a space, so allow optional whitespace.
state="$(tailscale status --json 2>/dev/null \
| sed -n 's/.*"BackendState": *"\([A-Za-z]*\)".*/\1/p')"
if [ "$state" = "Running" ]; then
echo "radiantwave-tailscale-auth: already joined as '$serial'"
exit 0
fi
echo "radiantwave-tailscale-auth: joining as '$serial' (state: ''${state:-unknown})"
for _ in $(seq 1 30); do
if tailscale up \
--auth-key=${pkgs.lib.escapeShellArg headscaleAuthKey} \
@@ -158,11 +179,45 @@ in
sleep 2
done
echo "radiantwave-tailscale-auth: could not join tailnet after retries" >&2
exit 1
# Best-effort by design: do NOT exit non-zero here.
#
# This unit is part of the new generation that switch-to-configuration
# starts, and *any* failing unit among those aborts the switch with
# status 4/NOPERMISSION. While the preauth key was expired that is exactly
# what happened: this unit failed, which failed `switch-to-configuration`,
# which failed nixos-upgrade.service -- so the tailscale key silently
# stopped *all* system upgrades on the fleet every 10 minutes.
#
# The retry timer below is what actually recovers the join, so a failed
# attempt here must never take down the system switch with it.
echo "radiantwave-tailscale-auth: could not join tailnet after retries;" >&2
echo "radiantwave-tailscale-auth: leaving the join to the retry timer" >&2
exit 0
'';
};
# The join only needs to happen once per node, but it fails for reasons
# outside our control: the network may not be usable yet at boot, tailscaled
# may be restarted later, or the preauth key may be rotated out from under
# the fleet. A timer re-runs the (idempotent) join so a kiosk recovers on its
# own instead of needing a manual `systemctl restart`.
#
# This replaces the old behavior where the one-shot unit failed once at boot
# and stayed failed forever (Restart=no, NRestarts=0).
systemd.timers.radiantwave-tailscale-auth = {
description = "Periodically ensure the kiosk is joined to the tailnet";
wantedBy = [ "timers.target" ];
timerConfig = {
# The join is attempted at boot via multi-user.target above; this is the
# first retry in case that one lost the race with the network.
OnBootSec = "5min";
# Measured from when the unit last went inactive, so a slow join doesn't
# immediately retrigger.
OnUnitInactiveSec = "10min";
Unit = "radiantwave-tailscale-auth.service";
};
};
system.autoUpgrade = {
enable = true;
flake = upgradeFlakeRef;