semaphoreui/semaphore · error

encryption_keys.keys_folder: read

Error message

encryption_keys.keys_folder: read %q: %w

What it means

While loading every regular file in encryption.keys_folder as an encryption key (labelled by filename), Semaphore's config loader calls os.ReadFile on each entry and wraps any read failure with this error. It means a key file that exists and passed the regular-file Stat check could not actually be read (permission denied, race deleting the file, I/O error). The startup key-resolution aborts.

Solutions

  1. Check file permissions/ownership of every file in the keys_folder so the semaphore process user can read them (chmod 644 / chown to the service user).
  2. Re-check the folder for files that vanish or are rotated while the server boots; exclude non-key files instead of leaving unreadable ones in the folder.
  3. Inspect the wrapped %w error (e.g. 'permission denied' vs 'no such file') to identify the exact failing file and fix the underlying OS cause.
  4. If the folder is a network/cloud mount, verify the mount is healthy and retry the server start.

Example fix

// before (unreadable file in keys folder)
ls -l /etc/semaphore/encryption-keys
# -rw------- 1 root root key1  <- semaphore user cannot read
// after
sudo chown semaphore:semaphore /etc/semaphore/encryption-keys/*
sudo chmod 600 /etc/semaphore/encryption-keys/*  # with matching process user
Defensive patterns

Strategy: validation

Validate before calling

// Go: pre-flight every file in the keys folder before startup
entries, _ := os.ReadDir(folder)
for _, e := range entries {
    if strings.HasPrefix(e.Name(), ".") { continue }
    f, err := os.Open(filepath.Join(folder, e.Name()))
    if err != nil { return fmt.Errorf("key %q unreadable: %w", e.Name(), err) }
    f.Close()
}

Try / catch

if err := util.ReloadEncryptionKeys(); err != nil {
    log.Fatalf("encryption keys unreadable: %v", err)
}

Prevention

When it happens

Trigger: loadKeysFolder() iterates encryption.keys_folder; os.Stat succeeded on a regular file but the subsequent os.ReadFile failed (permissions changed between stat and read, file deleted by an atomic secret swap, NFS/disk I/O error).

Common situations: Kubernetes secret mounts where the symlink target was rotated between Stat and ReadFile; keys folder readable via symlink stat but the real file has restrictive ownership; disk errors on the mounted volume.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07). Data as JSON: /api/errors/0f864ae8c34c4a3c. Report an issue: GitHub.

Appendix: source

Thrown at util/config.go:1590

// skipped; symlinks (how K8s mounts secret files) are followed via Stat.
func loadKeysFolder(folder string, addLabeled func(string, string) error) error {
	entries, err := os.ReadDir(folder)
	if err != nil {
		return fmt.Errorf("encryption_keys.keys_folder %q: %w", folder, err)
	}
	for _, e := range entries {
		name := e.Name()
		if strings.HasPrefix(name, ".") {
			continue
		}
		path := filepath.Join(folder, name)
		info, err := os.Stat(path) // follow symlink
		if err != nil || !info.Mode().IsRegular() {
			continue
		}
		data, err := os.ReadFile(path)
		if err != nil {
			return fmt.Errorf("encryption_keys.keys_folder: read %q: %w", name, err)
		}
		if err := addLabeled(name, strings.TrimSpace(string(data))); err != nil {
			return fmt.Errorf("encryption_keys.keys_folder: key %q: %w", name, err)
		}
	}
	return nil
}

// EncryptionKeysFile returns the configured keys-file path (encryption.keys_file),
// or "" when no encryption section is configured.
func (conf *ConfigType) EncryptionKeysFile() string {
	if conf.Encryption == nil {
		return ""
	}
	return conf.Encryption.KeysFile
}

// EncryptionKeysPollInterval returns how often the keys file is polled for

View on GitHub (pinned to 1774ccb71a)