Files
notfiles/docs/superpowers/specs/2026-04-16-tailscale-integration-design.md
2026-04-16 23:55:43 -04:00

4.6 KiB

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=<key>)
  • 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:

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<dyn Identity>

Updated source resolution order in notstrap:

  1. --key-file CLI arg
  2. YubikeySource (PIV slot 9c)
  3. BitwardenSource
  4. PromptSource

notstrap (updated)

BootstrapOptions gains:

pub tailscale: Option<TailscaleOptions>,

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:

[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