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

  1. Remove one of the two fields — keep `value:` for inline material or `file:` for a key path
  2. Check Helm/config templates so defaults do not inject `file` when you set `value` (or vice versa)
  3. 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

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


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)