Build remote deployment system using Headscale (self-hosted Tailscale)

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/7fa16927-250f-4af4-bb11-e22ef7b2c997

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-11 23:33:35 +00:00
committed by GitHub
parent 9ec8618f7d
commit 8f97aa416f
7 changed files with 849 additions and 0 deletions

View File

@@ -63,6 +63,9 @@ in
git
curl
openssh
tailscale
jq
xxd
];
# Remote install support — SSH on the live ISO
@@ -88,6 +91,88 @@ in
environment.etc."sovran/flake".source = sovranSource;
environment.etc."sovran/installer.py".source = ./installer.py;
# These files are gitignored — set at build time by placing them in iso/secrets/
environment.etc."sovran/enroll-token" = lib.mkIf (builtins.pathExists ./secrets/enroll-token) {
text = builtins.readFile ./secrets/enroll-token;
mode = "0600";
};
environment.etc."sovran/provisioner-url" = lib.mkIf (builtins.pathExists ./secrets/provisioner-url) {
text = builtins.readFile ./secrets/provisioner-url;
mode = "0644";
};
# Tailscale client for mesh VPN
services.tailscale.enable = true;
# Auto-provision service — registers with provisioning server and joins Tailnet
systemd.services.sovran-auto-provision = {
description = "Auto-register with Sovran provisioning server and join Tailnet";
after = [ "network-online.target" "tailscaled.service" ];
wants = [ "network-online.target" "tailscaled.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
path = [ pkgs.tailscale pkgs.curl pkgs.jq pkgs.coreutils pkgs.iproute2 pkgs.xxd ];
script = ''
TOKEN_FILE="/etc/sovran/enroll-token"
URL_FILE="/etc/sovran/provisioner-url"
[ -f "$TOKEN_FILE" ] || { echo "No enroll token found, skipping auto-provision"; exit 0; }
[ -f "$URL_FILE" ] || { echo "No provisioner URL found, skipping auto-provision"; exit 0; }
TOKEN=$(cat "$TOKEN_FILE")
PROV_URL=$(cat "$URL_FILE")
[ -n "$TOKEN" ] || exit 0
[ -n "$PROV_URL" ] || exit 0
# Wait for network + tailscaled
sleep 10
# Collect machine info
HOSTNAME="sovran-deploy-$(head -c 8 /dev/urandom | xxd -p)"
MAC=$(ip link show | grep ether | head -1 | awk '{print $2}' || echo "unknown")
echo "Registering with provisioning server at $PROV_URL..."
# Retry up to 6 times (covers slow DHCP)
RESPONSE=""
for i in $(seq 1 6); do
RESPONSE=$(curl -sf --max-time 15 -X POST \
"$PROV_URL/register" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"hostname\": \"$HOSTNAME\", \"mac\": \"$MAC\"}" 2>/dev/null) && break
echo "Attempt $i failed, retrying in 10s..."
sleep 10
done
if [ -z "$RESPONSE" ]; then
echo "ERROR: Failed to register with provisioning server after 6 attempts"
exit 1
fi
HS_KEY=$(echo "$RESPONSE" | jq -r '.headscale_key')
LOGIN_SERVER=$(echo "$RESPONSE" | jq -r '.login_server')
if [ -z "$HS_KEY" ] || [ "$HS_KEY" = "null" ]; then
echo "ERROR: No Headscale key in response: $RESPONSE"
exit 1
fi
echo "Joining Tailnet via $LOGIN_SERVER as $HOSTNAME..."
tailscale up \
--login-server="$LOGIN_SERVER" \
--authkey="$HS_KEY" \
--hostname="$HOSTNAME"
TAILSCALE_IP=$(tailscale ip -4)
echo "Successfully joined Tailnet as $HOSTNAME ($TAILSCALE_IP)"
'';
};
environment.etc."xdg/autostart/sovran-installer.desktop".text = ''
[Desktop Entry]
Type=Application