semaphoreui/semaphore · error

encryption_keys.keys_folder: key

Error message

encryption_keys.keys_folder: key %q: %w

What it means

After successfully reading a file from encryption.keys_folder, the loader passes its trimmed content to addLabeled, which validates the key material (base64/AES size). If validation fails the error is wrapped as 'encryption_keys.keys_folder: key %q: %w', naming the offending file. The keyring cannot be built and the server fails fast at startup.

Solutions

  1. Regenerate the offending file's content with a valid key: `openssl rand -base64 32` and write only that value into the file.
  2. Remove non-key files (README, backups, editor swap files) from encryption.keys_folder — every non-dot file is treated as a key.
  3. Verify each file decodes to exactly 16, 24, or 32 bytes: `base64 -d <file> | wc -c`.
  4. Strip any BOM or formatting: the content must be a single base64 string; use `printf '%s' "$(cat file)" > file` to normalize.

Example fix

// before: keys folder contains a passphrase file
$ cat /etc/semaphore/encryption-keys/mykey
my-secret-passphrase
// after
$ openssl rand -base64 32 > /etc/semaphore/encryption-keys/mykey
$ base64 -d /etc/semaphore/encryption-keys/mykey | wc -c  # must print 32
Defensive patterns

Strategy: validation

Validate before calling

// Validate each key file before the server starts
for _, f := range keyFiles {
    data, _ := os.ReadFile(f)
    raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(data)))
    if err != nil || (len(raw) != 16 && len(raw) != 24 && len(raw) != 32) {
        return fmt.Errorf("key %q invalid: decoded %d bytes", f, len(raw))
    }
}

Try / catch

if err := util.ReloadEncryptionKeys(); err != nil {
    log.Fatalf("invalid key in keys_folder: %v", err)
}

Prevention

When it happens

Trigger: loadKeysFolder() calls addLabeled(name, strings.TrimSpace(string(data))) for a file in encryption.keys_folder and the file's content is not a valid key (not base64, or decoded length not 16/24/32 bytes).

Common situations: A file placed in the keys folder contains a raw passphrase, a PEM block, trailing notes/comments, or a key generated with the wrong byte length; a README or backup file was accidentally left in the folder; an editor added whitespace/BOM (whitespace is trimmed, BOM is not).

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at util/config.go:1593

	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
// changes. It defaults to 15s, and returns 0 when polling is disabled
// (encryption.keys_poll_interval set to "0"). An unparseable value falls back to
// the default.

View on GitHub (pinned to 1774ccb71a)