Repository navigation
Open
Labels
enhancementNew feature or requestNew feature or request
Milestone
Description
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):
piquelctlruns 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-enrollon the Pi that only createspd-*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
- 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. - 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.
- 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 addor 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_addedanddevice_removedevents. - 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 inaccount inviteoutput. - 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
- feat(tailnet): tailnet nodes on a self-hosted control server #343: tailnet nodes on a self-hosted control server.
- feat(auth): link tailnet users to piqueld accounts #345: tailnet users linked to accounts.
Written with Claude Opus 5.5 in Claude Code (via T3 Code).
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request
Type
Projects
- StatusShow more project fieldsBlocked
Activity
[-]feat(auth): invitations carry a device, and devices are managed per account[/-][+]feat(auth): invitations enroll the invitee on the tailnet[/+]