getsops/sops · error
failed to read '%s': %w
Error message
failed to read '%s': %w
What it means
unwrapIdentities reads an age identity file (decrypting it if needed) with io.ReadAll bounded to 16 MiB. Any read error before that limit is wrapped as "failed to read '%s': %w", naming the file location. It indicates the underlying reader failed while consuming the key file, not a key-matching problem.
Source
Thrown at age/encrypted_keys.go:124
return fileKey, err
}
func unwrapIdentities(location string, reader io.Reader, allowMultipleKeysPerLine bool) (ParsedIdentities, error) {
b := bufio.NewReader(reader)
p, _ := b.Peek(14) // length of "age-encryption" and "-----BEGIN AGE"
peeked := string(p)
switch {
// An age encrypted file, plain or armored.
case peeked == "age-encryption" || peeked == "-----BEGIN AGE":
var r io.Reader = b
if peeked == "-----BEGIN AGE" {
r = armor.NewReader(r)
}
const privateKeySizeLimit = 1 << 24 // 16 MiB
contents, err := io.ReadAll(io.LimitReader(r, privateKeySizeLimit))
if err != nil {
return nil, fmt.Errorf("failed to read '%s': %w", location, err)
}
if len(contents) == privateKeySizeLimit {
return nil, fmt.Errorf("failed to read '%s': file too long", location)
}
// We use Base32 encoding instead of Base64 encoding, since our GPG agent package percent-encodes
// the cache ID. Base64 has two characters ('+' and '/') that would end up as a longer sequence,
// whence using Base64 encoding can suddenly blow up the cache key to more than GPG's maximum of
// 50 characters.
// By using 25 bytes, that translate to 25 / 5 * 8 = 40 letters/digits, we have a cache key of
// length 47, whose percent-encoding always has 47 characters.
contentsHash := sha256.Sum256(contents)
cacheKey := fmt.Sprintf("SopsAge%s", base32.StdEncoding.EncodeToString(contentsHash[:25]))
IncorrectPassphrase := func() {
conn, err := gpgagent.NewConn()
if err != nil {
return
}
defer func(conn *gpgagent.Conn) {View on GitHub (pinned to 13442bb981)
Solutions
- Check the file path exists and is readable by the current user (permissions/ownership).
- Retry if the failure came from a transient network filesystem error.
- Inspect the wrapped cause (%w) to identify the underlying I/O failure and fix that source.
Example fix
// before $ ls -l /run/secrets/age-key # root-only readable // after $ chmod 600 /run/secrets/age-key && chown $USER /run/secrets/age-key
Defensive patterns
Strategy: validation
Validate before calling
info, err := os.Stat(location)
if err != nil {
return fmt.Errorf("identity file %s not accessible: %w", location, err)
}
if !info.Mode().IsRegular() || info.Size() > 1<<24 {
return fmt.Errorf("identity file %s is not a regular file or too large", location)
} Try / catch
keys, err := loadIdentities(loc)
if err != nil && strings.Contains(err.Error(), "failed to read") {
// check permissions/mount and retry once
} Prevention
- Check file permissions with os.Stat before loading identities
- Avoid reading identities from flaky network mounts; copy locally first
- Inspect the wrapped error cause before retrying
When it happens
Trigger: Loading identities via loadIdentities when io.ReadAll fails on the identity file's reader — permission errors, missing file opened lazily, network filesystem dropout, or a failing custom io.Reader.
Common situations: Wrong path or unreadable permissions on the file referenced by SOPS_AGE_KEY_FILE; SSH agent or key provider streams that error mid-read; NFS/network mounts dropping connection during read.
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
- failed to decrypt identity file: %v
- failed to copy age decrypted data into bytes.Buffer: %w
- failed to open %s file: %w
- failed to open file: %w
- incorrect passphrase
AI-assisted analysis of getsops/sops@13442bb981 (2026-09-01).
Data as JSON: /api/errors/ba64712d52d66c99.
Report an issue: GitHub.