diff --git a/README.md b/README.md index c4ed42c..5f4f813 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,18 @@ dvm log cloudflared -f The cloudflared token is staged through a mode `0600` guest temp file during `sync` instead of being passed as a `limactl shell env` argument. -Tailscale Funnel VM (publish a local service at a public `*.ts.net` URL on demand): +Tailscale supports two patterns: + +**Private dev access** — reach app VMs by hostname from your Mac without +juggling host ports. Add `use tailscale` to each app VM and sync: + +```bash +TAILSCALE_AUTH_KEY="tskey-..." dvm sync fida +# then from the host browser: http://fida:5173 +``` + +**Public Funnel URLs** — publish a local service at a public `*.ts.net` URL +on demand: ```bash # One-time: create the VM and join your tailnet @@ -147,7 +158,8 @@ dvm sync tailscale Funnel is OFF by default; the recipe resets it on every sync, so leaving the target unset turns it off. Auth keys get the same mode `0600` guest temp-file handling as Cloudflare tokens. See [docs/services.md](docs/services.md#tailscale) -for the Funnel ACL prerequisite and full walkthrough. +for both patterns, the Funnel ACL prerequisite, and auth-key sourcing options +(global config, env var, or macOS Keychain). ## Recipes diff --git a/docs/config.md b/docs/config.md index 81dd810..fba7840 100644 --- a/docs/config.md +++ b/docs/config.md @@ -120,10 +120,13 @@ them from per-VM configs. Service recipes (`llama`, `cloudflared`) are not in - `DVM_CLOUDFLARED_SERVICE`, `DVM_CLOUDFLARED_TOKEN`: cloudflared service settings. The bundled cloudflared recipe receives the token through a mode `0600` guest temp file during `sync`, so it is not passed as a `limactl shell env` argument. -- `DVM_TAILSCALE_AUTH_KEY`: tailscale auth key (`tskey-...`). Passed in at sync time - via `TAILSCALE_AUTH_KEY=...` and staged through a mode `0600` guest temp file, the - same way as the cloudflared token. Required on first sync; subsequent syncs reuse - the persistent tailscaled state unless a new key is provided. +- `DVM_TAILSCALE_AUTH_KEY`: tailscale auth key (`tskey-...`). Either set in + `~/.config/dvm/config.sh` or passed at sync time via `TAILSCALE_AUTH_KEY=...` + (config wins when both are set). Staged through a mode `0600` guest temp file + during `sync`, the same way as the cloudflared token. Required on first sync; + subsequent syncs reuse the persistent tailscaled state unless a new key is + provided. See [Services → Auth key sourcing](services.md#auth-key-sourcing) + for global-config, env-var, and macOS Keychain patterns. - `DVM_TAILSCALE_HOSTNAME`: override the device name shown in the Tailscale admin console. Defaults to `$DVM_NAME`. - `DVM_TAILSCALE_FUNNEL_TARGET`: backend URL for `tailscale funnel`, e.g. diff --git a/docs/recipes.md b/docs/recipes.md index c7d0453..83f892d 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -233,10 +233,12 @@ CLOUDFLARED_TOKEN="$(security find-generic-password -a dvm -s cloudflared -w)" \ DVM does not provide a secret store command. `tailscale` joins the tailnet (private mesh) and optionally publishes a single -backend service via Tailscale Funnel. Configure either a dedicated proxy VM or -add `use tailscale` to any VM that should be reachable from your tailnet. See -[Services](services.md#tailscale) for tunnel-VM setup, the auth-key flow, and the -Funnel ACL prerequisite. The auth key is passed in at sync time through the same +backend service via Tailscale Funnel. Two patterns: add `use tailscale` to any +app VM for private hostname-based dev access from your tailnet, or use the +dedicated `tailscale` VM template for on-demand public Funnel URLs. See +[Services](services.md#tailscale) for both walkthroughs, the Funnel ACL +prerequisite, and the three auth-key sourcing options (global config, env var, +or macOS Keychain). The auth key is passed in at sync time through the same secret-staging path as `CLOUDFLARED_TOKEN`: ```bash diff --git a/docs/services.md b/docs/services.md index 5ed305f..f1e808e 100644 --- a/docs/services.md +++ b/docs/services.md @@ -86,22 +86,93 @@ compromised. ## Tailscale -Use case this recipe is designed for: **publish a local VM service at a public -`*.ts.net` URL on demand, share it with a teammate or external user, then turn -it off when you're done**. Funnel is OFF by default; you flip it on by passing +The `tailscale` recipe supports two patterns: + +1. **Private dev access.** Add `use tailscale` to any app VM so you can reach + its dev servers from your host (and other tailnet devices) by hostname, + without juggling `DVM_PORTS` across many VMs. +2. **Public Funnel URLs.** Use the dedicated `tailscale` VM template to publish + one HTTP backend (running in any other DVM VM) at a public `*.ts.net` URL on + demand. + +The two are independent — you can use either, both, or neither. Both share the +same auth-key flow; see [Auth key sourcing](#auth-key-sourcing). + +### Pattern 1: Private dev access (per app VM) + +Goal: reach `http://fida:5173`, `http://bar:3000`, etc. from your Mac without +ever picking unique host ports per VM. + +1. **Install Tailscale on your host** and sign in to the same tailnet. The host + resolves VM hostnames via MagicDNS. + +2. **Add the recipe to each app VM** that should be reachable: + + ```bash + # ~/.config/dvm/vms/fida.sh + use_tools + use tailscale + ``` + + You can drop `DVM_PORTS` from these VMs entirely — Tailscale gives each VM + its own IP, so port collisions don't exist on the tailnet. + +3. **Sync each VM**, providing the auth key (see + [Auth key sourcing](#auth-key-sourcing) for where to put it): + + ```bash + TAILSCALE_AUTH_KEY="tskey-..." dvm sync fida + ``` + + The recipe registers the VM with hostname `$DVM_NAME` (override with + `DVM_TAILSCALE_HOSTNAME`). + +4. **Run dev servers as usual** inside the VM, listening on `0.0.0.0`: + + ```bash + dvm ssh fida + npm run dev # vite on 0.0.0.0:5173 + ``` + + From the host browser: + + ```text + http://fida:5173 + http://bar:3000 + ``` + +#### Optional: drop the port with `tailscale serve` + +For a clean HTTPS URL without a port, run inside the VM: + +```bash +sudo tailscale serve --bg --https=443 http://localhost:5173 +``` + +This persists across reboots. `https://fida..ts.net` now hits vite +directly. `tailscale serve status` shows current config; `tailscale serve +reset` clears it. + +This is tailnet-private — only devices signed into your tailnet can reach it. +For a public URL, see Pattern 2. + +### Pattern 2: Public Funnel URLs (dedicated VM) + +Use case: **publish a local VM service at a public `*.ts.net` URL on demand, +share it with a teammate or external user, then turn it off when you're +done**. Funnel is OFF by default; you flip it on by passing `DVM_TAILSCALE_FUNNEL_TARGET=` at sync time and OFF by syncing again without it. -DVM ships a `tailscale` recipe and a dedicated `tailscale` VM template that -joins the tailnet and proxies one HTTP backend (running in any other DVM VM -reachable via Lima's internal DNS) to a public Funnel URL. +DVM ships a dedicated `tailscale` VM template that joins the tailnet and +proxies one HTTP backend (running in any other DVM VM reachable via Lima's +internal DNS) to a public Funnel URL. -### One-time setup +#### One-time setup -1. **Get an auth key.** Sign in at `login.tailscale.com/admin` → **Settings → - Keys** → **Generate auth key**. For a long-lived proxy node, use a - **reusable** key. Recommended: tag the key (e.g. `tag:dvm`) so ACLs can - reason about it. Copy the `tskey-...` value. +1. **Get an auth key** (see [Auth key sourcing](#auth-key-sourcing)). For a + long-lived proxy node, use a **reusable** key. Recommended: tag the key + (e.g. `tag:dvm`) so ACLs can reason about it. 2. **Allow Funnel in your tailnet ACL.** Open **Access controls** in the admin console and ensure your policy grants the `funnel` node attribute to the @@ -131,7 +202,7 @@ The bundled template sets `DVM_NO_BASELINE=1` and maps `TAILSCALE_AUTH_KEY` to file during `sync`, so the value is never passed as a `limactl shell env` argument on the host. -### Day-to-day: turn Funnel on/off +#### Day-to-day: Funnel on/off When a teammate needs to see your dev app for a few hours: @@ -156,7 +227,7 @@ dvm sync tailscale The recipe runs `tailscale funnel reset` on every sync, so leaving `DVM_TAILSCALE_FUNNEL_TARGET` unset means OFF. No state to forget about. -### Pinning a permanent target +#### Pinning a permanent target If a single VM should always be the funnel target (e.g. a dedicated demo VM that's always sharing the same service), set the variable in the VM config @@ -170,7 +241,7 @@ DVM_TAILSCALE_FUNNEL_TARGET="http://lima-dvm-demo.internal:8080" Then every `dvm sync tailscale` keeps Funnel ON pointing there. Comment the line out (or delete it) when you want to go back to the on-demand workflow. -### Limits to know +#### Limits to know - **One Funnel target per node.** To expose multiple services publicly at the same time, run multiple Tailscale VMs (`dvm init demo-a tailscale`, @@ -185,13 +256,72 @@ line out (or delete it) when you want to go back to the on-demand workflow. `..ts.net` — no custom domain. If you need a custom domain, use the `cloudflared` recipe instead. +### Auth key sourcing + +A single **reusable** auth key works for every VM that uses the recipe. +Generate one in the admin console under **Settings → Keys → Generate auth +key**. DVM reads the key from `DVM_TAILSCALE_AUTH_KEY` first and falls back to +`TAILSCALE_AUTH_KEY` (apply.sh:14) — so a value in config wins over an env +var. Pick whichever sourcing fits your threat model. + +#### A. Global config file (lowest friction, plaintext on disk) + +Set the key once in `~/.config/dvm/config.sh`: + +```bash +DVM_TAILSCALE_AUTH_KEY="tskey-auth-..." +``` + +Then `dvm sync ` works with no env var. The key sits unencrypted in your +home directory; reasonable if your laptop disk is encrypted and the key is a +personal-tailnet reusable key. + +#### B. Shell env var (per-sync or per-shell) + +```bash +TAILSCALE_AUTH_KEY="tskey-auth-..." dvm sync fida +``` + +Or export it in your shell rc so every sync in that shell picks it up: + +```bash +# ~/.zshrc +export TAILSCALE_AUTH_KEY="tskey-auth-..." +``` + +Same disk-plaintext concern as (A) when stored in rc files; (A) is usually +simpler if you're going that route. + +#### C. macOS Keychain (no plaintext on disk) + +Store the key once: + +```bash +security add-generic-password -a "$USER" -s dvm-tailscale-auth-key \ + -w "tskey-auth-..." +``` + +Wrap `dvm sync` so it pulls the key from the Keychain on demand: + +```bash +# ~/.zshrc +dvm-sync() { + TAILSCALE_AUTH_KEY="$(security find-generic-password \ + -a "$USER" -s dvm-tailscale-auth-key -w)" \ + dvm sync "$@" +} +``` + +Then `dvm-sync fida` reads the key at sync time only; it never lands on disk +in plaintext and never enters shell history. + ### Auth key rotation and tear-down To rotate the auth key, generate a new one in the admin console, run -`TAILSCALE_AUTH_KEY="tskey-new..." dvm sync tailscale`, then delete the old -key. To remove the VM from your tailnet, delete the device in the admin -console (or `dvm sh tailscale` then `sudo tailscale logout`) and then -`dvm rm tailscale --yes`. +`TAILSCALE_AUTH_KEY="tskey-new..." dvm sync ` (or update whichever +sourcing path you use), then delete the old key. To remove a VM from your +tailnet, delete the device in the admin console (or `dvm sh ` then +`sudo tailscale logout`) and then `dvm rm --yes`. ## Logs