getsops/sops · error

malformed SSH identity in %q: %w

Error message

malformed SSH identity in %q: %w

What it means

sops parses the SSH private key file with an SSH decoder to obtain its public key. This error means the file could not be parsed as a valid SSH private key at all, so no identity could be derived from it.

Source

Thrown at age/ssh_parse.go:81

			if err != nil {
				return nil, err
			}
		}
		passphrasePrompt := func() ([]byte, error) {
			pass, err := pluginTerminalUI.RequestValue("", fmt.Sprintf("Enter passphrase for %q:", keyPath), true)
			if err != nil {
				return nil, fmt.Errorf("could not read passphrase for %q: %v", keyPath, err)
			}
			return []byte(pass), nil
		}
		i, err := agessh.NewEncryptedSSHIdentity(pubKey, contents, passphrasePrompt)
		if err != nil {
			return nil, fmt.Errorf("could not create encrypted SSH identity: %w", err)
		}
		return i, nil
	}
	if err != nil {
		return nil, fmt.Errorf("malformed SSH identity in %q: %w", keyPath, err)
	}
	return id, nil
}

View on GitHub (pinned to 13442bb981)

Solutions

  1. Verify the path points to a valid SSH PRIVATE key (not .pub): run ssh-keygen -y -f <path>
  2. Check the file exists and is readable: ls -l / cat the file
  3. Decrypt/convert legacy PEM keys: ssh-keygen -p -f <keyfile> to re-save in the new OpenSSH format
  4. Regenerate the key if the file is corrupted: ssh-keygen -t ed25519

Example fix

// before
export SOPS_AGE_SSH_PRIVATE_KEY_FILE=~/.ssh/id_ed25519.pub  # public key, unparseable
// after
export SOPS_AGE_SSH_PRIVATE_KEY_FILE=~/.ssh/id_ed25519       # private key file
Defensive patterns

Strategy: validation

Validate before calling

info, err := os.Stat(keyPath)
if err != nil || info.IsDir() { return fmt.Errorf("SSH key path missing: %s", keyPath) }
head, _ := os.ReadFile(keyPath)
if !strings.Contains(string(head), "PRIVATE KEY") {
    return fmt.Errorf("%s does not look like a private key", keyPath)
}
if err := exec.Command("ssh-keygen", "-y", "-f", keyPath).Run(); err != nil {
    return fmt.Errorf("unparseable SSH private key: %w", err)
}

Type guard

func isSSHPrivateKeyFile(path string) bool {
    b, err := os.ReadFile(path)
    return err == nil && strings.Contains(string(b), "PRIVATE KEY")
}

Try / catch

id, err := parseSSHIdentityFromPrivateKeyFile(keyPath)
if err != nil {
    if strings.Contains(err.Error(), "malformed SSH identity") {
        return fmt.Errorf("check SOPS_AGE_SSH_PRIVATE_KEY_FILE points to a valid private key: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: parseSSHIdentityFromPrivateKeyFile reads keyPath, and the SSH decoder (x/ssh.ParseRawPrivateKey family) returns an error on the file contents — the file is not a valid PEM/OpenSSH private key, is encrypted in a way the parser rejects without a passphrase, or is truncated.

Common situations: Pointing SOPS_AGE_SSH_PRIVATE_KEY_FILE at a public key file, a config file, or a path that doesn't exist; file contains an old PEM-encrypted RSA key; corrupted download; wrong file (e.g. known_hosts) passed in.

Understand the failure class

Related errors


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