Skip to content

feat(auth): invitations enroll the invitee on the tailnet #344

Open

Description

@PiquelChips

Phase 4 of sourceorigin-dev/infra#7 (#348): needed once a second person needs access. Until then the operator enrolls people by hand over SSH (headscale preauthkeys create) and sends a normal invitation.

Problem

New people need tailnet access before they can reach piqueld at all, and today that happens outside piqueld, in the tailnet's admin console.

Proposed solution

Invitations enroll people on Headscale (sourceorigin-dev/infra#9), so piqueld stays the one place people are managed.

No Headscale API key

Headscale's API keys have no scopes: any key is full admin of the network, so piqueld must not hold one. Open decision, lean first:

  • Operator-side (lean): piquelctl runs Headscale's local CLI on the Pi over the operator's own hardware-key SSH, then creates the piqueld invitation. No Headscale credential in piqueld and nothing to build on the Pi, but the operator is present for every enrollment and device change.
  • Forced command: piqueld holds an SSH key restricted to one command, a small Rust hs-enroll on the Pi that only creates pd-* users, issues single-use untagged keys for them and deletes their nodes, through the local CLI. Self-service device adds become possible; a stolen key can only enroll untagged members, which the policy limits to the apps node.

Enrolling a new account

  1. An admin runs piquelctl account invite alice --preset qa, or uses the dashboard. piqueld creates the invitation; enrollment creates a Headscale user for it and a single-use pre-auth key whose expiry matches the invitation's.
  2. The admin sends the bundle (the login server URL, the pre-auth key, the invite link and its expiry) over a private channel, telling the invitee to install Tailscale first.
  3. The invitee joins the tailnet with the key, then opens the invite link, which only resolves on the tailnet, and registers a passkey. The account is created and linked to the tailnet user (feat(auth): link tailnet users to piqueld accounts #345).
  • Clients without auth keys: where a Tailscale app can't take a pre-auth key, the invitee starts a normal login against the server and sends the registration key it shows; the admin approves it with piquelctl invite approve, which registers the node to the invitation's user.
  • Refresh: one operation replaces an invitation's bundle, invalidating the old key.
  • Expiry: an unredeemed invitation expires with its pre-auth key; its Headscale user is deleted if it has no nodes.
  • First account: the operator's own nodes join through Nix (feat(network): every host on the Headscale tailnet, with policy from the registry infra#3). The setup link links the operator's existing tailnet user instead of creating one.

Managing devices

  • Adding: from an enrolled device, piquelctl device add or the dashboard issues a new pre-auth key for the account's tailnet user, after a fresh passkey check through the existing browser approval flow; a saved CLI token alone doesn't satisfy it. Self-service needs the forced command; otherwise the operator issues the key.
  • Listing and removing: the account's nodes. Removing a node ends its connections on the next lease recheck.
  • Lost every device: an admin issues a replacement key, like an invitation.
  • Limits: scoped credentials can't add devices. Account managers can list and remove other accounts' devices under the usual account management rules.
  • Audit: device_added and device_removed events.
  • Secrets: pre-auth keys are shown once, never logged, and served with Cache-Control: no-store. CLI bundles pass them as --auth-key=file:<path> with a 0600 file, to keep them out of shell history.

Surfaces

  • CLI: piquelctl device list|add|remove, invite approve, and the bundle in account invite output.
  • Dashboard: a Devices section on each account, and the bundle on the invitation page.
  • API and OpenAPI, the NixOS module (the forced-command key, if chosen), and the managing accounts docs.

To check

  • Which Tailscale clients (macOS, iOS, Android, Windows) accept a pre-auth key with a custom login server. iOS and Android do since Tailscale 1.80.

Dependencies


Written with Claude Opus 5.5 in Claude Code (via T3 Code).

Activity

  1. changed the title [-]feat(auth): invitations carry a device, and devices are managed per account[/-] [+]feat(auth): invitations enroll the invitee on the tailnet[/+] on Oct 29, 2026
  2. moved this from Needs Triage to Up Next in origin-src.devon Oct 30, 2026
  3. added this to the 0.1 milestone on Oct 30, 2026
  4. moved this from Up Next to Blocked in origin-src.devon Oct 30, 2026
  5. modified the milestones: 0.1, 0.2 on Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions