# Headscale pilot on PD Self-hosted Headscale + Headplane pilot stack for replacing the Tailscale free-tier 3-user limit without doing a blind cutover. ## Why this exists - remove the 3-human-user ceiling from the current Tailscale free-tier setup - keep the control plane on PD, the long-term primary Docker host - prove a small household-safe access model before expanding scope - keep the repo copy as the source of truth for stack config and policy ## Pilot shape - Host: **PlausibleDeniability** - Live path: `/mnt/docker-ssd/docker/compose/headscale` - Services: - `headscale` - `headplane` - Data paths: - `/mnt/docker-ssd/docker/appdata/headscale` - `/mnt/docker-ssd/docker/appdata/headplane` - No OIDC on day one - No subnet-router or exit-node rollout on day one - No public Pangolin exposure on day one ## URLs for the pilot - Headscale control plane: `http://headscale.home.paccoco.com:8084` - Headplane UI: `http://headplane.home.paccoco.com:3005/admin` These are the intended LAN/private-DNS endpoints for the pilot. If you later want Traefik/Pangolin exposure, use `examples/traefik-routes.yml` as a starting point instead of changing the pilot shape first. ### Current live pilot note The live PD pilot is currently using direct-IP URLs instead: - Headscale: `http://10.5.30.6:8084` - Headplane: `http://10.5.30.6:3005/admin` Reason: the `home.paccoco.com` records for these pilot names have not yet been added in Technitium, and the current Technitium admin flow is 2FA-gated. Once those DNS records exist, flip the live stack back to the hostname-based URLs above. ## Initial users - `fizzlepoof` - `manndra` Policy references them as: - `fizzlepoof@` - `manndra@` ## Initial tags - `tag:infra` - `tag:apps` - `tag:admin` ## Access model - `fizzlepoof@` = full pilot admin access to `tag:infra`, `tag:apps`, `tag:admin`, and his own devices - `manndra@` = app access only via `tag:apps` plus her own devices - no limited-user tier yet - no routed-LAN access yet ## Files - `docker-compose.yaml` - `.env.example` - `config/headscale/config.yaml` - `config/headscale/policy.hujson` - `config/headplane/config.yaml` - `examples/traefik-routes.yml` ## Pre-deploy edits Before first deploy, replace the placeholder values in: - `.env` copied from `.env.example` - `config/headplane/config.yaml` Required changes: - replace `CHANGE_ME_TO_EXACTLY_32_CHARS` with a real 32-character cookie secret - confirm the hostnames/IP-backed DNS records exist in Technitium - if you want different ports or hostnames, update both the config files and `.env` ## Deploy on PD 1. Sync this directory into the live compose tree: - `/mnt/docker-ssd/docker/compose/headscale` 2. Copy `.env.example` to `.env` 3. Create persistent appdata paths: - `/mnt/docker-ssd/docker/appdata/headscale` - `/mnt/docker-ssd/docker/appdata/headscale/run` - `/mnt/docker-ssd/docker/appdata/headplane` 4. Validate: ```bash cd /mnt/docker-ssd/docker/compose/headscale docker compose --env-file .env config ``` 5. Bring up Headscale first: ```bash docker compose --env-file .env up -d headscale docker logs headscale --tail=100 ``` 6. Create pilot users: ```bash docker exec -it headscale headscale users create fizzlepoof docker exec -it headscale headscale users create manndra docker exec -it headscale headscale users list ``` 7. Generate a Headscale API key for Headplane login: ```bash docker exec -it headscale headscale apikeys create --expiration 90d ``` Save the returned key somewhere secure. Headplane uses it for the initial admin login. 8. Enroll John's admin device. 9. Bring up Headplane: ```bash docker compose --env-file .env up -d headplane docker logs headplane --tail=100 ``` 10. Log in to Headplane with the API key from step 7. 11. Register one tagged service node. 12. Enroll one Manndra device. ## Policy reload After editing `config/headscale/policy.hujson`: ```bash docker exec -it headscale kill -HUP 1 ``` Then inspect the container logs for policy parse results. ## Owner-facing Headplane pilot checklist This section is for John as the stack owner using the Web UI. Doris should handle the CLI and backend admin work; this checklist is about what John should look for in Headplane and when to escalate. ### Where to go - Open: `http://headplane.home.paccoco.com:3005/admin` - Log in with the temporary Headscale API key Doris generated for the pilot ### What you should expect to see first After initial pilot bring-up, Headplane should show: - two users: - `fizzlepoof` - `manndra` - John's enrolled device(s) - one tagged pilot service node - a generally healthy/online control plane with no obvious UI errors If any of those are missing, that is a Doris problem to fix, not a John problem to debug. ### UI-first smoke test #### 1. Confirm the users exist Look for: - `fizzlepoof` - `manndra` Good: - both users appear once Bad / message Doris if: - a user is missing - a duplicate or unexpected user exists - user ownership looks wrong #### 2. Confirm John's device is present Look for at least one clearly identifiable John-owned device. Good: - device shows as online - device is owned by `fizzlepoof` - the name is recognizable enough to tell what it is Bad / message Doris if: - device is offline when it should be online - device is attached to the wrong user - device appears more than once unexpectedly #### 3. Confirm Manndra's device is present Good: - device is owned by `manndra` - it appears separately from John's devices Bad / message Doris if: - it shows under the wrong user - it never appears after enrollment - it appears to have broad access it should not have #### 4. Confirm the tagged node looks like a service node Look for one node tagged as a pilot service node, typically with `tag:apps` or `tag:infra`. Good: - the node is visibly tagged - it is not owned like a normal human personal device - it is online when the underlying service is online Bad / message Doris if: - the node is untagged - it appears owned like a personal device when it should be infra - it has the wrong tag #### 5. Confirm the UI is usable enough to keep Good: - pages load consistently - node/user details are understandable - no obvious blank/error screens - refreshing does not randomly lose state Bad / message Doris if: - login repeatedly fails with a known-good API key - pages partially load or spin forever - the UI looks disconnected from reality - the UI feels too broken to trust for visibility ### Expected access behavior John does not need to test raw ACL syntax. The practical expectations are: - `fizzlepoof` should have broad pilot visibility/access - `manndra` should be limited to app-level access - Manndra should **not** have broad admin/infra access by default If Manndra can reach something that feels like core infra/admin, treat that as a problem and tell Doris. ### What counts as pilot success from John's side The pilot is good enough to continue if: - Headplane reliably loads - the users/nodes make sense at a glance - John's devices are visible - Manndra's device is visible and separated correctly - the tagged service node is visible and clearly infra-like - nothing suggests accidental overexposure ### When John should call Doris instead of poking at it Call Doris if: - login stops working - users disappear or duplicate - devices are attached to the wrong owner - a tagged node loses its tag or looks wrong - Manndra appears to have too much access - the UI starts showing stale, contradictory, or obviously broken state The point of the Web UI is visibility and confidence, not pushing John into VPN-control-plane babysitting. ## What to validate before expanding - Headscale stays healthy - Headplane is usable enough to justify keeping it - John can reach `tag:infra`, `tag:admin`, and `tag:apps` - Manndra can reach `tag:apps` but not `tag:infra` or `tag:admin` - one tagged service node registers cleanly and behaves as expected - existing Tailscale remains intact as rollback during the pilot ## Notes on Headplane integration This stack enables Headplane's Docker integration so it can identify the Headscale container via the label: - `me.tale.headplane.target=headscale` It also mounts the tracked Headscale config into Headplane so the UI can inspect and, if you later choose, manage more than just node registration. ## Not done here on purpose - no OIDC wiring yet - no DERP customization yet - no public auth/proxy routing yet - no explicit subnet-router policy yet - no migration of the existing infrastructure stack's `tailscale` container yet Pilot first. Cutover later.