diff --git a/docs/tailscale-acl-proposed.hujson b/docs/tailscale-acl-proposed.hujson new file mode 100644 index 0000000..11498f1 --- /dev/null +++ b/docs/tailscale-acl-proposed.hujson @@ -0,0 +1,130 @@ +// PROPOSED tailnet policy — Phase 2 of docs/security-plan.md. DRAFT for +// Kyle's review, 2026-11-01. NOT LIVE. Baseline it replaces: +// docs/tailscale-acl.hujson. +// +// Design: single-user tailnet. Kyle's interactive machines stay UNTAGGED +// (they speak as isomk@ → full access via autogroup:members). Tags exist to +// constrain the devices that are NOT him-at-a-keyboard: phones/TV, IoT, and +// the server fleet. Tagging a device removes its user identity, which is +// exactly the point. +// +// Tag assignments (apply on the Machines page before saving this policy): +// tag:server (home) : rift, orion, dixie, butler, sk, talos +// tag:cloud (public-facing remote): phobos, deimos (Hetzner colo, web), +// fs (devlogs/primers, remote dev), svc +// tag:mobile : iphone-15-pro, ipad-pro-11-gen-3, pixel-8a, apple-tv +// tag:iot : homeassistant, synology +// UNtagged (full access, by design): straylight, titan, callisto, io, +// wntrmute (agent host). +// +// Known flows pre-allowed (expect a first-week breakage pass anyway): +// - agents on tagged servers → Mattermost on straylight (serve 443) +// - orion VictoriaMetrics → node exporters (*:9100, *:8420) +// - server agents → wntrmute Hermes peer API (:8642) +// - Syncthing mesh on :22000 for every device incl. tagged +// +// Unresolved: fs also ADVERTISES exit node — deliberate? (dst +// autogroup:internet below covers use; drop the advertisement if unwanted.) + +{ + "tagOwners": { + "tag:server": ["autogroup:admin"], + "tag:cloud": ["autogroup:admin"], + "tag:mobile": ["autogroup:admin"], + "tag:iot": ["autogroup:admin"], + }, + + "acls": [ + // Kyle's own (untagged) devices: full access. NOTE: autogroup:members + // = every tailnet member; safe only while this stays a single-user + // tailnet. Revisit if anyone is ever invited. + {"action": "accept", "src": ["autogroup:members"], "dst": ["*:*"]}, + + // Exit-node egress for user devices (fs currently advertises). + {"action": "accept", "src": ["autogroup:members"], "dst": ["autogroup:internet:*"]}, + + // Syncthing mesh: every device, both directions, one port. + {"action": "accept", "src": ["*"], "dst": ["*:22000"]}, + + // Server ↔ server ↔ cloud: agents, metrics, compose, DBs. + {"action": "accept", "src": ["tag:server", "tag:cloud"], + "dst": ["tag:server:*", "tag:cloud:*"]}, + + // Tagged servers reach the untagged hub hosts only on specific flows: + // Mattermost (straylight serve 443 + direct 8065), exporters anywhere. + {"action": "accept", "src": ["tag:server", "tag:cloud"], + "dst": [ + "straylight.scylla-hammerhead.ts.net:443", + "straylight.scylla-hammerhead.ts.net:8065", + ]}, + {"action": "accept", "src": ["tag:server"], "dst": ["*:9100", "*:8420"]}, + + // Server agents → wntrmute Hermes peer API only (Kyle: outbound + + // maybe inbound peer API). + {"action": "accept", "src": ["tag:server"], + "dst": ["wntrmute.scylla-hammerhead.ts.net:8642"]}, + + // Phones / tablet / TV: HA is a must; Mattermost, Syncthing, and DSM + // nice-to-have. Nothing else — no SSH, no DBs, no exporters. + {"action": "accept", "src": ["tag:mobile"], + "dst": [ + "homeassistant.scylla-hammerhead.ts.net:8123", + "straylight.scylla-hammerhead.ts.net:443", + "synology.scylla-hammerhead.ts.net:5000", + "synology.scylla-hammerhead.ts.net:5001", + ]}, + // (Syncthing for mobile arrives via the *:22000 mesh rule above.) + + // tag:iot initiates nothing except the Syncthing mesh above. + ], + + // Tailscale SSH: unchanged default (members → own devices). Once P1 + // adopts --ssh fleet-wide, consider narrowing dst to tag:server and + // adding a checkPeriod so sessions re-auth instead of persisting. + "ssh": [ + { + "action": "accept", + "src": ["autogroup:members"], + "dst": ["autogroup:self"], + "users": ["autogroup:nonroot", "root"], + }, + ], + + "nodeAttrs": [ + // Funnel: servers and cloud boxes may publish; user desktops, + // phones, and IoT may not. (straylight is untagged by design and so + // loses Funnel eligibility — it runs serve, tailnet-only, which + // needs no attr.) + { + "target": ["tag:server", "tag:cloud"], + "attr": ["funnel"], + }, + ], + + // Run on every save; the console executes these before accepting. + "tests": [ + { + "src": "tag:mobile", + "accept": ["homeassistant.scylla-hammerhead.ts.net:8123", + "straylight.scylla-hammerhead.ts.net:443", + "100.95.252.120:22000"], + "deny": ["rift.scylla-hammerhead.ts.net:22", + "rift.scylla-hammerhead.ts.net:8000", + "dixie.scylla-hammerhead.ts.net:5432", + "orion.scylla-hammerhead.ts.net:9100"], + }, + { + "src": "tag:server", + "accept": ["dixie.scylla-hammerhead.ts.net:11434", + "rift.scylla-hammerhead.ts.net:8000", + "straylight.scylla-hammerhead.ts.net:443", + "wntrmute.scylla-hammerhead.ts.net:8642"], + "deny": ["titan.scylla-hammerhead.ts.net:22"], + }, + { + "src": "tag:iot", + "accept": ["synology.scylla-hammerhead.ts.net:22000"], + "deny": ["rift.scylla-hammerhead.ts.net:22"], + }, + ], +}