juanfont/headscale · error · util.ErrDirectoryPermission

creating directory failed with permission error

Error message

creating directory failed with permission error

What it means

ErrDirectoryPermission (hscontrol/util/file.go:25) is returned from the directory-creation helper at hscontrol/util/file.go:56 when os.MkdirAll fails with a filesystem permission error for a configured directory (the message appends the dir path: "creating directory failed with permission error: <dir>"). Headscale creates several runtime directories from config (private key path, ACME/Let's Encrypt cache dir, noise private key dir, etc.) and refuses to continue if it cannot create and write them. The PermissionFallback = 0o700 constant shows dirs are created owner-only.

Source

Thrown at hscontrol/util/file.go:25

	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/spf13/viper"
)

const (
	Base8              = 8
	Base10             = 10
	BitSize16          = 16
	BitSize32          = 32
	BitSize64          = 64
	PermissionFallback = 0o700
)

// ErrDirectoryPermission is returned when creating a directory fails due to permission issues.
var ErrDirectoryPermission = errors.New("creating directory failed with permission error")

func AbsolutePathFromConfigPath(path string) string {
	// If a relative path is provided, prefix it with the directory where
	// the config file was found.
	if (path != "") && !strings.HasPrefix(path, string(os.PathSeparator)) {
		dir, _ := filepath.Split(viper.ConfigFileUsed())
		if dir != "" {
			path = filepath.Join(dir, path)
		}
	}

	return path
}

func GetFileMode(key string) fs.FileMode {
	modeStr := viper.GetString(key)

	mode, err := strconv.ParseUint(modeStr, Base8, BitSize64)

View on GitHub (pinned to 565fd254d0)

Solutions

  1. chown the state/cache directories to the user running headscale: `chown -R headscale:headscale /var/lib/headscale`
  2. Point the failing config key (visible in the error text after the colon) at a writable absolute path
  3. If running in a container, ensure the volume is mounted writable

Example fix

# before
tls_letsencrypt_cache_dir: /etc/letsencrypt-cache   # not writable by headscale user

# after
tls_letsencrypt_cache_dir: /var/lib/headscale/acme
# and: chown -R headscale:headscale /var/lib/headscale
Defensive patterns

Strategy: validation

Validate before calling

// pre-flight: every configured dir must be creatable+writable
for _, dir := range []string{cfg.TLS.LetsEncrypt.CacheDir, stateDir} {
	if dir == "" {
		continue
	}
	if err := unix.Access(filepath.Dir(dir), unix.W_OK); err != nil {
		return fmt.Errorf("no write access for %s: %w", dir, err)
	}
}

Try / catch

if err := util.EnsureDirWritable(dir); err != nil {
	if errors.Is(err, util.ErrDirectoryPermission) {
		// message already carries the dir path; chown it or change the config key
		log.Fatalf("fix permissions on the directory named in: %v", err)
	}
}

Prevention

When it happens

Trigger: Starting headscale as a user that lacks write access to a configured path such as tls_letsencrypt_cache_dir, the private key directory, or database directory when that parent does not yet exist; a read-only filesystem or wrong ownership under /var/lib/headscale.

Common situations: Running the binary as root once (dirs become root-owned) then switching to the headscale user; systemd unit with an unexpected WorkingDirectory/User; container with a read-only mount at the state path.

Related errors


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