semaphoreui/semaphore · error

encryption_keys.keys_folder

Error message

encryption_keys.keys_folder %q: %w

What it means

loadKeysFolder wraps the os.ReadDir failure when encryption_keys.keys_folder cannot be listed. The wrapped OS error indicates the folder does not exist, is not a directory, or is not readable by the semaphore process.

Solutions

  1. Create the folder or fix the path: `mkdir -p <folder>` and place key files there
  2. Mount the volume/secret at the exact configured keys_folder path in your deployment
  3. Fix directory permissions (read+execute for the semaphore process user)
  4. Verify from inside the container: `ls -la <folder>`

Example fix

# before
encryption_keys:
  keys_folder: /etc/semaphore/keys   # never mounted
# after (K8s)
volumes:
  - name: enc-keys
    secret: {secretName: semaphore-enc-keys}
volumeMounts:
  - {name: enc-keys, mountPath: /etc/semaphore/keys, readOnly: true}
encryption_keys:
  keys_folder: /etc/semaphore/keys
Defensive patterns

Strategy: validation

Validate before calling

if cfg.Encryption != nil && cfg.Encryption.KeysFolder != "" {
    info, err := os.Stat(cfg.Encryption.KeysFolder)
    if err != nil || !info.IsDir() {
        return fmt.Errorf("keys_folder %s is not a readable directory", cfg.Encryption.KeysFolder)
    }
}

Type guard

func isReadableDir(path string) bool {
    info, err := os.Stat(path)
    return err == nil && info.IsDir()
}

Try / catch

if err := loadKeysFolder(folder, addLabeled); err != nil {
    return fmt.Errorf("cannot load encryption keys: %w", err)
}

Prevention

When it happens

Trigger: keys_folder set to a path that was never created/mounted; path is a file, not a directory; permission denied for the process user; typo in the mounted volume path.

Common situations: Kubernetes secret volume mounted at a different mountPath than configured; Docker volume not mounted in the container; directory created only on the host, not in the image; wrong ownership after chown.

Related errors


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

Appendix: source

Thrown at util/config.go:1576

			return "", fmt.Errorf("encryption_keys.active.%s_key_file: %w", kind, err)
		}
		material := strings.TrimSpace(string(data))
		if err := addLabeled(file, material); err != nil {
			return "", err
		}
		return material, nil
	}

	return flat, nil
}

// loadKeysFolder reads every regular file in folder as one key, labelled by its
// filename. Dot-prefixed entries (e.g. Kubernetes' "..data" / "..2024_*") are
// 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)
		}

View on GitHub (pinned to 1774ccb71a)