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.
89 lines
3.1 KiB
Markdown
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
|
|
```
|