sipeed/picoclaw · error
credential: cannot read SSH key %q: %w
Error message
credential: cannot read SSH key %q: %w
What it means
deriveKey passed the whitelist check but os.ReadFile(sshKeyPath) still failed. The original *PathError is wrapped with %w, so errors.Is(err, fs.ErrNotExist) / os.IsPermission(err) work on the returned error. This is a plain filesystem failure at the moment of reading, distinct from the path-policy error at line 294.
Source
Thrown at pkg/credential/credential.go:301
//
// ikm = HMAC-SHA256(key=SHA256(sshKeyBytes), msg=passphrase)
// Final key: HKDF-SHA256(ikm, salt, info="picoclaw-credential-v1", 32 bytes)
// sshKeyPath must be non-empty; returns an error otherwise.
func deriveKey(passphrase, sshKeyPath string, salt []byte) ([]byte, error) {
if sshKeyPath == "" {
return nil, fmt.Errorf(
"credential: SSH private key is required but not found" +
" (set PICOCLAW_SSH_KEY_PATH or place key at ~/.ssh/picoclaw_ed25519.key)")
}
if !allowedSSHKeyPath(sshKeyPath) {
return nil, fmt.Errorf(
"credential: SSH key path %q is not in an allowed location (PICOCLAW_SSH_KEY_PATH, PICOCLAW_HOME, or ~/.ssh/)",
sshKeyPath,
)
}
sshBytes, err := os.ReadFile(sshKeyPath)
if err != nil {
return nil, fmt.Errorf("credential: cannot read SSH key %q: %w", sshKeyPath, err)
}
sshHash := sha256.Sum256(sshBytes)
mac := hmac.New(sha256.New, sshHash[:])
mac.Write([]byte(passphrase))
ikm := mac.Sum(nil)
key, err := hkdf.Key(sha256.New, ikm, salt, hkdfInfo, keyLen)
if err != nil {
return nil, fmt.Errorf("credential: HKDF expand failed: %w", err)
}
return key, nil
}
// pickSSHKeyPath returns the SSH private key path to use for encryption/decryption.
//
// Priority:
// 1. override (non-empty explicit argument)
// 2. PICOCLAW_SSH_KEY_PATH env varView on GitHub (pinned to 49183d7e8d)
Solutions
- Inspect the path: `ls -l <path>` - confirm it exists and shows mode 0600
- Fix ownership/permissions to the daemon user: `chown <svcuser> <key> && chmod 600 <key>`
- If the key is genuinely gone, regenerate it (credential.GenerateSSHKey) and re-encrypt credentials encrypted with the old key - old blobs will no longer decrypt
- If HOME or the env var points somewhere unexpected, re-check PICOCLAW_SSH_KEY_PATH / $HOME for the service context
Example fix
// before: assumes key is readable
enc, err := credential.Encrypt(pass, keyPath, secret)
// after: pre-flight read check with a precise diagnosis
if fi, err := os.Stat(keyPath); err != nil {
return fmt.Errorf("key path unusable: %w", err)
} else if fi.Mode().Perm()&0o077 != 0 {
return fmt.Errorf("key %s is group/world accessible; chmod 600", keyPath)
}
enc, err := credential.Encrypt(pass, keyPath, secret) Defensive patterns
Strategy: validation
Validate before calling
func keyReadable(path string) error {
fi, err := os.Stat(path)
if err != nil {
return fmt.Errorf("SSH key stat: %w", err)
}
if fi.IsDir() {
return fmt.Errorf("SSH key path %s is a directory", path)
}
if fi.Mode().Perm()&0o400 == 0 {
return fmt.Errorf("SSH key %s not readable by uid %d", path, os.Getuid())
}
return nil
} Type guard
import "io/fs"
func classifyKeyReadError(err error) string {
switch {
case errors.Is(err, fs.ErrNotExist):
return "missing"
case errors.Is(err, fs.ErrPermission):
return "permission"
default:
return "other"
}
} Try / catch
if _, err := credential.Encrypt(pass, keyPath, secret); err != nil {
var keyErr error
if strings.Contains(err.Error(), "cannot read SSH key") {
keyErr = errors.Unwrap(errors.Unwrap(err)) // *fs.PathError
}
// decide: regenerate key (missing) vs fix perms (permission) vs abort
} Prevention
- Keep key files 0600 and owned by the uid running the daemon
- Re-run keygen on hosts where the key was lost, then re-encrypt dependent credentials
- Audit key files after user/uid changes in systemd units
- Avoid network home directories for key material where unmounts are common
When it happens
Trigger: Encrypt or decrypt when the key file is deleted or renamed between path selection and read; key mode 0600 owned by a different uid (EACCES) - typical when the daemon runs as root but the key belongs to a user; path points at a directory; dangling symlink; key on an unmounted network volume.
Common situations: Service user changed after setup (systemd User= directive added); key regenerated for another user; NFS/home unmounted on a laptop; another admin rotated keys and removed the old file; containers where the key volume is mounted read-only at a different uid.
Related errors
- mkdir media dir: %w
- credential: failed to read credential file %q: %w
- credential: keygen: cannot create directory %q: %w
- failed to load credentials: %v
- create output dir: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/6e65c9d3139942af.
Report an issue: GitHub.