fix(tailscale): verify key-granted tags for Headscale enrollment

This commit is contained in:
kaka
2026-11-04 07:00:31 +08:00
parent 96ffd3d969
commit 0b18353a2b

View File

@@ -80,13 +80,17 @@ main() {
elif [[ "$state" == NeedsLogin ]]; then elif [[ "$state" == NeedsLogin ]]; then
[[ -n "$auth" ]] || { fail 'Provide a short-lived single-use Headscale enrollment key'; return 1; } [[ -n "$auth" ]] || { fail 'Provide a short-lived single-use Headscale enrollment key'; return 1; }
local up_args=(up --login-server="$login" --hostname="$name" --accept-routes=true --auth-key="file:$auth") local up_args=(up --login-server="$login" --hostname="$name" --accept-routes=true --auth-key="file:$auth")
[[ -z "$tags" ]] || up_args+=(--advertise-tags="$tags") # Headscale 0.29.6 takes pre-authenticated node tags from the key.
# It rejects client RequestTags for every pre-auth-key registration.
if ! tailscale "${up_args[@]}" > "$task/enroll-output" 2>&1; then if ! tailscale "${up_args[@]}" > "$task/enroll-output" 2>&1; then
fail 'Enrollment failed; inspect Headscale and the original node identity before retrying'; return 1 fail 'Enrollment failed; inspect Headscale and the original node identity before retrying'; return 1
fi fi
else fail 'Existing backend is not ready for first enrollment; inspect it without resetting identity'; return 1; fi else fail 'Existing backend is not ready for first enrollment; inspect it without resetting identity'; return 1; fi
tailscale status --json > "$task/status.json" || { fail 'Tailscale enrollment status is unavailable'; return 1; } tailscale status --json > "$task/status.json" || { fail 'Tailscale enrollment status is unavailable'; return 1; }
jq -e '.BackendState == "Running" and (.Self.TailscaleIPs | length > 0)' "$task/status.json" >/dev/null || { fail 'Tailscale did not reach Running with an address'; return 1; } jq -e '.BackendState == "Running" and (.Self.TailscaleIPs | length > 0)' "$task/status.json" >/dev/null || { fail 'Tailscale did not reach Running with an address'; return 1; }
if [[ -n "$tags" ]]; then
jq -e --arg expected "$tags" '($expected | split(",") | sort) == ((.Self.Tags // []) | sort)' "$task/status.json" >/dev/null || { fail 'Headscale authorization tags differ; inspect the pre-auth key and node without resetting identity'; return 1; }
fi
tailscale debug prefs > "$task/prefs.json" || { fail 'Tailscale preferences are unavailable'; return 1; } tailscale debug prefs > "$task/prefs.json" || { fail 'Tailscale preferences are unavailable'; return 1; }
jq -e --arg url "$login" --arg name "$name" '(.ControlURL | if type == "string" then rtrimstr("/") else "" end) == $url and .Hostname == $name' "$task/prefs.json" >/dev/null || { fail 'Enrolled node belongs to another controller or name'; return 1; } jq -e --arg url "$login" --arg name "$name" '(.ControlURL | if type == "string" then rtrimstr("/") else "" end) == $url and .Hostname == $name' "$task/prefs.json" >/dev/null || { fail 'Enrolled node belongs to another controller or name'; return 1; }
echo 'Node enrollment checked. Service /32 addressing, exact route approval and cross-host access still require separate verification.' echo 'Node enrollment checked. Service /32 addressing, exact route approval and cross-host access still require separate verification.'