juanfont/headscale · critical
changing socket permission: %w
Error message
changing socket permission: %w
What it means
Returned when os.Chmod(h.cfg.UnixSocket, h.cfg.UnixSocketPermission) fails right after the Unix socket is bound (hscontrol/app.go:636). The chmod applies the configured unix_socket_permission mode to the freshly created socket. Failure means the socket file could not be modified, which is nearly always a race or a filesystem limitation rather than a bad mode value.
Source
Thrown at hscontrol/app.go:636
if err != nil {
return fmt.Errorf("removing old socket file: %w", err)
}
socketDir := filepath.Dir(h.cfg.UnixSocket)
err = util.EnsureDir(socketDir)
if err != nil {
return fmt.Errorf("setting up unix socket: %w", err)
}
socketListener, err := new(net.ListenConfig).Listen(context.Background(), "unix", h.cfg.UnixSocket)
if err != nil {
return fmt.Errorf("setting up socket: %w", err)
}
// Change socket permissions
if err := os.Chmod(h.cfg.UnixSocket, h.cfg.UnixSocketPermission); err != nil { //nolint:noinlineerr
return fmt.Errorf("changing socket permission: %w", err)
}
// The Huma v1 API mux matches full /api/v1/... paths and is shared by
// the local unix socket (served without authentication, local trust)
// and the remote TCP router (served behind the API-key middleware).
humaMux, _ := apiv1.Handler(apiv1.Backend{
State: h.state,
Change: h.Change,
Cfg: h.cfg,
})
// 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,View on GitHub (pinned to 565fd254d0)
Solutions
- Verify only one headscale runs per socket path: pgrep -a headscale; stop duplicates and restart.
- Re-run the service; transient ENOENT races resolve on a clean single-instance start.
- If a cleanup daemon deletes the socket, exclude the socket directory from it or relocate unix_socket.
- On exotic container storage, move the socket to a tmpfs mount (--tmpfs /var/run/headscale).
Example fix
# before: two units pointing at the same socket # headscale.service and headscale2.service both use /var/run/headscale/headscale.sock # after: give each instance its own socket in config.yaml unix_socket: /var/run/headscale/headscale.sock # instance A unix_socket: /var/run/headscale2/headscale.sock # instance B
Defensive patterns
Strategy: retry
Validate before calling
// after startup, verify permissions took effect
fi, err := os.Stat(cfg.UnixSocket)
if err == nil && fi.Mode().Perm() != cfg.UnixSocketPermission {
// log/repair
} Try / catch
if err := h.Serve(); err != nil && strings.Contains(err.Error(), "changing socket permission") {
// race with concurrent instance: stop the other process and restart once, do not loop Prevention
- Run exactly one instance per socket path.
- Pre-set sane unix_socket_permission in config so chmod is a no-op of the default.
- Keep aggressive tmp-cleanup daemons away from the socket directory.
When it happens
Trigger: The socket file disappeared between Listen and Chmod (another process or a second headscale removed it -> ENOENT); the socket sits on a filesystem that does not support chmod semantics; the process lacks ownership rights over the file it just created (unusual, e.g. after a setfsuid transition or idmapped mounts in containers).
Common situations: Two headscale instances with identical configs starting at once (one removes/chmods the other's socket); cleanup scripts (tmpwatch, container sidecars) deleting sockets aggressively; unusual container storage drivers that break chmod on sockets.
Related errors
- removing old socket file: %w
- setting up unix socket: %w
- creating directory failed with permission error
- reading or creating Noise protocol private key: %w
- reading or creating DERP server private key: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/c948c80b5be030b0.
Report an issue: GitHub.