diff --git a/.gitignore b/.gitignore index c7535ff..a1cfa62 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,10 @@ .envrc .worktrees/ .claude.local.md -docs/ +docs/* +!docs/superpowers/ librust_out.rlib .remember/ -!docs/superpowers/ # handoff-begin diff --git a/docs/superpowers/specs/2026-04-16-tailscale-integration-design.md b/docs/superpowers/specs/2026-04-16-tailscale-integration-design.md new file mode 100644 index 0000000..005f347 --- /dev/null +++ b/docs/superpowers/specs/2026-04-16-tailscale-integration-design.md @@ -0,0 +1,141 @@ +# Tailscale Integration Design + +**Date:** 2026-04-16 +**Status:** Approved + +## Goal + +Make notfiles environments ephemeral but reliable. A fresh machine runs `notstrap` and gets a +fully configured environment — no public GitHub exposure for dotfiles, no manual Tailscale setup, +no cloud dependency for secrets on bootstrap day. + +## Overview + +Add a `notnet` crate that owns Tailscale network presence, extend `notsecrets` with a +`YubikeySource`, and thread both into `notstrap`'s orchestration sequence. The dotfiles repo lives +on a private Gitea instance on minibox, reachable only over Tailscale. + +## Crate Responsibilities + +### `notnet` (new) + +Owns all Tailscale concerns: + +- Detect if Tailscale is already running (`tailscale status`) +- Install Tailscale if missing (package manager detection: `apt`/`brew`/`pacman`, fallback to + official install script) +- Resolve the auth key via priority chain (see below) +- Join the tailnet (`tailscale up --authkey=`) +- Verify the target peer is reachable (ping by Tailscale hostname) + +**Auth key resolution chain:** + +1. `TS_AUTHKEY` env var +2. YubiKey PIV slot 9d (via `yubikey` crate) +3. Interactive prompt + +**Public API:** + +```rust +pub struct TailscaleOptions { + pub peer_hostname: String, + pub gitea_url: String, + pub install: bool, +} + +/// Ensure this machine is connected to the tailnet and the target peer is reachable. +/// Returns immediately if Tailscale is already running. +pub fn ensure_connected(opts: &TailscaleOptions) -> Result<()>; +``` + +### `notsecrets` (extended) + +New `YubikeySource` implementing `IdentitySource`: + +- Uses the `yubikey` crate (pure Rust, no `ykman` dependency) +- Opens the first connected YubiKey +- Reads age private key material from PIV slot 9c +- Returns `Box` + +Updated source resolution order in notstrap: + +1. `--key-file` CLI arg +2. `YubikeySource` (PIV slot 9c) +3. `BitwardenSource` +4. `PromptSource` + +### `notstrap` (updated) + +`BootstrapOptions` gains: + +```rust +pub tailscale: Option, +``` + +`None` = skip notnet entirely (existing machines, tests, CI). + +`notstrap.toml` gains a `[tailscale]` section. When present, `gitea_url` is used as the clone +URL instead of `[bootstrap].dotfiles_repo`. + +## YubiKey PIV Slot Layout + +| Slot | Purpose | Managed by | +|------|----------------------------------|-------------| +| 9a | SSH authentication | 1Password | +| 9c | Age private key (dotfiles/SOPS) | notsecrets | +| 9d | Tailscale auth key | notnet | + +Slot 9a is untouched — 1Password agent remains the primary SSH auth for day-to-day use. + +## Configuration + +`notstrap.toml` example with Tailscale enabled: + +```toml +[bootstrap] +dotfiles_dir = "~/dotfiles" +bw_age_item = "age-key-dotfiles" +sops_file = "secrets/bootstrap.sops.env" +# dotfiles_repo is optional when [tailscale] is present + +[tailscale] +peer_hostname = "minibox" +gitea_url = "http://minibox:3000/joe/dotfiles.git" +install = true +``` + +## Full Bootstrap Sequence + +| Step | Crate | Skip condition | +|-------------------|---------------|---------------------------------------| +| 1. Prerequisites | notstrap | — | +| 2. Tailscale | notnet | Already on tailnet | +| 3. Clone dotfiles | notstrap/repo | Already cloned | +| 4. Age key | notsecrets | — | +| 5. Decrypt SOPS | notstrap | No injector configured | +| 6. Link dotfiles | notfiles | — | +| 7. Dot hooks | nothooks | — | +| 8. Setup hooks | nothooks | Already run (state file) | + +## Error Handling + +- If `TS_AUTHKEY` is unset and no YubiKey is connected and user declines prompt → clear error, + bootstrap aborts at step 2 +- If Tailscale installs but peer is unreachable → error naming the peer hostname, bootstrap aborts +- If YubiKey slot 9c is empty → `YubikeySource` returns `Err`, resolver falls through to + `BitwardenSource` +- All steps follow the existing `StepStatus::Failed` / early-return pattern in notstrap + +## Dependencies (new) + +| Crate | Used by | Purpose | +|-------------|---------------------|-------------------------------| +| `yubikey` | notnet, notsecrets | PIV slot access (pure Rust) | +| `tailscale` | notnet | tailscale-rs SDK | + +## Out of Scope + +- Multi-YubiKey support (first connected device wins) +- Tailscale exit node or subnet router configuration +- Rotating auth keys automatically +- Windows support