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
- Regenerate the offending file's content with a valid key: `openssl rand -base64 32` and write only that value into the file.
- Remove non-key files (README, backups, editor swap files) from encryption.keys_folder — every non-dot file is treated as a key.
- Verify each file decodes to exactly 16, 24, or 32 bytes: `base64 -d <file> | wc -c`.
- 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
- Generate keys only with `openssl rand -base64 32`.
- Never hand-edit key files; automate rotation scripts to write exact base64 content.
- Add a CI/startup lint step that checks every file in the keys folder decodes to 16/24/32 bytes.
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
- : 'value' and 'file' are mutually exclusive
- access_key_encryption must be a valid base64 string
- access_key_encryption has invalid decoded length
- value of field ' ' is not valid: (Must match regex: ' ')
- : read key file
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)