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:
committed by
GitHub
parent
9ec8618f7d
commit
8f97aa416f
@@ -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
|
||||
|
||||
0
iso/secrets/.gitkeep
Normal file
0
iso/secrets/.gitkeep
Normal file
@@ -14,6 +14,8 @@ Options:
|
||||
--relay-user USER Relay username (default: deploy)
|
||||
--relay-port PORT Relay SSH port (default: 22)
|
||||
--tunnel-port PORT Reverse tunnel port on relay (default: 2222)
|
||||
--headscale-server URL Headscale login server for post-install Tailnet
|
||||
--headscale-key KEY Headscale pre-auth key for the installed OS
|
||||
USAGE
|
||||
}
|
||||
|
||||
@@ -28,6 +30,8 @@ RELAY_HOST=""
|
||||
RELAY_USER="deploy"
|
||||
RELAY_PORT="22"
|
||||
TUNNEL_PORT="2222"
|
||||
HEADSCALE_SERVER=""
|
||||
HEADSCALE_KEY=""
|
||||
|
||||
FLAKE="/etc/sovran/flake"
|
||||
LOG="/tmp/sovran-headless-install.log"
|
||||
@@ -58,6 +62,8 @@ while [[ $# -gt 0 ]]; do
|
||||
--relay-user) RELAY_USER="$2"; shift 2 ;;
|
||||
--relay-port) RELAY_PORT="$2"; shift 2 ;;
|
||||
--tunnel-port) TUNNEL_PORT="$2"; shift 2 ;;
|
||||
--headscale-server) HEADSCALE_SERVER="$2"; shift 2 ;;
|
||||
--headscale-key) HEADSCALE_KEY="$2"; shift 2 ;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
@@ -225,6 +231,7 @@ if [[ -n "$DEPLOY_KEY" ]]; then
|
||||
relayUser = "${RELAY_USER}";
|
||||
relayPort = ${RELAY_PORT};
|
||||
reverseTunnelPort = ${TUNNEL_PORT};
|
||||
$([ -n "${HEADSCALE_SERVER}" ] && echo " headscaleServer = \"${HEADSCALE_SERVER}\";")
|
||||
};
|
||||
}
|
||||
EOF
|
||||
@@ -232,6 +239,14 @@ else
|
||||
cp /mnt/etc/nixos/custom.template.nix /mnt/etc/nixos/custom.nix
|
||||
fi
|
||||
|
||||
# ── Write Headscale auth key if provided ─────────────────────────────────────
|
||||
if [[ -n "$HEADSCALE_KEY" ]]; then
|
||||
mkdir -p /mnt/var/lib/secrets
|
||||
echo "$HEADSCALE_KEY" > /mnt/var/lib/secrets/headscale-authkey
|
||||
chmod 600 /mnt/var/lib/secrets/headscale-authkey
|
||||
log "Headscale auth key written to /mnt/var/lib/secrets/headscale-authkey"
|
||||
fi
|
||||
|
||||
# ── Step 11: Copy configs to host for flake evaluation ───────────────────────
|
||||
log "=== Copying config files to host /etc/nixos for flake evaluation ==="
|
||||
mkdir -p /etc/nixos
|
||||
@@ -252,3 +267,5 @@ log "You can now reboot into Sovran_SystemsOS."
|
||||
log "After reboot, the machine will be accessible via SSH on port 22 (if --deploy-key was provided)."
|
||||
[[ -n "$RELAY_HOST" ]] && \
|
||||
log "Reverse tunnel will connect to ${RELAY_USER}@${RELAY_HOST}:${RELAY_PORT} — forward port ${TUNNEL_PORT} maps to the machine's SSH."
|
||||
[[ -n "$HEADSCALE_SERVER" ]] && \
|
||||
log "Tailscale will connect to Headscale at ${HEADSCALE_SERVER} on first boot."
|
||||
|
||||
Reference in New Issue
Block a user