getsops/sops · error

failed to open file: %w

Error message

failed to open file: %w

What it means

The default age key file at <userConfigDir>/sops/age/keys.txt exists as a path but could not be opened — the open failed with something other than ErrNotExist (e.g. a permissions error, or the path is a directory). This is a candidate-location failure collected during loadIdentities.

Source

Thrown at age/keysource.go:461

			errs = append(errs, err)
		} else {
			readers[SopsAgeKeyCmdEnv] = identityReader{
				reader:                   bytes.NewReader(out),
				allowMultipleKeysPerLine: false,
			}
		}
	} else {
		unusedLocations = append(unusedLocations, SopsAgeKeyCmdEnv)
	}

	userConfigDir, err := getUserConfigDir()
	if err != nil && len(readers) == 0 && len(identities) == 0 {
		errs = append(errs, fmt.Errorf("user config directory could not be determined: %w", err))
	} else if userConfigDir != "" {
		ageKeyFilePath := filepath.Join(userConfigDir, filepath.FromSlash(SopsAgeKeyUserConfigPath))
		f, err := os.Open(ageKeyFilePath)
		if err != nil && !errors.Is(err, os.ErrNotExist) {
			errs = append(errs, fmt.Errorf("failed to open file: %w", err))
		} else if errors.Is(err, os.ErrNotExist) && len(readers) == 0 && len(identities) == 0 {
			unusedLocations = append(unusedLocations, ageKeyFilePath)
		} else if err == nil {
			defer f.Close()
			readers[ageKeyFilePath] = identityReader{
				reader:                   f,
				allowMultipleKeysPerLine: false,
			}
		}
	}

	for location, r := range readers {
		ids, err := unwrapIdentities(location, r.reader, r.allowMultipleKeysPerLine)
		if err != nil {
			errs = append(errs, err)
		} else {
			identities = append(identities, ids...)
			if len(ids) == 0 {

View on GitHub (pinned to 13442bb981)

Solutions

  1. Check ownership and permissions: ls -l ~/.config/sops/age/keys.txt and chmod 600 with correct owner.
  2. Verify the path is a regular file, not a directory (the error 'is a directory' means a mount/volume collision).
  3. If run under a service account, grant that user read access instead of relying on your own shell user.
  4. In containers, ensure the secret is mounted as a file (subPath in Kubernetes) at ~/.config/sops/age/keys.txt.
  5. Fallback: point SOPS_AGE_KEY_FILE directly at the keys file to bypass the default-location probe.

Example fix

# before
$ ls -l ~/.config/sops/age/keys.txt
-rw------- 1 root root ... keys.txt   # owned by root, sops runs as app
// failed to open file: open /home/app/.config/sops/age/keys.txt: permission denied

# after
sudo chown app:app ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt
Defensive patterns

Strategy: validation

Validate before calling

// shell: validate the default key file before invoking sops
KEY="$HOME/.config/sops/age/keys.txt"
[ -f "$KEY" ] && [ -r "$KEY" ] && [ ! -d "$KEY" ] \
  || { echo "age key file unusable: $KEY"; exit 1; }

Try / catch

if err := runSopsDecrypt(); err != nil &&
   strings.Contains(err.Error(), "failed to open file") {
    return fmt.Errorf("default age keys.txt unreadable; check owner/perms or set SOPS_AGE_KEY_FILE: %w", err)
}

Prevention

When it happens

Trigger: loadIdentities builds ageKeyFilePath from getUserConfigDir plus SopsAgeKeyUserConfigPath, calls os.Open, and gets a non-ErrNotExist error (EACCES, EISDIR, too many open files, etc.).

Common situations: keys.txt copied with root ownership (chmod'd wrong) after sudo provisioning; secrets mounted as directories in Kubernetes/containers instead of files; keys.txt created with 000 permissions.

Understand the failure class

Background: "open() failed", "failed to open file", "cannot create file" — what a file open error means and how to fix it — this error's family across 42 libraries.

Related errors


AI-assisted analysis of getsops/sops@13442bb981 (2026-09-01). Data as JSON: /api/errors/9ce482009ce2e9ff. Report an issue: GitHub.