semaphoreui/semaphore · error
: read key file
Error message
%s: read key file %q: %w
What it means
resolveKeySource wraps the os.ReadFile failure when a KeySource specifies a `file` path that cannot be read. The %w preserves the underlying OS error (ENOENT, permission denied, is-a-directory), so the message reads `<name>: read key file "<path>": <os error>`.
Solutions
- Verify the file exists at the exact path: `ls -l <path>` from inside the container
- Mount the secret/key file into the container at the configured path and fix the volume/secret reference
- Fix file permissions so the semaphore process user can read it (e.g. chmod 640, correct owner)
- Use an absolute path, or confirm the process working directory for relative paths
Example fix
# before
keys:
primary:
file: ./secrets/primary.key # not mounted in container
# after
keys:
primary:
file: /etc/semaphore/keys/primary.key Defensive patterns
Strategy: validation
Validate before calling
if ks.File != "" {
if _, err := os.Stat(ks.File); err != nil {
return fmt.Errorf("key file %s unreadable before start: %w", ks.File, err)
}
} Type guard
func readableFile(path string) bool {
f, err := os.Open(path)
if err != nil { return false }
f.Close()
return true
} Try / catch
material, err := resolveKeySource(ks, name)
if err != nil {
log.Fatalf("key material unavailable (%s): %v — check mount and permissions", name, err)
} Prevention
- Use absolute paths for key files in containerized deployments
- Verify secret mounts exist inside the container, not just on the host
- Set file ownership/permissions readable by the semaphore process user
- Test key loading after every deployment or secret rotation
When it happens
Trigger: encryption_keys.keys.<label>.file pointing at a non-existent path, a path the semaphore process cannot read, a directory instead of a file, or a path valid on the operator's machine but not inside the container.
Common situations: Docker/K8s secret not mounted at the configured path; wrong relative path (resolved against process CWD, not config dir); permissions after running as non-root; secret name typo in the deployment.
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
- encryption_keys.active.%s_key_file
- encryption_keys.keys_folder
- : 'value' and 'file' are mutually exclusive
- encryption_keys.active.%s_key: no key labelled
- encryption_keys.keys_folder: read
AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07).
Data as JSON: /api/errors/06f9c9983de454af.
Report an issue: GitHub.
Appendix: source
Thrown at util/config.go:1448
return fmt.Errorf(
"value of field '%v' is not valid: %v (Must match regex: '%v')",
fieldType.Name, strVal, rule,
)
}
return nil
}
// resolveKeySource returns the key material from a KeySource: the inline Value,
// or the trimmed contents of File. Value and File are mutually exclusive.
func resolveKeySource(ks KeySource, name string) (string, error) {
if ks.Value != "" && ks.File != "" {
return "", fmt.Errorf("%s: 'value' and 'file' are mutually exclusive", name)
}
if ks.File != "" {
data, err := os.ReadFile(ks.File)
if err != nil {
return "", fmt.Errorf("%s: read key file %q: %w", name, ks.File, err)
}
return strings.TrimSpace(string(data)), nil
}
return ks.Value, nil
}
// resolveEncryptionKeysFrom builds the runtime keyset from the keys-file config
// plus the legacy flat fields, validating every resolved key. It does not mutate
// global state. The flat fields are added to the registry (so new writes can stamp
// them) and recorded as the legacy no-prefix decrypt keys.
func resolveEncryptionKeysFrom(enc *EncryptionKeysConfig, flatAccess, flatOption string) (*keyset, error) {
ks := &keyset{
byID: map[string]string{},
legacyAccess: flatAccess,
legacyOption: flatOption,
}
// byLabel maps a human label (inline keys map key, or a folder filename) to itsView on GitHub (pinned to 1774ccb71a)