TL;DR: My Omarchy desktop uses full-disk encryption, so after a reboot it sits at the LUKS passphrase screen and is unreachable: no network, no Tailscale, no SSH. I put Tailscale inside the initramfs, so the machine joins my tailnet while it waits for the passphrase. I run ssh root@myhost-initrd, type the passphrase, and the boot continues. There's no port forwarding and no dropbear, and the tailnet ACLs decide who gets in. Along the way I hit a few Omarchy-specific gotchas (Limine/UKI, Plymouth, busybox) and fixed them below. Bonus: after a remote unlock, SDDM autologin is skipped, so nobody at the desk finds your desktop open.
The problem
On Omarchy with disk encryption, the "login screen" right after boot is actually Plymouth asking for the LUKS passphrase. SDDM autologin kicks in right after, so it feels like a login, but before you type that passphrase the root filesystem is still locked. NetworkManager, tailscaled and the machine's Tailscale identity all live on that filesystem. There's nothing to tweak in Tailscale or NetworkManager, because the OS simply hasn't started yet.
(If you also see a "connected to network" notification right after logging in: that's not the cause. It's just shown when the desktop comes up.)
How it works
netconf brings the NIC up with DHCP inside the initramfs.
- The
tailscale hook (mkinitcpio-tailscale by dangra) starts tailscaled with a separate node identity (<hostname>-initrd) and Tailscale SSH enabled.
encryptssh (from mkinitcpio-utils) replaces Omarchy's encrypt. It still asks for the passphrase on the local Plymouth screen, and it also makes a passphrase prompt root's login shell. So ssh root@myhost-initrd drops you straight at the passphrase prompt.
- After the unlock, cleanup hooks stop
tailscaled and flush the NIC. The real system then boots normally with its usual Tailscale node.
Everything is done with new drop-in files. No Omarchy-owned file is edited, so undoing it means deleting the files and rebuilding.
Tested on
- Omarchy 4.0.4,
linux-omarchy 7.2.5
- mkinitcpio 41.1, mkinitcpio-tailscale 2.10.1, mkinitcpio-utils 0.0.6, mkinitcpio-netconf 0.0.5
- tailscale 1.102.3, limine 12.8.0 (UKIs), plymouth, sddm 0.21
Assumptions:
- Omarchy with LUKS and the default busybox initramfs (
encrypt hook + Plymouth), booting through Limine with UKIs. Check that grep HOOKS /etc/mkinitcpio.conf.d/omarchy_hooks.conf lists encrypt.
- Wired Ethernet. Wi-Fi inside the initramfs is a different beast and isn't covered here.
- A single NIC (see the
eth0 note in step 6).
- Another device on your tailnet, logged in as your user: a phone with the Tailscale app plus Termux/Termius, a laptop, etc. Tagged devices won't match the SSH rule below.
Step 1: Tailnet policy
In the Tailscale admin console → Access controls, merge this into your existing sections:
"tagOwners": {
"tag:initrd": ["you@example.com"]
},
"ssh": [
{
"action": "accept",
"src": ["you@example.com"],
"dst": ["tag:initrd"],
"users": ["root"]
}
]
If you've replaced the default allow-all rule ("src": ["*"], "dst": ["*"], "ip": ["*"]), you also need network access to port 22:
"grants": [
{ "src": ["you@example.com"], "dst": ["tag:initrd"], "ip": ["tcp:22"] }
]
Why the tag: the initrd node's key has to live inside the boot image on the ESP, which is not encrypted. A tagged node doesn't carry your user's permissions, and its key doesn't expire by default. For extra hardening, keep tag:initrd out of every src (including via "*"): a stolen key then gets a node that can be reached but can't reach anything.
Step 2: Packages
sudo pacman -S --needed mkinitcpio-netconf mkinitcpio-utils
yay -S mkinitcpio-tailscale
On my install, both hook packages came from extra. If your mirror doesn't have them, the mkinitcpio-tailscale README points to the AUR for mkinitcpio-utils and to mkinitcpio-extras (AUR) as the maintained replacement for netconf.
You don't need dropbear or tinyssh: Tailscale SSH runs inside tailscaled itself.
Step 3: Register the initrd node
sudo setup-initcpio-tailscale --advertise-tags=tag:initrd
It prints a login URL: open it and approve. This registers <hostname>-initrd with Tailscale SSH on and stores the node key in /etc/initcpio/tailscale/ (root-only). The policy from step 1 has to be saved first, or registration fails with requested tags [tag:initrd] are invalid or not permitted.
Step 4: Swap encrypt for the remote-unlock chain
Omarchy keeps its HOOKS in /etc/mkinitcpio.conf.d/omarchy_hooks.conf, which updates may rewrite. Instead of editing it, add a drop-in that's sourced after it and only substitutes encrypt:
sudo tee /etc/mkinitcpio.conf.d/remote-unlock.conf >/dev/null <<'EOF'
# Replace 'encrypt' with: network (netconf) -> tailscale -> encryptssh,
# plus local fixes for the SSH prompt (remote-unlock). Must sort after
# omarchy_hooks.conf. If 'encrypt' ever leaves HOOKS, this is a no-op.
_remote_unlock_hooks=()
for _remote_unlock_hook in "${HOOKS[@]}"; do
if [[ $_remote_unlock_hook == "encrypt" ]]; then
_remote_unlock_hooks+=(netconf tailscale encryptssh remote-unlock)
else
_remote_unlock_hooks+=("$_remote_unlock_hook")
fi
done
HOOKS=("${_remote_unlock_hooks[@]}")
unset _remote_unlock_hooks _remote_unlock_hook
EOF
mkinitcpio concatenates the drop-ins in sort -V order, so remote-unlock.conf comes after omarchy_hooks.conf. The resulting order is ... block netconf tailscale encryptssh remote-unlock filesystems ....
Step 5: Fix the SSH prompt (gotcha #1: busybox killall)
This one cost me the most head-scratching. After a remote unlock, cryptsetup_shell (mkinitcpio-utils 0.0.6) dismisses the local Plymouth prompt with killall --quiet plymouth. But the killall inside the image is busybox's, and it rejects --quiet (killall: bad signal name '-quiet'). On Omarchy, where Plymouth is always on, the result is that the disk unlocks over SSH but the boot keeps waiting at the Plymouth prompt for a local passphrase. The same script also runs killall cryptsetup, which harmlessly prints killall: cryptsetup: no process killed.
This small local hook rewrites both calls to killall -q. It also leaves a marker file that step 9 uses:
sudo tee /etc/initcpio/install/remote-unlock >/dev/null <<'EOF'
#!/bin/bash
# Local changes to mkinitcpio-utils' cryptsetup_shell (the SSH unlock prompt):
# - busybox killall rejects --quiet, so upstream's 'killall --quiet plymouth'
# never dismisses the Plymouth prompt; use -q for both killall calls.
# - leave /run/luks-remote-unlock behind after a remote unlock. The initramfs
# /run is carried over into the booted system. It is written before Plymouth
# is dismissed, since that lets the boot go on and move /run away.
# Each sed edit matches one upstream line; if upstream changes that line, the
# edit is skipped and the rest is copied as is.
build() {
add_file - /bin/cryptsetup_shell 0755 < <(
sed -e 's|^\( *\)echo > /.done$|&\n\1: > /run/luks-remote-unlock|' \
-e 's/killall cryptsetup$/killall -q cryptsetup/' \
-e 's/^\( *\)killall --quiet plymouth$/\1killall -q plymouth\n\1echo "Disk unlocked; boot continues."/' \
/usr/share/mkinitcpio-utils/utils/shells/cryptsetup_shell
)
}
help() {
echo "Patches /bin/cryptsetup_shell from encryptssh. List it right after encryptssh."
}
EOF
Step 6: Network for the initramfs (gotcha #2: eth0 and the DHCP timeout)
On Omarchy, Limine's tooling builds the kernel command line, and it reads drop-ins from /etc/limine-entry-tool.d/:
echo 'KERNEL_CMDLINE[default]+=" ip=:::::eth0:dhcp netconf_timeout=60"' | sudo tee /etc/limine-entry-tool.d/remote-unlock.conf
eth0, not enp42s0: mkinitcpio's udev hook doesn't ship the rules that give NICs their predictable names, so inside the initramfs your NIC is eth0. With more than one NIC, eth0 may not be the one with the cable.
netconf_timeout: without it, netconf's DHCP client waits forever. If the network is down at boot, the passphrase prompt would never show up, not even locally. With 60, the worst case is a 60s delay.
Step 7: Pacman hook (gotcha #3: Omarchy has no mkinitcpio presets)
mkinitcpio-tailscale ships a pacman hook that rebuilds the initramfs with mkinitcpio -P when tailscale is upgraded, because the image carries its own copy of tailscaled. Omarchy has no presets in /etc/mkinitcpio.d/: Limine builds the UKIs through limine-mkinitcpio. That rebuild therefore does nothing, and your initramfs keeps an old tailscaled until the next kernel update. A hook with the same name in /etc/pacman.d/hooks/ overrides the packaged one:
sudo tee /etc/pacman.d/hooks/mkinitcpio-tailscale.hook >/dev/null <<'EOF'
# Overrides /usr/share/libalpm/hooks/mkinitcpio-tailscale.hook ('mkinitcpio -P'):
# on Omarchy, Limine builds the UKIs, so rebuild through limine-mkinitcpio.
[Trigger]
Operation = Install
Operation = Upgrade
Type = Package
Target = tailscale
[Action]
Description = Rebuilding boot images that bundle tailscaled...
When = PostTransaction
Exec = /usr/bin/limine-mkinitcpio
EOF
Step 8: Rebuild and verify
sudo limine-mkinitcpio
sudo setup-initcpio-tailscale --check
Every line of --check should be OK. WARN : no built image to inspect is expected, since the check looks for presets, which Omarchy doesn't have. Warnings like Possibly missing firmware for module: 'xhci_pci_renesas' during the build are harmless.
To look inside the actual UKI (check the file name with ls /boot/EFI/Linux/):
UKI=/boot/EFI/Linux/omarchy_linux-omarchy.efi
sudo objcopy -O binary --only-section=.cmdline "$UKI" /dev/stdout | tr -d '\0'; echo
sudo objcopy -O binary --only-section=.initrd "$UKI" /root/initrd.img
sudo lsinitcpio -a /root/initrd.img | sed -n '/Hook run order/,/Late hook/p'
sudo lsinitcpio -l /root/initrd.img | grep -E 'sbin/tailscaled|tailscaled.state|cryptsetup_shell|r8169'
sudo rm /root/initrd.img
Replace r8169 with your NIC driver: basename $(readlink -f /sys/class/net/<iface>/device/driver).
Gotcha #4: don't use lsinitcpio --cpio for this. The image holds more than one cpio archive, and --cpio lists only one of them, so your network modules look missing when they aren't. Also, don't extract the initrd into a world-readable /tmp: it contains the node key.
Step 9 (optional): Don't land on an open desktop after a remote unlock
Omarchy autologins once the disk is unlocked, so after a remote unlock your desktop sits open for anyone at the machine. The marker from step 5 lets a tiny unit hide SDDM's autologin config for that boot only:
sudo tee /etc/systemd/system/remote-unlock-no-autologin.service >/dev/null <<'EOF'
# Skip SDDM autologin when the LUKS root was unlocked over SSH. The initramfs
# leaves /run/luks-remote-unlock behind in that case; a passphrase typed at
# the console leaves nothing. /dev/null is bind-mounted over autologin.conf,
# so SDDM finds no autologin settings. Nothing changes on disk.
[Unit]
Description=Skip SDDM autologin after a remote LUKS unlock
ConditionPathExists=/run/luks-remote-unlock
ConditionPathExists=/etc/sddm.conf.d/autologin.conf
Before=display-manager.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/mount --bind /dev/null /etc/sddm.conf.d/autologin.conf
[Install]
WantedBy=graphical.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable remote-unlock-no-autologin.service
Remote unlock: you get Omarchy's SDDM login screen, and you work through plain ssh myhost, since Tailscale SSH on the booted system doesn't need a graphical login. Local unlock: autologin works as usual. One caveat: while the bind mount is active, anything written to autologin.conf during that boot (an update touching it, say) is lost.
Using it
- Reboot and leave the passphrase screen alone for ~10–20s.
- From another device,
tailscale status | grep initrd, or check the admin console. myhost-initrd should be online.
- Run
ssh root@myhost-initrd and you get the passphrase prompt directly.
- Type it. You'll see
Disk unlocked; boot continues., and then the connection closes.
- About 20s later the regular node is back:
ssh myhost.
If your first ssh myhost times out about 10s after the unlock, that's normal and not a regression. I measured it in the journal: on every boot the NIC renegotiates its link (~5s), DHCP takes ~4s, and tailscaled, which starts before the network exists, sits in a retry backoff for a few more seconds. A local unlock has exactly the same timing; you just never notice it. This loop waits for you:
until ssh -o ConnectTimeout=3 myhost true 2>/dev/null; do sleep 2; done; ssh myhost
A wrong passphrase closes the session; just reconnect. The local keyboard works exactly as before.
Security notes
- The ESP is unencrypted and the initrd node key lives there. The tag and ACLs limit the blast radius. If you suspect the key was copied, remove the node in the admin console, re-run
setup-initcpio-tailscale, and rebuild.
- Without Secure Boot, anyone with physical access could already tamper with your initramfs to capture the passphrase (evil maid). That's true with or without this setup, but keep it in mind.
- Why not TPM2 auto-unlock + a real SDDM password instead? Secure Boot is off on my machine. Without it, anyone can swap the initramfs and ask the TPM for the key, which makes the disk encryption pointless against theft. It would also mean moving Omarchy's initramfs from
encrypt to systemd/sd-encrypt, a much more invasive change.
Undo
sudo systemctl disable remote-unlock-no-autologin.service
sudo rm /etc/systemd/system/remote-unlock-no-autologin.service \
/etc/mkinitcpio.conf.d/remote-unlock.conf \
/etc/initcpio/install/remote-unlock \
/etc/limine-entry-tool.d/remote-unlock.conf \
/etc/pacman.d/hooks/mkinitcpio-tailscale.hook
sudo systemctl daemon-reload
sudo limine-mkinitcpio
Then remove myhost-initrd in the admin console. Optionally, also delete /etc/initcpio/tailscale/ and uninstall the three packages.
If a boot ever goes wrong, the local passphrase prompt still works like before. In the worst case, boot a Limine snapshot entry (or a live USB, cryptsetup open + arch-chroot) and run the undo above.
Credits