getsops/sops · error
could not create encrypted SSH identity: %w
Error message
could not create encrypted SSH identity: %w
What it means
sops wraps agessh.NewEncryptedSSHIdentity to turn an age-encrypted SSH private key file into an age identity. This error means the key file was read and the public key parsed, but the encrypted-SSH-identity object could not be constructed, almost always because the public key blob is not one of the supported SSH key types (ssh-ed25519 or RSA) or the blob is corrupt.
Source
Thrown at age/ssh_parse.go:76
id, err := agessh.ParseIdentity(contents)
if sshErr, ok := err.(*ssh.PassphraseMissingError); ok {
pubKey := sshErr.PublicKey
if pubKey == nil {
pubKey, err = readPublicKeyFile(keyPath)
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
- Replace the SSH key with an ed25519 (or RSA) key: ssh-keygen -t ed25519 and use the new key file
- Verify the key file is a complete, valid OpenSSH private key: ssh-keygen -y -f <keyfile> prints the public key without error
- Check that the age binary and sops versions support the key type; upgrade sops if using a newer key format
- Convert the workflow to a native age key (age-keygen) instead of SSH keys
Example fix
// before ssh-keygen -t ecdsa -f ~/.ssh/id_ecdsa # unsupported by age // after ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 # supported by agessh
Defensive patterns
Strategy: validation
Validate before calling
pub, err := os.ReadFile(keyPath)
if err != nil { return err }
out, err := exec.Command("ssh-keygen", "-y", "-f", keyPath).Output()
if err != nil { return fmt.Errorf("not a usable SSH private key: %w", err) }
kt := strings.Fields(string(out)) // must be ssh-ed25519 or ssh-rsa
if len(kt) < 2 || (kt[len(kt)-1] != "ssh-ed25519" && kt[len(kt)-1] != "ssh-rsa") {
return fmt.Errorf("unsupported key type %q for age SSH identity", kt[len(kt)-1])
} Type guard
func isAgeSupportedSSHKey(pubKeyLine string) bool {
f := strings.Fields(pubKeyLine)
return len(f) >= 2 && (f[len(f)-1] == "ssh-ed25519" || f[len(f)-1] == "ssh-rsa")
} Try / catch
i, err := loadAgeSSHIdentities(paths)
if err != nil {
var uerr *UnsupportedSSHKeyError
if errors.As(err, &uerr) { /* regenerate as ed25519 */ }
return err
} Prevention
- Generate age SSH identities only with ssh-keygen -t ed25519
- Validate keys with ssh-keygen -y before configuring sops
- Keep sops and age tooling up to date
When it happens
Trigger: Calling parseSSHIdentityFromPrivateKeyFile (via loadAgeSSHIdentities) on an SSH private key whose parsed public key is unsupported (e.g. ECDSA or ed25519-sk), or a pubKey/contents pair that fails agessh.NewEncryptedSSHIdentity validation.
Common situations: Pointing SOPS_AGE_SSH_PRIVATE_KEY_FILE or an ssh recipient/identity at an ECDSA SSH key; corrupted or truncated key file; a key generated with a newer/algorithms the age ssh library doesn't accept; using an OpenSSH key with unsupported cipher features.
Related errors
- malformed SSH identity in %q: %w
- failed to parse input as age-ssh public key: %w
- incorrect passphrase
- failed to decrypt identity file: %v
- could not read passphrase: %v
AI-assisted analysis of getsops/sops@13442bb981 (2026-09-01).
Data as JSON: /api/errors/fcee44c1cbf90e94.
Report an issue: GitHub.