semaphoreui/semaphore · error
: 'value' and 'file' are mutually exclusive
Error message
%s: 'value' and 'file' are mutually exclusive
What it means
resolveKeySource returns this error when a KeySource (e.g. an entry under encryption_keys.keys) specifies both an inline Value and a File. The two sources are mutually exclusive by design, so the configuration is rejected rather than guessing which to use.
Solutions
- Remove one of the two fields — keep `value:` for inline material or `file:` for a key path
- Check Helm/config templates so defaults do not inject `file` when you set `value` (or vice versa)
- Re-run config validation after editing to confirm only one source remains per key
Example fix
# before
encryption_keys:
keys:
primary:
value: "k1"
file: /etc/semaphore/primary.key
# after
encryption_keys:
keys:
primary:
file: /etc/semaphore/primary.key Defensive patterns
Strategy: validation
Validate before calling
for label, ks := range cfg.Encryption.Keys {
if ks.Value != "" && ks.File != "" {
return fmt.Errorf("key %q sets both value and file", label)
}
} Try / catch
material, err := resolveKeySource(ks, name)
if err != nil {
return fmt.Errorf("key source %s misconfigured: %w", name, err)
} Prevention
- Choose one key source per entry: inline value OR file, never both
- Audit Helm/Ansible templates for defaults that inject the other field
- Validate the encryption config before applying it to production
When it happens
Trigger: Config with `encryption_keys.keys.<label>: {value: ..., file: ...}` both set; templating that merges defaults (file) with overrides (value) leaving both populated; hand-editing a keys file adding a value while file remains.
Common situations: Kubernetes-managed keys files combined with a manually added inline key; Helm values where both key/value and key/file got set; copy-pasting a config example and only partially removing the other field.
Related errors
- value of field ' ' is not valid: (Must match regex: ' ')
- : read key file
- encryption_keys.active.%s_key: no key labelled
- encryption_keys.active.%s_key_file
- encryption_keys.keys_folder
AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07).
Data as JSON: /api/errors/2940433af8e36d2e.
Report an issue: GitHub.
Appendix: source
Thrown at util/config.go:1443
if strings.Contains(fieldName, "password") || strings.Contains(fieldName, "secret") || strings.Contains(fieldName, "key") {
strVal = "***"
}
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{},View on GitHub (pinned to 1774ccb71a)