diff --git a/deployments/headscale/README.md b/deployments/headscale/README.md index 09c5725..5b32fca 100644 --- a/deployments/headscale/README.md +++ b/deployments/headscale/README.md @@ -46,3 +46,19 @@ Unattended provisioning: [`cloud-init.yml`](cloud-init.yml). DNS for `HEADSCALE_DOMAIN` must resolve to the host and 80/443 be reachable before deploy. + +## 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 +``` diff --git a/deployments/headscale/build.sh b/deployments/headscale/build.sh index 4074c7c..5416126 100644 --- a/deployments/headscale/build.sh +++ b/deployments/headscale/build.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash # -# build.sh -- (re)embed docker-compose.yml, Caddyfile, config.yaml, and -# .env.example into deploy.sh as a base64-encoded tar.gz payload after the -# __ARCHIVE_BELOW__ marker. Idempotent: strips any existing payload first. +# build.sh -- (re)embed docker-compose.yml, Caddyfile, config.yaml, +# policy.hujson and .env.example into deploy.sh as a base64-encoded tar.gz +# payload after the __ARCHIVE_BELOW__ marker. Idempotent: strips first. # # Run after editing any of the loose files. The resulting deploy.sh is # self-contained and can be scp'd to the target box on its own. @@ -14,12 +14,12 @@ SCRIPT="$DIR/deploy.sh" MARKER="__ARCHIVE_BELOW__" [[ -f "$SCRIPT" ]] || { echo "deploy.sh not found at $SCRIPT" >&2; exit 1; } -for f in docker-compose.yml Caddyfile config.yaml .env.example; do +for f in docker-compose.yml Caddyfile config.yaml policy.hujson .env.example; do [[ -f "$DIR/$f" ]] || { echo "Missing $DIR/$f" >&2; exit 1; } done PAYLOAD=$(tar -czf - -C "$DIR" \ - docker-compose.yml Caddyfile config.yaml .env.example | base64) + docker-compose.yml Caddyfile config.yaml policy.hujson .env.example | base64) TMP=$(mktemp) trap 'rm -f "$TMP"' EXIT diff --git a/deployments/headscale/config.yaml b/deployments/headscale/config.yaml index 7c30c12..fe64c73 100644 --- a/deployments/headscale/config.yaml +++ b/deployments/headscale/config.yaml @@ -44,6 +44,12 @@ log: level: info format: text +# ACL policy, loaded from a HuJSON file (see policy.hujson, mounted into the +# container). headscale uses Tailscale's legacy "acls" format, not "grants". +policy: + mode: file + path: /etc/headscale/policy.hujson + # Magic DNS for the tailnet. base_domain becomes the suffix for node # DNS names (e.g. laptop.__TAILNET_DOMAIN__). dns: diff --git a/deployments/headscale/deploy.sh b/deployments/headscale/deploy.sh index beea08a..8f4200e 100644 --- a/deployments/headscale/deploy.sh +++ b/deployments/headscale/deploy.sh @@ -22,9 +22,9 @@ # regenerates config.yaml from the current .env so edits to .env propagate # (but secrets in .env are never touched once seeded). # -# Self-contained: docker-compose.yml, Caddyfile, config.yaml, .env.example -# are embedded as a base64-encoded tar.gz at the bottom of this file. -# Rebuild with build.sh after editing the loose source files. +# Self-contained: docker-compose.yml, Caddyfile, config.yaml, policy.hujson +# and .env.example are embedded as a base64-encoded tar.gz at the bottom of +# this file. Rebuild with build.sh after editing the loose source files. # # Usage: # bash deploy.sh # interactive prompts @@ -130,7 +130,7 @@ else die "No embedded archive found. Run build.sh to embed deployment files." fi -for f in docker-compose.yml Caddyfile config.yaml .env.example; do +for f in docker-compose.yml Caddyfile config.yaml policy.hujson .env.example; do [[ -f "$SCRIPT_DIR/$f" ]] || die "Embedded archive missing $f" done @@ -213,6 +213,16 @@ set -a; . "$ENV_FILE"; set +a log "Generating config.yaml from template..." install -m 0600 "$SCRIPT_DIR/config.yaml" "$STACK_DIR/config.yaml" +# ACL policy: install the default on first run only, so your edits survive +# re-deploys. headscale reads it on start (policy mode = file); after editing, +# validate with `headscale policy check` and `docker compose restart headscale`. +if [[ ! -f "$STACK_DIR/policy.hujson" ]]; then + log "Installing default ACL policy -> $STACK_DIR/policy.hujson" + install -m 0640 "$SCRIPT_DIR/policy.hujson" "$STACK_DIR/policy.hujson" +else + log "ACL policy exists; leaving $STACK_DIR/policy.hujson alone." +fi + # Escape replacement values for sed (handle slashes/&). Headscale URLs and # OIDC secrets can contain those. sed_escape() { printf '%s' "$1" | sed -e 's/[\/&]/\\&/g'; } @@ -301,6 +311,10 @@ Stack dir: ${STACK_DIR} OIDC redirect/callback URI -- register THIS in your pocket-id client: https://${HEADSCALE_DOMAIN}/oidc/callback +ACL policy: ${STACK_DIR}/policy.hujson + headscale policy check # validate after editing + cd ${STACK_DIR} && docker compose restart headscale # apply changes + Headscale CLI -- use the installed wrapper from anywhere: headscale users create alice headscale users list @@ -327,58 +341,75 @@ EOF # __ARCHIVE_BELOW__ marker is the embedded tar.gz payload (base64). exit 0 __ARCHIVE_BELOW__ -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 +ea3kewBQAAA= diff --git a/deployments/headscale/docker-compose.yml b/deployments/headscale/docker-compose.yml index 0b58393..8f463d1 100644 --- a/deployments/headscale/docker-compose.yml +++ b/deployments/headscale/docker-compose.yml @@ -55,6 +55,7 @@ services: - /var/run/headscale volumes: - ./config.yaml:/etc/headscale/config.yaml:ro + - ./policy.hujson:/etc/headscale/policy.hujson:ro - headscale-data:/var/lib/headscale healthcheck: test: ["CMD", "headscale", "health"] diff --git a/deployments/headscale/policy.hujson b/deployments/headscale/policy.hujson new file mode 100644 index 0000000..75fd8b8 --- /dev/null +++ b/deployments/headscale/policy.hujson @@ -0,0 +1,49 @@ +// headscale ACL policy (HuJSON: JSON with // comments and trailing commas). +// +// IMPORTANT: headscale uses Tailscale's *legacy* policy format -- the top-level +// "acls" array -- NOT the newer "grants" syntax from the Tailscale admin +// console. This file is the grants-style default translated into "acls". +// +// Edit, then: +// headscale policy check # validate (uses the wrapper) +// docker compose restart headscale # apply (policy mode = file) +// +// A few features here (autogroup:self, the ssh block) are newer/experimental +// in headscale and may depend on your version. If headscale rejects this +// policy at start, switch to the permissive fallback at the bottom. +{ + // Static user groups, e.g. "group:admins": ["alice@example.com"]. + "groups": {}, + + // Who may apply which tags to devices. + "tagOwners": { + // tag:shared -- a device any member is allowed to reach. + "tag:shared": ["autogroup:member"], + }, + + // Network access rules (first match wins; default-deny otherwise). + "acls": [ + // Each user can reach their own devices, on all ports. + { "action": "accept", "src": ["autogroup:member"], "dst": ["autogroup:self:*"] }, + + // Anyone can reach devices tagged tag:shared, on all ports. + { "action": "accept", "src": ["*"], "dst": ["tag:shared:*"] }, + ], + + // Tailscale SSH rules (nodes must run `tailscale up --ssh`). + "ssh": [ + // SSH into your own devices. "check" = periodic browser re-auth (the + // Tailscale default). Use "accept" if your headscale lacks check mode. + { + "action": "check", + "src": ["autogroup:member"], + "dst": ["autogroup:self"], + "users": ["autogroup:nonroot", "root"], + }, + ], + + // ---- Permissive fallback ---------------------------------------------- + // Replace the "acls" above with this (and drop "ssh") if the rules are + // rejected, to allow everything while you iterate: + // "acls": [ { "action": "accept", "src": ["*"], "dst": ["*:*"] } ], +}