tailscale/tailscale · error

error waiting for tailscaled socket: %w

Error message

error waiting for tailscaled socket: %w

What it means

tailscaled.go:55 is hit inside the wait loop for the tailscaled socket: os.Stat(cfg.Socket) returned an error that is not fs.ErrNotExist (the loop tolerates a missing file and keeps polling). Typically EACCES/permission denied on the socket path or its parent directory, or the path exists but is not a regular unix socket file.

Source

Thrown at cmd/containerboot/tailscaled.go:55

		cmd.Env = append(os.Environ(), "TS_CERT_SHARE_MODE="+cfg.CertShareMode)
	}
	log.Printf("Starting tailscaled")
	if err := cmd.Start(); err != nil {
		return nil, nil, fmt.Errorf("starting tailscaled failed: %w", err)
	}

	// Wait for the socket file to appear, otherwise API ops will racily fail.
	log.Printf("Waiting for tailscaled socket at %s", cfg.Socket)
	for {
		if ctx.Err() != nil {
			return nil, nil, errors.New("timed out waiting for tailscaled socket")
		}
		_, err := os.Stat(cfg.Socket)
		if errors.Is(err, fs.ErrNotExist) {
			time.Sleep(100 * time.Millisecond)
			continue
		} else if err != nil {
			return nil, nil, fmt.Errorf("error waiting for tailscaled socket: %w", err)
		}
		break
	}

	tsClient := &local.Client{
		Socket:        cfg.Socket,
		UseSocketOnly: true,
	}

	return tsClient, cmd.Process, nil
}

// tailscaledArgs uses cfg to construct the argv for tailscaled.
func tailscaledArgs(cfg *settings) []string {
	args := []string{"--socket=" + cfg.Socket}
	switch {
	case cfg.KubeSecret != "":
		args = append(args, "--state=kube:"+cfg.KubeSecret)

View on GitHub (pinned to cfe32b8be6)

Solutions

  1. Check the stat error in the log (it is wrapped with %w) to confirm permission vs other causes
  2. Ensure the socket directory is writable by the container user (fsGroup, runAsUser, or writable emptyDir/hostPath)
  3. Verify TS_SOCKET points at a socket file path, not a directory
  4. Run with the stock image defaults, which provision /var/run/tailscale correctly

Example fix

# before
securityContext:
  runAsNonRoot: true
# volume mounted read-only at /var/run/tailscale
# after
securityContext:
  runAsNonRoot: true
  fsGroup: 1000
volumes:
  - name: run
    emptyDir: {} # writable socket dir
Defensive patterns

Strategy: validation

Validate before calling

// Preflight: the socket path must be stat-able by this process (parent dir traversable).
dir := filepath.Dir(socketPath)
if info, err := os.Stat(dir); err != nil || !info.IsDir() {
    log.Fatalf("socket dir %s unusable: %v", dir, err)
}
if _, err := os.Stat(socketPath); err != nil && !errors.Is(err, fs.ErrNotExist) {
    log.Fatalf("socket path %s not checkable (permissions?): %v", socketPath, err)
}

Prevention

When it happens

Trigger: TS_SOCKET (or the default /var/run/tailscale/tailscaled.sock) lives in a directory the container cannot traverse due to read-only mounts, wrong fsGroup, or SELinux; the path points at a directory; the socket was created by a different UID.

Common situations: Non-root securityContext with restrictive volumeMounts on /var/run/tailscale; mounting a hostPath with root-owned perms; overriding TS_SOCKET to a path under a read-only volume.

Related errors


AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15). Data as JSON: /api/errors/36e1941143502705. Report an issue: GitHub.