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
- chown the state/cache directories to the user running headscale: `chown -R headscale:headscale /var/lib/headscale`
- Point the failing config key (visible in the error text after the colon) at a writable absolute path
- 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
- chown state/cache dirs to the service user as part of deployment
- Run the server as one dedicated user consistently — never mix root and service user
- Check writable mounts in containers before first boot
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
- reading or creating Noise protocol private key: %w
- reading or creating DERP server private key: %w
- removing old socket file: %w
- setting up unix socket: %w
- changing socket permission: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/12527d62d1320f31.
Report an issue: GitHub.