hscontrol, cli: serve the v2 API on the local socket and add the oauth-clients command

Serve the v2 API over the local unix socket for the CLI and add the
oauth-clients command that drives it.
This commit is contained in:
Kristoffer Dalby committed 2026-06-26 09:18:06 +02:00
1 parent 84715e976c
commit b05f8875c3
5 files changed
+4355 -24

No files matched your search

+26 -7
View File
@@ -319,6 +319,11 @@ func (h *Headscale) scheduledTasks(ctx context.Context) {
revokedKeyGCChan = revokedKeyTicker.C
}
// OAuth access tokens are short-lived (1h) and re-minted on demand; reap
// expired rows hourly so the table stays bounded.
accessTokenTicker := time.NewTicker(time.Hour)
defer accessTokenTicker.Stop()
for {
select {
case <-ctx.Done():
@@ -335,6 +340,14 @@ func (h *Headscale) scheduledTasks(ctx context.Context) {
log.Info().Int("count", reaped).Msg("reaped revoked pre-auth keys")
}
case <-accessTokenTicker.C:
reaped, err := h.state.DeleteExpiredAccessTokens(time.Now())
if err != nil {
log.Error().Err(err).Msg("reaping expired oauth access tokens")
} else if reaped > 0 {
log.Debug().Int64("count", reaped).Msg("reaped expired oauth access tokens")
}
case <-expireTicker.C:
var (
expiredNodeChanges []change.Change
@@ -627,20 +640,26 @@ func (h *Headscale) Serve() error {
Cfg: h.cfg,
})
// The Headscale v2 API. It is served only over the remote
// listener (Basic/Bearer auth); no unix-socket mount yet, as nothing local
// calls it — the CLI still speaks v1.
// The Headscale v2 API. Served behind Basic/Bearer auth on the remote
// listener, and over the local unix socket (local trust) so the CLI can
// manage OAuth clients through the same v2 keys handler the Tailscale
// ecosystem uses.
humaV2Mux, _ := apiv2.Handler(apiv2.Backend{
State: h.state,
Change: h.Change,
Cfg: h.cfg,
})
// Serve the Huma API over the unix socket without TLS or auth: socket access
// implies trust. WithLocalTrust marks these requests so the security
// middleware skips the API-key check.
// Serve both Huma APIs over the unix socket without TLS or auth: socket
// access implies trust. WithLocalTrust marks these requests so each API's
// security middleware skips the credential check. v2 paths route to the v2
// mux; everything else (the v1 paths) to v1.
socketHandler := http.NewServeMux()
socketHandler.Handle("/api/v2/", apiv2.WithLocalTrust(humaV2Mux))
socketHandler.Handle("/", apiv1.WithLocalTrust(humaMux))
socketServer := &http.Server{
Handler: apiv1.WithLocalTrust(humaMux),
Handler: socketHandler,
ReadTimeout: types.HTTPTimeout,
}