Tailscale (optional)
From Heimdall v0.4.36 · View source
Status: built, off by default, and tested against a local fake of the Tailscale API (the documented OAuth token exchange and device listing). Run
sudo heimdall tailscale syncfirst; it prints exactly what it found.
If your team reaches servers over a Tailscale tailnet, Heimdall can ask the Tailscale API which person and device each tailnet address belongs to. A privilege session from 100.101.102.103 then shows alice@example.com’s laptop-01, not just “Tailnet”.
It also tells you which of your Heimdall hosts are on the tailnet, which helps before you allow SSH only from the tailnet: Hosts → Not on tailnet lists the ones that aren’t yet.
It’s read only. Heimdall lists the tailnet’s devices and never changes anything in Tailscale: no devices, keys, tags or policy.
Set it up
Section titled “Set it up”-
Create an OAuth client (recommended: it doesn’t expire).
sudo heimdall tailscale set-keyprints the link (https://login.tailscale.com/admin/settings/oauth). In the Tailscale admin console go to Settings → Trust credentials (formerly OAuth clients), choose OAuth, and give it only the Devices → Core → Read scope (devices:core:read). Read-only scopes don’t need tags. Copy the client ID and the secret (tskey-client-…); the secret is shown once.An API access token (
tskey-api-…, from Settings → Keys) also works, but it has your full admin rights and expires after 1 to 90 days.heimdall tailscale statuswarns two weeks before it expires. -
Turn it on in
heimdall.toml(sudo heimdall config updateadds the section, off, to an older file):[tailscale]enabled = truetailnet = "-" # "-" = the credential's own tailnet; or its ID or name, e.g. "example.com"sync_every = "15m" # 1m to 24h -
Store the credential. It never goes in the config file:
Terminal window sudo heimdall tailscale set-key # prompts for the secret (echo off), then the client IDsudo heimdall tailscale set-key --client-id kXXXX --stdin < secret.txt # or pipe itThis writes
/etc/heimdall/tailscale-key(owner-only, atomically, likeai set-key) and addsLoadCredential=heimdall-tailscale:/etc/heimdall/tailscale-keyto the hub’s unit, so the hub reads it as a private systemd credential. Outside systemd, setHEIMDALL_TAILSCALE_KEY_FILEto the file instead. -
Check it, then restart:
Terminal window sudo heimdall tailscale sync # one sync now: device, label and host counts; never the secretsudo heimdall restartheimdall tailscale status # credential kind, last sync, hosts not on the tailnet, warnings
The hub syncs 30 seconds after it starts and then every sync_every.
What’s synced and stored
Section titled “What’s synced and stored”Heimdall calls two endpoints on https://api.tailscale.com/api/v2 (its only new outbound host; allow it if you firewall the hub):
| Call | What for |
|---|---|
POST /oauth/token (grant_type=client_credentials) |
OAuth clients only: a one-hour access token, cached and renewed as needed |
GET /tailnet/{tailnet}/devices?fields=default |
the device list (needs devices:core:read) |
GET /tailnet/{tailnet}/keys/{keyId} |
API access tokens only: when the token expires (best effort) |
For each device it stores its ID, MagicDNS name, hostname, Tailscale addresses, owner’s login (none for tagged devices), tags, OS, client version and whether an update is available, whether it’s connected, when it was last seen, and its node key expiry (table tailscale_devices, replaced on each sync). It doesn’t store routes, posture data, machine or node keys.
Each address also becomes an IP label (source tailscale, every org) that the rest of Heimdall uses to name addresses:
- a person’s device: kind staff, person = the owner’s login, name = the device’s hostname
- a tagged device (e.g.
tag:server), or a device that is one of your Heimdall hosts: kind infra - the note says it’s from Tailscale, with its tags, OS, and online or last seen
If a sync fails (network, revoked credential), the last good devices and labels stay, and tailscale status shows the error.
Host page
Section titled “Host page”Hosts get a Tailnet card, just above Privileged access, when Tailscale is on and has synced:
- the device’s name (MagicDNS name on hover) and owner, its tailnet addresses
- online, or when it was last seen; tags; Tailscale client version, and “client update available”
- node key expires … within two weeks, or node key expired (disable key expiry for servers in the admin console)
- or Not on the tailnet, when no device matches
A host matches a device when its reports come from one of the device’s addresses, or else when its name matches the device’s MagicDNS name, then its hostname (ignoring case and any domain: web-01.example.com matches web-01.tail1234.ts.net). If two devices share a hostname, the most recently seen one wins.
Stopped reporting, still online
Section titled “Stopped reporting, still online”A switched-off collector is the classic sign of tampering, but on its own it looks the same as a server that’s down. Tailscale tells them apart: when a host has been quiet for hosts.alert_after (or stale_after) and its tailnet device was still online, or seen online well after the host’s last report, Heimdall raises silent_online (high). The machine is up, so something stopped or blocked its collector. A server that’s off or unplugged leaves the tailnet too, and raises only the usual silence.
It needs a Tailscale sync less than an hour old, so a stale sync never raises it. Maintenance windows and suppressions apply, and it’s raised once per silence. See anomaly signals.
Troubleshooting
Section titled “Troubleshooting”- HTTP 401, “the credential was refused”: the secret was mistyped, revoked, or (an API access token) expired. Store a new one with
sudo heimdall tailscale set-key, thensudo heimdall restart. - HTTP 403: the OAuth client lacks the
devices:core:readscope. Edit it in the admin console, or create a new one. - HTTP 404, “tailnet not found”:
tailnetin[tailscale]is wrong. Use"-"(the credential’s own tailnet). - “Tailscale sync is enabled but has no credential” in the hub’s log: the unit has no
LoadCredential=heimdall-tailscale:…line. Re-runset-key, or add it yourself, then restart. - A host shows “Not on the tailnet” but is: its Heimdall name and its tailnet hostname differ, and it reports over the public internet. Rename one to match, or have the collector report over the tailnet so the report address matches.
auth keyrefused by set-key:tskey-auth-…keys are for adding devices, not for the API. Create an OAuth client as above.

