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

  1. Verify only one headscale runs per socket path: pgrep -a headscale; stop duplicates and restart.
  2. Re-run the service; transient ENOENT races resolve on a clean single-instance start.
  3. If a cleanup daemon deletes the socket, exclude the socket directory from it or relocate unix_socket.
  4. 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

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


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/c948c80b5be030b0. Report an issue: GitHub.