Skip to content

Instantly share code, notes, and snippets.

@antifuchs
Created September 12, 2022 19:40
Show Gist options
  • Save antifuchs/e30d58a64988907f282c82231dde2cbc to your computer and use it in GitHub Desktop.
Save antifuchs/e30d58a64988907f282c82231dde2cbc to your computer and use it in GitHub Desktop.
# To set this up, first get tailscale working in an isolated linux shell:
# 1. sudo systemctl stop tailscaled.service
# 2. tailscaled -port 9993 -state tailscale-luks-setup.state -tun userspace-networking -socket ./tailscaled.sock
# 3. tailscale -socket ./tailscaled.sock up -hostname HOSTNAME-luks
# 4. tailscale -socket ./tailscaled.sock down
# 5. ctrl-c out of tailscaled
# 6 sudo systemctl start tailscaled.service
#
# Then add the .state file to your machine secrets and pass its path as tailscaleStatePath.
{ config, lib, pkgs, ... }: {
options = {
remote-machine.boot.tailscaleUnlock = with lib; {
enable = mkOption {
description = "Turn on unlock via tailscale";
default = false;
};
tailscaleStatePath = mkOption {
description = "Pre-initialized tailscale state file as a secret. Make sure to set it to not require re-authentication, otherwise the machine may not boot up after a few weeks.";
};
};
};
config =
let
cfg = config.remote-machine.boot.tailscaleUnlock;
# TODO: This uses old-style non-nftables iptables; ideally, we wouldn't have to opt out of that.
# Enabling nftables compat means having to shuffle the list of
# modules down in availableKernelModules; that's a bunch of work
# (deploying to a linux machine & rebooting to see what doesn't
# work this time), so I'm a bit too lazy for that now.
iptables-static = (pkgs.iptables.override { nftablesCompat = false; }).overrideAttrs (old: {
dontDisableStatic = true;
configureFlags = (lib.remove "--enable-shared" old.configureFlags) ++ [
"--enable-static"
"--disable-shared"
];
});
in
lib.mkIf cfg.enable {
boot.initrd = {
secrets = {
"/var/lib/tailscale/tailscaled.state" = cfg.tailscaleStatePath;
"/etc/ssl/certs/ca-certificates.crt" = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
"/etc/ssl/certs/ca-bundle.crt" = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
};
network = {
enable = true;
flushBeforeStage2 = true;
postCommands = ''
# Bring up tailscaled and dial in
echo 'nameserver 8.8.8.8' > /etc/resolv.conf
mkdir /dev/net
mknod /dev/net/tun c 10 200
.tailscaled-wrapped 2>/dev/null &
sleep 5
.tailscale-wrapped up
.tailscale-wrapped status
echo "echo 'Use cryptsetup-askpass to unlock!'" >> /root/.profile
'';
};
availableKernelModules = [
"ip6_tables"
"ip6table_filter"
"ip6table_nat"
"ip6table_raw"
"ip_tables"
"iptable_filter"
"iptable_nat"
"iptable_raw"
"nf_conntrack"
"nf_nat"
"tun"
"xt_comment"
"xt_conntrack"
"xt_mark"
"xt_MASQUERADE"
"xt_LOG"
"xt_tcpudp"
];
extraUtilsCommands = ''
copy_bin_and_libs ${pkgs.tailscale}/bin/.tailscaled-wrapped
copy_bin_and_libs ${pkgs.tailscale}/bin/.tailscale-wrapped
copy_bin_and_libs ${pkgs.iproute}/bin/ip
copy_bin_and_libs ${iptables-static}/bin/iptables
copy_bin_and_libs ${iptables-static}/bin/xtables-legacy-multi
copy_bin_and_libs ${pkgs.strace}/bin/strace
'';
postMountCommands = ''
# tear down tailscale
pkill .tailscaled-wrapped
.tailscaled-wrapped --cleanup
'';
};
};
}
@razielgn
Copy link

Just tested: it works, thanks! NixOS 23.05

@antifuchs
Copy link
Author

Glad to hear it! I was talking to some tailscale folks about this earlier and they said that this does work, but they'll eventually start requiring a write-able connection state. I'm thinking about redoing this in the form of a little partition that is the home for that state store and the server SSH keys; if the host system has SecureBoot, it could even be encrypted on disk and unlock automatically via the TPM2. But for now, this should continue to work ok (:

@patrick-theprogrammer
Copy link

I was still able to get this working in 2025, but it took some modification due to updates to the tailscale nix package and tailscale in general.

I also made some small improvements, like a timeout for tailscale up to prevent getting stuck in an unrecoverable boot hang if eg something breaks with tailscale (or if you are missing misc network config like initrd kernel modules for your nic).

Here’s what I ended up with:


# To set this up, first get tailscale working in an isolated linux shell:
#  1. tailscaled --port 41642 --statedir=/var/lib/tailscale/boot --tun userspace-networking --socket=/var/lib/tailscale/boot/tailscaled.sock
#  2. & tailscale --socket=/var/lib/tailscale/boot/tailscaled.sock up --ssh --hostname=HOSTNAME-boot --reset
#  3. tailscale --socket=/var/lib/tailscale/boot/tailscaled.sock down
#  4. ctrl-c out of tailscaled
#
# Then pass the .state directory as stateDir.

{ config, lib, pkgs, ... }: {
  options = {
    boot.initrd.network.tailscale = with lib; {
      enable = mkOption {
        description = "Turn on unlock via tailscale";
        default = false;
      };

      stateDir = mkOption {
        description = "Pre-initialized tailscale state file directory for secrets. Make sure to set it to not require re-authentication, otherwise the machine may not boot up after a few weeks.";
      };
    };
  };

  config =
    let
      cfg = config.boot.initrd.network.tailscale;
      iptables-static = (pkgs.iptables.override { nftablesCompat = false; }).overrideAttrs (old: {
        dontDisableStatic = true;
        configureFlags = (lib.remove "--enable-shared" old.configureFlags) ++ [
          "--enable-static"
          "--disable-shared"
        ];
      });
    in
    lib.mkIf cfg.enable {
      boot.initrd = {
        secrets = {
          "/var/lib/tailscale/tailscaled.state" = "${cfg.stateDir}/tailscaled.state";
          "/etc/ssl/certs/ca-certificates.crt" = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
          "/etc/ssl/certs/ca-bundle.crt" = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
          "/etc/group" = "/etc/group"; # Needed for ssh
        };
        network = {
          enable = true;
          flushBeforeStage2 = true;
          postCommands = ''
            # Bring up tailscaled and dial in.
            echo "Starting Tailscale..."
            echo "nameserver 8.8.8.8" > /etc/resolv.conf
            mkdir /dev/net
            mknod /dev/net/tun c 10 200
            .tailscaled-wrapped &> /var/lib/tailscale/tailscaled-log.txt &
            sleep 5
            timeout 120s sh -c "${pkgs.tailscale}/bin/tailscale up" && echo "Tailscale started successfully."
          '';
        };
        availableKernelModules = [
          "ip6_tables"
          "ip6table_filter"
          "ip6table_nat"
          "ip6table_raw"
          "ip_tables"
          "iptable_filter"
          "iptable_nat"
          "iptable_raw"
          "nf_conntrack"
          "nf_nat"
          "tun"
          "xt_comment"
          "xt_conntrack"
          "xt_mark"
          "xt_MASQUERADE"
          "xt_LOG"
          "xt_tcpudp"
        ];
        extraUtilsCommands = ''
          copy_bin_and_libs ${pkgs.tailscale}/bin/.tailscaled-wrapped
          copy_bin_and_libs ${pkgs.iproute2}/bin/ip
          copy_bin_and_libs ${iptables-static}/bin/iptables
          copy_bin_and_libs ${iptables-static}/bin/xtables-legacy-multi
          copy_bin_and_libs ${pkgs.strace}/bin/strace
        '';
        postMountCommands = ''
          # Tear down tailscale.
          pkill .tailscaled-wrapped
          .tailscaled-wrapped --cleanup
        '';
      };
    };
}

@antifuchs
Copy link
Author

Ooh, I'd totally forgotten that I posted this gist! You can totally do this for boot-unlocking, but I no longer use it; instead, I wrote and now use this: https://github.com/boinkor-net/hoopsnake - a fully-integrated SSH server on a tailnet, that works with systemd TPM-encrypted secrets. It's been running here for about 3/4 of a year, and has kept allowing me to unlock remote machines all that time (:

If you don't want to throw your fate at a relatively young project with only one live maintainer, I can recommend this repo as a skeleton, too: https://github.com/ElvishJerricco/stage1-tpm-tailscale - it is backed by systemd in initrd, and is very modern and sleek. Also uses TPM-encrypted secrets that can be made optional.

@patrick-theprogrammer
Copy link

patrick-theprogrammer commented Apr 17, 2025

Wow I took a look through hoopsnake and I think that’s quite an elegant solution. Thank you for sharing.

I was actually already planning to take a look at ElvishJerricco’s solution next, but now I’m thinking I’ll give hoopsnake a shot :) Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment