A plain-language Tailscale overview plugin for Cockpit — see your whole tailnet at a glance, with warnings that tell you what to do, not what protocol failed.
Adds a Tailscale page to Cockpit's menu that answers three questions: Is it working? What is connected? Do I need to do anything? No jargon — no DERP, NAT traversal, ACLs or key-expiry terminology anywhere in the UI. Every warning is an observation plus an action:
"iPhone loses its connection in 5 days. Open the Tailscale app on the phone and sign in again."
| All good | Needs attention |
|---|---|
![]() |
![]() |
This is a personal hobby project I build for my own use and publish in case it's useful to someone else. I work on it in my spare time, so issues and PRs are welcome but replies may be slow. Use at your own risk.
- One-line status — a breathing green lamp and "Everything looks fine — 4 of 6 devices connected", or an amber "3 things need your attention"
- Positive assurance — when nothing is pending: "All 7 devices are known and acknowledged by you. Last new device joined 11 days ago."
- Actionable warnings — key about to expire, unknown new device, device removed from the network, device waiting for approval, rarely-used device suddenly online, outdated clients
- Acknowledgments — new-device and removed-device warnings stay until you press "Yes, that was me". They never silently expire, so nothing sneaks past you while you're on holiday.
- Activity journal — a plain-language log of what happened: devices joining, leaving, being approved, renewing their keys
- Device list — icon by device type, owner, OS, connected/last-seen state, expandable details with copy buttons for the MagicDNS name and Tailscale IP
- Cleanup list — devices not seen for 60+ days that should be removed
- Push alerts (optional) — ntfy and/or SMTP email, with sensible cooldowns so you are never spammed
- Monthly summary — one push/email per month: device count, what joined and left, pending updates, cleanup candidates
- Online history — SQLite log of every device's connectivity, powering a 30-day "online % of the time" stat per device
New to Cockpit plugins? A plugin is just a directory of static files under
/usr/share/cockpit/ — the install script below puts everything in place and
the page appears in Cockpit's left-hand menu after a browser reload. No
compilation, no services beyond a cron job.
Tailscale API ──> poller.py (cron, every 5 min) ──> SQLite (history + journal)
(OAuth, │
devices:read) ├──> /var/lib/cockpit-tailscale/snapshot.json
│ │ cockpit.file().watch() — live updates
│ ▼
│ Cockpit plugin page ("Tailscale" in the menu)
│ │ "Yes, that was me" ──> acks.json
│ ▼ (read by poller)
└──> alerts (ntfy / email / stdout) + monthly summary
poller.py— single-file Python, stdlib only. Fetches the device list, records online/offline observations, diffs against history to detect new and removed devices, keeps the event journal, and atomically writes a JSON snapshot.cockpit-tailscale/— the plugin: vanilla HTML/CSS/JS, no build step, no frameworks. Reads the snapshot through Cockpit's file API and re-renders live whenever the poller writes.
- Zero dependencies. Python stdlib + vanilla JS + Cockpit itself. One ~470-line file does all the backend work. Easy to audit.
- Minimal blast radius. The Tailscale OAuth client needs only the
devices:readscope — the plugin cannot change your tailnet. All administration stays in Tailscale's admin console, which the panel links to. - Cockpit-native. No web server, no new open port, no extra auth surface — the page rides on your Cockpit session.
- Polling, not webhooks — by design. Webhooks would require a publicly reachable endpoint; many home servers deliberately have none.
- Polling latency. New-device detection lags up to one poll interval
(default 5 min); "connected" is derived from
lastSeenand can lag a few minutes in both directions. - History starts at install. The rarely-online alert needs ~1 day of observations to arm and ~30 days to be meaningful. The first poll is a baseline — devices already present never trigger "new device".
- Household scale. Built for one tailnet with a handful of devices.
Owner names come from a manual config mapping, because the API's
userfield is stripped on tagged devices. Nothing paginates. - Approval warnings only fire if device approval is enabled in your tailnet.
- Young project. Used on one server so far; expect rough edges.
Requirements: Cockpit ≥ 264, Python 3.10+, a Tailscale account. Any Linux distribution that runs Cockpit should work (developed on Ubuntu Server 24.04).
git clone https://github.com/cgillinger/cockpit_tailscale.git
cd cockpit_tailscale
# 1. Create an OAuth client with ONLY the devices:read scope:
# https://login.tailscale.com/admin/settings/oauth
# 2. Configure (config.json is gitignored — it holds your secret):
cp config.example.json config.json
nano config.json # fill in oauth_client_id / oauth_client_secret
# 3. Install plugin + state dir + cron job, and run the first poll:
sudo bash scripts/install.shReload Cockpit in your browser — Tailscale appears in the menu.
Uninstall with sudo bash scripts/uninstall.sh (add --keep-data to keep
the history database).
| Key | Meaning |
|---|---|
tailnet |
- means the OAuth client's own tailnet |
owners |
device shortname → display name shown in the UI |
device_types |
shortname → server/nas/phone/tablet/computer icon override |
alerts.key_expiry_warn_days |
warn this many days before a device key expires (default 14) |
alerts.rare_online_threshold_pct |
"rarely online" threshold (default 5 %) |
alerts.stale_device_days |
cleanup-list threshold (default 60) |
alerts.ntfy |
enabled, url, topic — use a long secret topic on ntfy.sh |
alerts.email |
enabled plus SMTP host/port/user/password/from/to |
Alerts always log to the poller log even when both channels are disabled. Cooldowns: new device fires once ever, device removed / approval / key expiry every 3 days, rarely-online at most once per day. The monthly summary goes out on the first poll of each month.
- Generate a long random topic name — it works like a password:
echo "tailscale-$(openssl rand -hex 12)" - Put it in
config.jsonunderalerts.ntfywith"enabled": true(and store it in your password manager). - Install the ntfy app (iOS/Android), choose Subscribe to topic and enter the same topic name.
- Test it:
curl -d "Test" "https://ntfy.sh/YOUR-TOPIC?title=Tailscale"— the message should appear on your phone within seconds.
No account or server needed — ntfy.sh relays messages for free. Anyone who
knows the topic name can read your alerts, hence the long random name. For
full privacy, self-host ntfy or use the token field with a paid plan.
Everything lives in /var/lib/cockpit-tailscale/: tailscale.db (history +
event journal), snapshot.json (what the panel renders), acks.json
(your "Yes, that was me" confirmations) and poller.log. The acknowledgment
button writes acks.json through Cockpit with your session's privileges —
if you log in to Cockpit as a different user than the poller runs as, that
user needs write access to the state directory.
Outside Cockpit the page falls back to fetching data/snapshot.json, so the
whole chain runs locally without credentials. The mock device file uses
relative timestamps (-3d, +5d) resolved at load time:
python3 poller.py --config tests/dev_config.json --mock tests/sample_devices.json
cd cockpit-tailscale && python3 -m http.server 8000 # open http://localhost:8000ACL/policy editing, subnet routes, exit nodes, per-device traffic stats, or replacing the Tailscale admin console. Read-only by design.
I build a small family of Cockpit plugins for home servers, all dependency-light and made to be readable at a glance:
- cockpit-temps — hardware temperature history with thresholds
- cockpit-smart — S.M.A.R.T. disk health with trend tracking
- cockpit-pcloud — pCloud storage quota and backup folder status
- cockpit-tailscale — plain-language Tailscale network overview (this plugin)
Browse them all via the cockpit-plugin topic.

