Files
automations/deployments/headscale/README.md
T
57_Wolve 6a3fc68b75 fix(headscale): valid default ACL + document /admin gating
headscale rejects autogroups as tagOwners (only user/group:/tag:), which made
the shipped policy fatal. Replace with a valid allow-all default plus correct
commented examples for tightening. Document gating /admin to a pocket-id
superuser group via the headplane client's Allowed User Groups. Rebuild archive.
2026-06-12 16:17:26 -05:00

95 lines
3.4 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`).
**Gate `/admin` to a superuser group:** restrict it at pocket-id — edit the
headplane OIDC client and set its **Allowed User Groups** to your superuser
group. Only members of that group can complete the OIDC login, so only they can
reach `/admin`. (Enforced at the IdP, so it covers the UI regardless of
headplane's own checks.)
> 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
```