Skip to content

Instantly share code, notes, and snippets.

@Sn0wo2
Last active October 24, 2025 17:02
Show Gist options
  • Select an option

  • Save Sn0wo2/111fa61db8f2996d79baf0ea5e9d18dc to your computer and use it in GitHub Desktop.

Select an option

Save Sn0wo2/111fa61db8f2996d79baf0ea5e9d18dc to your computer and use it in GitHub Desktop.
Batch delete unauthorized tailscale machines
# tailscale找了一圈没有批量删除所有未授权设备的地方然后写了个脚本
# docker配置失误发现给我创建了几百个未授权machines...
import requests, ssl, threading, time, queue, sys
from requests.adapters import HTTPAdapter
from urllib3 import Retry
API_TOKEN = "REPLACE_YOUR_API_KEY"
HEADERS = {"Authorization": f"Bearer {API_TOKEN}", "Connection": "close"}
BASE_URL = "https://api.tailscale.com/api/v2"
THREADS = 1
session = requests.Session()
retries = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET", "DELETE"],
)
adapter = HTTPAdapter(max_retries=retries, pool_connections=THREADS, pool_maxsize=THREADS)
session.mount("https://", adapter)
session.verify = True
def safe_request(method, url, **kwargs):
for attempt in range(3):
try:
return session.request(method, url, headers=HEADERS, timeout=(5, 10), **kwargs)
except (ssl.SSLEOFError, requests.RequestException) as e:
print(f"[retry {attempt+1}] {e}", flush=True)
time.sleep(1)
return None
def worker(q):
while True:
try:
device = q.get_nowait()
except queue.Empty:
break
r = safe_request("DELETE", f"{BASE_URL}/device/{device['id']}")
code = r.status_code if r else "fail"
device_name = device.get('name') or device.get('hostname') or device.get('id')
print(f"Deleted: {device_name} ({r.status_code if r else 'fail'})")
q.task_done()
def interactive_delete():
resp = safe_request("GET", f"{BASE_URL}/tailnet/-/devices")
if not resp:
print("Failed to fetch device list.")
return
devices = resp.json().get("devices", [])
print(f"Fetched {len(devices)} devices.")
for i, d in enumerate(devices, 1):
s = "Authorized" if d.get("authorized") else "Unauthorized"
print(f"{i:3d}. {d['name']:30} ({s}) - {d.get('user', 'N/A')}")
print("\nEnter machine numbers (comma separated), or 'unauthorized' to delete all unauthorized devices:")
choice = input().strip()
if choice.lower() == "unauthorized":
targets = [d for d in devices if not d.get("authorized")]
else:
idx = [int(x.strip()) - 1 for x in choice.split(",")]
targets = [devices[i] for i in idx if 0 <= i < len(devices)]
print(f"\nConfirm deleting {len(targets)} devices? (Y/N): ", end="")
sys.stdout.flush()
if input().strip().upper() != "Y":
print("Canceled.")
return
q = queue.Queue()
for d in targets:
q.put(d)
threads = [threading.Thread(target=worker, args=(q,), daemon=True) for _ in range(THREADS)]
for t in threads:
t.start()
q.join()
print("All task done!", flush=True)
if __name__ == "__main__":
interactive_delete()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment