Files
automations/deployments/headscale/README.md
T
57_Wolve 1ca79938cd feat(headscale): add headplane web UI at /admin
Integrate headplane (ghcr.io/tale/headplane) into the headscale stack, served
by Caddy at /admin. API-only (no Docker socket); deploy.sh mints a headscale
API key on first run, generates headplane.yaml, and wires optional OIDC login
via pocket-id (second client, /admin/oidc/callback). Adds HEADPLANE_* env,
compose service, Caddy routing; rebuild embedded archive.
2026-06-12 16:15:34 -05:00

89 lines
3.1 KiB
Markdown

# headscale
[Headscale](https://headscale.net) — a self-hosted Tailscale control server —
behind Caddy, with OIDC login delegated to [pocket-id](../pocket-id/).
## Prerequisite
Register an OIDC client in pocket-id's admin UI with redirect URI:
```
https://${HEADSCALE_DOMAIN}/oidc/callback
```
Then paste its `OIDC_CLIENT_ID` / `OIDC_CLIENT_SECRET` below.
## Required `.env` values
| Variable | Notes |
|----------|-------|
| `HEADSCALE_DOMAIN` | Public hostname (e.g. `hs.example.com`). Clients connect here over HTTPS. |
| `ACME_EMAIL` | Let's Encrypt registration email. |
| `TAILNET_DOMAIN` | MagicDNS suffix (e.g. `tail.example.com`). Must differ from `HEADSCALE_DOMAIN`. |
| `POCKETID_DOMAIN` | OIDC issuer hostname (your pocket-id). |
| `OIDC_CLIENT_ID` / `OIDC_CLIENT_SECRET` | From the pocket-id client above. |
The deploy substitutes these into `config.yaml` from the embedded template. See
[`.env.example`](.env.example).
## Deploy
```bash
./automations.sh # Deploy on this host → deploy: headscale
```
Or build + run the self-contained artifact:
```bash
./build.sh
scp deploy.sh root@host:
ssh root@host 'bash deploy.sh'
# non-interactive: pass HEADSCALE_DOMAIN, ACME_EMAIL, TAILNET_DOMAIN,
# POCKETID_DOMAIN, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, SKIP_PROMPTS=1
```
Unattended provisioning: [`cloud-init.yml`](cloud-init.yml).
DNS for `HEADSCALE_DOMAIN` must resolve to the host and 80/443 be reachable
before deploy.
## Headplane (web UI)
[Headplane](https://github.com/tale/headplane) ships in the stack, served by
Caddy at **`https://HEADSCALE_DOMAIN/admin`**. It runs **API-only** — it talks
to the headscale API with a key and does *not* get the Docker socket, so it
can't control the host. `deploy.sh` mints the headscale API key automatically
on first deploy (stored in `.env`) and generates `headplane.yaml`.
**Login:** to use OIDC via pocket-id, register a **second** OIDC client in
pocket-id with redirect URI:
```
https://HEADSCALE_DOMAIN/admin/oidc/callback
```
and pass `HEADPLANE_OIDC_CLIENT_ID` / `HEADPLANE_OIDC_CLIENT_SECRET` (prompted
by the launcher, or set in `.env`). Leave them blank to use headplane's
API-key login instead. Pin the image with `HEADPLANE_TAG` (defaults to
`latest`).
> headplane integration is version-sensitive; if the UI doesn't come up,
> check `docker compose logs headplane` and verify `headplane.yaml` /
> the minted `HEADPLANE_HS_API_KEY` in `.env`.
## ACL policy
A starter ACL ships in [`policy.hujson`](policy.hujson) (installed to
`$STACK_DIR/policy.hujson` on first deploy; your edits survive re-deploys).
**headscale uses Tailscale's legacy `acls` format, not the newer `grants`
syntax** from the Tailscale admin console — the bundled policy is that default
translated over (per-user self-access, a `tag:shared` everyone can reach, and
SSH to your own devices). Some pieces (`autogroup:self`, the `ssh` block) are
newer/experimental in headscale; a permissive fallback is included in the file.
```bash
$EDITOR /srv/headscale/policy.hujson
headscale policy check # validate (host CLI wrapper)
cd /srv/headscale && docker compose restart headscale # apply
```