juanfont/headscale · warning
auth-key expired
Error message
auth-key expired
What it means
Sentinel in hscontrol/db/preauth_keys.go returned by UsePreAuthKey when the key's expiration time is in the past. The key exists, is not exhausted, but its set expiration (visible via `headscale preauthkeys list`) has passed, so registration is refused.
Source
Thrown at hscontrol/db/preauth_keys.go:22
"errors"
"fmt"
"slices"
"strings"
"time"
"github.com/juanfont/headscale/hscontrol/types"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"tailscale.com/util/rands"
"tailscale.com/util/set"
)
var (
// ErrPreAuthKeyNotFound wraps gorm.ErrRecordNotFound so an unknown or
// deleted key is treated as a missing record by callers, which the
// registration handler maps to a 401 rather than a raw server error.
ErrPreAuthKeyNotFound = fmt.Errorf("auth-key not found: %w", gorm.ErrRecordNotFound)
ErrPreAuthKeyExpired = errors.New("auth-key expired")
ErrSingleUseAuthKeyHasBeenUsed = errors.New("auth-key has already been used")
ErrUserMismatch = errors.New("user mismatch")
ErrPreAuthKeyACLTagInvalid = errors.New("auth-key tag is invalid")
)
// validateACLTags deduplicates, sorts, and checks that every tag carries the
// "tag:" prefix. Shared by the pre-auth-key and OAuth credential paths so both
// enforce the same tag shape.
func validateACLTags(tags []string) ([]string, error) {
tags = set.SetOf(tags).Slice()
slices.Sort(tags)
for _, tag := range tags {
if !strings.HasPrefix(tag, "tag:") {
return nil, fmt.Errorf(
"%w: '%s' did not begin with 'tag:'",
ErrPreAuthKeyACLTagInvalid,
tag,View on GitHub (pinned to 565fd254d0)
Solutions
- Create a new key: `headscale preauthkeys create --user <user> --expiration <duration>` and use it immediately
- Check `headscale preauthkeys list` to confirm the key's expiration before use
- For long-lived automation, create keys with a longer --expiration, or integrate the API to mint keys on demand
Example fix
# before headscale preauthkeys create --user alice --expiration 1h # ...days later tailscale up --auth-key=<old-key> # auth-key expired # after headscale preauthkeys create --user alice --expiration 30d tailscale up --auth-key=<new-key>
Defensive patterns
Strategy: try-catch
Validate before calling
# before enrollment, check the key's expiry from list output:
headscale preauthkeys list --user "$USER" | awk -v k="$KEY" '$0 ~ k {print $3, $4}' Try / catch
if err := db.UsePreAuthKey(tx, pak, user); err != nil {
if errors.Is(err, db.ErrPreAuthKeyExpired) {
return registrationFailed(401, "auth-key expired; create a new one")
}
return err
} Prevention
- Create keys just-in-time for enrollment rather than stockpiling them
- Mint keys with --expiration matched to the provisioning window
- Automations should request fresh keys via the API on each run
When it happens
Trigger: Registering a node with `tailscale up --auth-key=...` (or `tailscale login`) using a pre-auth key created with --expiration that has since passed; also possible with server clock skew.
Common situations: Keys created with short expirations (default 1h) reused days later; expired key left in automation config; NTP drift on the server expiring keys early.
Related errors
- auth-key has already been used
- user mismatch
- failed to parse auth-key
- oauth access token expired
- registration expired
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/d4aa5a60a423c746.
Report an issue: GitHub.