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

  1. Verify the file exists at the exact path: `ls -l <path>` from inside the container
  2. Mount the secret/key file into the container at the configured path and fix the volume/secret reference
  3. Fix file permissions so the semaphore process user can read it (e.g. chmod 640, correct owner)
  4. 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

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


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 its

View on GitHub (pinned to 1774ccb71a)