diff --git a/Caddyfile b/Caddyfile index d3b3663..726bf9e 100644 --- a/Caddyfile +++ b/Caddyfile @@ -17,7 +17,3 @@ git.waldson.com.br { push.waldson.com.br { reverse_proxy ntfy-ntfy-1:8080 } - -remote.waldson.com.br { - reverse_proxy headscale-headscale-1:8080 -} diff --git a/README.md b/README.md index ad3fecb..7e61355 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,6 @@ forgejo/ the Forgejo stack (own compose file); web UI behind Caddy, git-over-SSH published on 2222 ntfy/ the ntfy stack (own compose file); push server behind Caddy, nothing published (pure HTTP through the proxy) -headscale/ the Headscale stack (own compose file); control plane behind - Caddy, STUN for the embedded DERP relay published on 3478/udp ``` ## Prerequisites @@ -314,113 +312,6 @@ docker compose pull && docker compose up -d A major bump is a deliberate tag edit; read the ntfy release notes first. -## Headscale stack - -A self-hosted [Headscale](https://headscale.net) coordination server (the -open-source Tailscale control plane) for the owner's mesh VPN (Virtual Private -Network) at `remote.waldson.com.br`. It lives in `headscale/` and deploys to -`/home/waldson/central-proxy/headscale` on the droplet. Every device (mercurio, -hermes, quicksilver, the phone, the Home Assistant box) joins as a stock -Tailscale client pointed at this login URL, so remote access to the home LAN -never depends on Tailscale Inc.'s servers. - -The control plane and login endpoints ride Caddy's existing 80/443: the central -`Caddyfile` carries the `remote.waldson.com.br` block reverse-proxying to -`headscale-headscale-1:8080`, and Caddy transparently upgrades the long-lived -client control connections (the same handling ntfy relies on). The one thing -published on the host is **STUN on `3478/udp`** for the embedded DERP -(Designated Encrypted Relay for Packets) server; the relay itself rides Caddy's -443. The whole posture (server URL, MagicDNS, embedded DERP) is a committed -`headscale/config.yaml`, so it is reproduced on every recreate rather than -depending on runtime state. - -The embedded DERP relay replaces Tailscale Inc.'s DERP map entirely -(`derp.urls: []`), so even traffic that cannot go peer-to-peer is relayed -through this droplet. MagicDNS is enabled with base domain `ts.waldson.com.br` -(deliberately different from the login hostname, which Headscale requires, and -never published to public DNS), so devices reach each other by name -(`ssh mercurio`). The gRPC remote-CLI port is not exposed; administration is -CLI-only via `docker exec ... headscale`, like every other stack. - -### Where data lives - -The SQLite database (`db.sqlite`), the auto-generated Noise and DERP private -keys, and the CLI unix socket all live under the host bind mount `headscale/data` -(mounted at `/var/lib/headscale` in the container), so state - the node list and -keys - survives container recreation and is inspectable over plain SSH. The -directory is git-ignored via the repo's `data/` rule. The container runs as -`1000:1000`; if the droplet's deploy user has a different id, adjust `user:` in -`compose.yaml` so the state files stay owned by that user. Headscale's default -unix socket lives under root-only `/var/run`, so `config.yaml` relocates it into -this data dir to keep the non-root container's CLI working. - -### Deploy - -From `headscale/` on the droplet: - -```sh -docker compose up -d -``` - -The container comes up as `headscale-headscale-1` on `proxy_net`, which is the -name Caddy reverse-proxies to. Recreating it preserves the node list and keys: - -```sh -docker compose up -d --force-recreate -``` - -### First-run setup (CLI) - -There is no web sign-up. All devices register under a single Headscale user, -created once via the CLI after the container is first up: - -```sh -docker exec headscale-headscale-1 headscale users create waldson -docker exec headscale-headscale-1 headscale users list -``` - -### Enrolling a device - -Registration is manual one-time approval per device (no pre-shared keys). On the -client, point Tailscale at this login server: - -```sh -tailscale up --login-server https://remote.waldson.com.br -``` - -The client prints a registration URL ending in an auth id; approve the node -under the user on the droplet with that id: - -```sh -docker exec headscale-headscale-1 \ - headscale auth register --user waldson --auth-id -docker exec headscale-headscale-1 headscale nodes list -``` - -Subnet routes advertised by a node (e.g. the Home Assistant box and mercurio -advertising the home LAN) are approved per node: - -```sh -docker exec headscale-headscale-1 headscale nodes approve-routes --identifier --routes -``` - -### Upgrade - -The image is pinned to the current Headscale minor (`headscale/headscale:v0.29`). -Because Headscale is pre-1.0, the minor is the breaking-change axis; patch -updates within it are safe: - -```sh -docker compose pull && docker compose up -d -``` - -A minor bump (editing the tag to `v0.30`) can change the config schema and may -run database migrations: diff `config.yaml` against that release's -`config-example.yaml`, back up `headscale/data` first, and read the release -notes. Headscale also trails the newest Tailscale client capabilities, so review -client compatibility when bumping - if a client refuses to connect after an -upgrade, version skew is the first suspect. - ## Droplet groundwork Baseline host configuration for the droplet (`wloud`, 174.138.36.232, @@ -461,19 +352,15 @@ firewall blocks them. Ports in use: | 80, 443 | tcp | Caddy (all HTTP/HTTPS traffic) | | 2222 | tcp | Forgejo git-over-SSH | | 22000 | tcp+udp | Syncthing sync protocol | -| 3478 | udp | Headscale STUN (embedded DERP) | -ntfy adds no row here: it is reached only through Caddy on 80/443. Headscale is -also fronted by Caddy on 80/443, but its embedded DERP relay needs STUN on -3478/udp for NAT traversal, which is the one row it adds. If ufw rules are ever -tightened, keep the rows above allowed. To re-verify reachability from any -machine outside the droplet: +ntfy adds no row here: it is reached only through Caddy on 80/443. If ufw +rules are ever tightened, keep the four rows above allowed. To +re-verify reachability from any machine outside the droplet: ```sh nc -vz 174.138.36.232 2222 nc -vz 174.138.36.232 22000 nc -vzu 174.138.36.232 22000 -nc -vzu 174.138.36.232 3478 ``` "Connection refused" means the port is reachable but nothing listens yet; @@ -491,14 +378,12 @@ and point at the droplet: | git.waldson.com.br | A | 174.138.36.232 | Forgejo | | sync.waldson.com.br| A | 174.138.36.232 | Syncthing GUI | | push.waldson.com.br| A | 174.138.36.232 | ntfy push server | -| remote.waldson.com.br | A | 174.138.36.232 | Headscale control plane | personaledson.com.br and nazareonline.com.br also resolve here for the -existing stacks. `ts.waldson.com.br` (the MagicDNS base domain) deliberately has -**no** public record: it exists only inside the tailnet. Check resolution with: +existing stacks. Check resolution with: ```sh -dig +short waldson.com.br www.waldson.com.br git.waldson.com.br sync.waldson.com.br remote.waldson.com.br +dig +short waldson.com.br www.waldson.com.br git.waldson.com.br sync.waldson.com.br ``` ## Notes diff --git a/headscale/compose.yaml b/headscale/compose.yaml deleted file mode 100644 index f4629f6..0000000 --- a/headscale/compose.yaml +++ /dev/null @@ -1,50 +0,0 @@ -name: headscale - -# Self-hosted Headscale coordination server (the open-source Tailscale control -# plane) for the owner's tailnet, behind the central Caddy proxy at -# remote.waldson.com.br. The HTTP API and the login/OIDC endpoints are reached -# only through Caddy on proxy_net; Caddy transparently upgrades the long-lived -# client control connections, the same as ntfy. The one thing published to the -# host is STUN on 3478/udp for the embedded DERP relay. Administration is -# CLI-only via `docker exec ... headscale` (see README.md for the deploy, -# upgrade, and user/node runbook). The whole posture lives in the committed -# config.yaml; only SQLite state and generated keys are mutable at runtime. - -services: - headscale: - # Pinned to the current minor series. Headscale is pre-1.0, so the minor is - # the breaking-change axis: `pull` brings 0.29.x patches while a minor bump - # (0.30) stays a deliberate tag edit. Headscale trails new Tailscale client - # capabilities, so review compatibility before bumping the tag - if a client - # refuses to connect after an upgrade, version skew is the first suspect. - image: headscale/headscale:v0.29 - # The image entrypoint is `headscale`; the server needs the `serve` - # subcommand (same shape as ntfy's `serve`). - command: serve - restart: unless-stopped - # Run as the droplet's deploy user so the SQLite db and generated private - # keys in the bind mount stay owned by 1000 and are manageable over plain - # SSH, matching the Syncthing and ntfy stacks. Headscale's default unix - # socket lives under root-only /var/run; config.yaml relocates it into the - # data dir so this non-root process can create it and the CLI still works. - user: "1000:1000" - volumes: - # Committed posture: server URL, MagicDNS, embedded DERP. Read-only. - - ./config.yaml:/etc/headscale/config.yaml:ro - # SQLite database, noise/DERP private keys, and the CLI socket live here on - # a host bind mount, so the instance survives recreation and is - # inspectable over SSH. Git-ignored via the repo's data/ rule. - - ./data:/var/lib/headscale - ports: - # STUN for the embedded DERP relay's NAT traversal. The relay itself and - # every control connection ride Caddy's existing 443; only this UDP port - # is new on the host firewall. The gRPC remote-CLI port is deliberately - # not published - admin is via docker exec over the unix socket. - - "3478:3478/udp" - mem_limit: 256m - networks: - - proxy_net - -networks: - proxy_net: - external: true diff --git a/headscale/config.yaml b/headscale/config.yaml deleted file mode 100644 index 57441d1..0000000 --- a/headscale/config.yaml +++ /dev/null @@ -1,146 +0,0 @@ -# Headscale coordination server for the owner's self-hosted tailnet, reached at -# https://remote.waldson.com.br through the central Caddy proxy. This file is -# the committed posture: MagicDNS, the embedded DERP relay, and the SQLite data -# layout are all reproduced on every recreate instead of depending on runtime -# state. Only the SQLite database and the auto-generated private keys (under -# /var/lib/headscale, the git-ignored bind mount) are mutable at runtime. -# -# Schema tracks the pinned image (headscale/headscale:v0.29). When bumping the -# minor tag, diff this against that release's config-example.yaml: Headscale's -# config schema changes between minors. - -# Public login URL every client connects to. Caddy terminates TLS for this -# hostname and reverse-proxies to listen_addr below. DERP requires https here. -server_url: https://remote.waldson.com.br - -# Bind on all interfaces inside the container so Caddy can reach it by name on -# proxy_net (headscale-headscale-1:8080). The port is never published to the -# host; only Caddy on 80/443 fronts it, the same shape as the other stacks. -listen_addr: 0.0.0.0:8080 - -# Metrics and gRPC stay bound to loopback inside the container and are never -# published. Administration is done with `docker exec ... headscale` over the -# unix socket below, so the gRPC remote-CLI surface is not exposed. -metrics_listen_addr: 127.0.0.1:9090 -grpc_listen_addr: 127.0.0.1:50443 -grpc_allow_insecure: false - -# Caddy is the only thing in front of Headscale; trust its forwarding headers so -# logs and rate limiting see the real client IP. Caddy reaches Headscale from -# its own container on proxy_net, so trust that private range. -trusted_proxies: - - 172.16.0.0/12 - -noise: - private_key_path: /var/lib/headscale/noise_private.key - -# Tailnet address pools. Left at the standard Tailscale CGNAT/ULA ranges. -prefixes: - v4: 100.64.0.0/10 - v6: fd7a:115c:a1e0::/48 - allocation: sequential - -# Embedded DERP relay: run our own relay and STUN so even traffic that cannot go -# direct is relayed through this droplet, never through Tailscale Inc. The -# upstream DERP map is dropped (urls: [] and no auto-update) so the tailnet uses -# only this region. -derp: - server: - enabled: true - region_id: 999 - region_code: "waldson" - region_name: "Waldson Embedded DERP" - verify_clients: true - # STUN for NAT traversal. Published on the host as 3478/udp (the one new - # firewall row this stack adds); mandatory when the embedded DERP is on. - stun_listen_addr: "0.0.0.0:3478" - private_key_path: /var/lib/headscale/derp_server_private.key - automatically_add_embedded_derp_region: true - # Droplet public IPv4 so clients can reach this DERP region directly for - # better connection stability. No IPv6 line: the droplet has no stable - # public IPv6, and advertising a bogus one would send clients to a dead - # address. - ipv4: 174.138.36.232 - # No external DERP maps: relayed traffic must stay on our own infrastructure. - urls: [] - paths: [] - auto_update_enabled: false - update_frequency: 3h - -disable_check_updates: false - -node: - expiry: 0 - ephemeral: - inactivity_timeout: 30m - # HA subnet-router health probing. Kept on so that when the Home Assistant - # box (primary) and mercurio (failover) both advertise 192.168.0.0/24, the - # primary role fails over to whichever is healthy. This is the route-failover - # support the tailnet topology depends on. - routes: - ha: - probe_interval: 10s - probe_timeout: 5s - -database: - type: sqlite - debug: false - gorm: - prepare_stmt: true - parameterized_queries: true - skip_err_record_not_found: true - slow_threshold: 1000 - sqlite: - path: /var/lib/headscale/db.sqlite - write_ahead_log: true - wal_autocheckpoint: 1000 - -# TLS is terminated by Caddy, so Headscale serves plain HTTP on listen_addr and -# manages no certificates of its own. -tls_letsencrypt_hostname: "" -tls_letsencrypt_challenge_type: HTTP-01 -tls_cert_path: "" -tls_key_path: "" - -log: - level: info - format: text - -policy: - # Allow-all within the tailnet: single-user fleet, the tailnet boundary is the - # security boundary. An empty path keeps the built-in allow-all default. - mode: file - path: "" - -# MagicDNS so devices reach each other by name (ssh mercurio). The base domain -# is deliberately different from server_url's host, which Headscale requires, -# and exists only inside the tailnet (no public records for it). -dns: - magic_dns: true - base_domain: ts.waldson.com.br - override_local_dns: true - nameservers: - global: - - 1.1.1.1 - - 1.0.0.1 - - 2606:4700:4700::1111 - - 2606:4700:4700::1001 - split: {} - search_domains: [] - extra_records: [] - -# CLI connects over this unix socket with no auth. Relocated from the default -# /var/run/headscale (root-owned, not writable by our non-root container) into -# the bind-mounted data dir, so the UID 1000 process can create it and -# `docker exec ... headscale` works. -unix_socket: /var/lib/headscale/headscale.sock -unix_socket_permission: "0770" - -logtail: - enabled: false - -taildrop: - enabled: true - -auto_update: - enabled: false