{"record":{"id":"4e926f4df713bffd","repo":"sipeed/picoclaw","slug":"credential-ssh-private-key-is-required-but-not-fo","errorCode":null,"errorMessage":"credential: SSH private key is required but not found (set PICOCLAW_SSH_KEY_PATH or place key at ~/.ssh/picoclaw_ed25519.key)","messagePattern":"credential: SSH private key is required but not found \\(set PICOCLAW_SSH_KEY_PATH or place key at ~/\\.ssh/picoclaw_ed25519\\.key\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/credential/credential.go","lineNumber":289,"sourceCode":"\n\t// Within ~/.ssh/.\n\tif userHome, err := os.UserHomeDir(); err == nil {\n\t\tif isWithinDir(clean, filepath.Join(userHome, \".ssh\")) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// deriveKey derives a 32-byte AES-256 key from passphrase and SSH private key.\n//\n// ikm = HMAC-SHA256(key=SHA256(sshKeyBytes), msg=passphrase)\n// Final key: HKDF-SHA256(ikm, salt, info=\"picoclaw-credential-v1\", 32 bytes)\n// sshKeyPath must be non-empty; returns an error otherwise.\nfunc deriveKey(passphrase, sshKeyPath string, salt []byte) ([]byte, error) {\n\tif sshKeyPath == \"\" {\n\t\treturn nil, fmt.Errorf(\n\t\t\t\"credential: SSH private key is required but not found\" +\n\t\t\t\t\" (set PICOCLAW_SSH_KEY_PATH or place key at ~/.ssh/picoclaw_ed25519.key)\")\n\t}\n\tif !allowedSSHKeyPath(sshKeyPath) {\n\t\treturn nil, fmt.Errorf(\n\t\t\t\"credential: SSH key path %q is not in an allowed location (PICOCLAW_SSH_KEY_PATH, PICOCLAW_HOME, or ~/.ssh/)\",\n\t\t\tsshKeyPath,\n\t\t)\n\t}\n\tsshBytes, err := os.ReadFile(sshKeyPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"credential: cannot read SSH key %q: %w\", sshKeyPath, err)\n\t}\n\tsshHash := sha256.Sum256(sshBytes)\n\tmac := hmac.New(sha256.New, sshHash[:])\n\tmac.Write([]byte(passphrase))\n\tikm := mac.Sum(nil)\n","sourceCodeStart":271,"sourceCodeEnd":307,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/credential/credential.go#L271-L307","documentation":"deriveKey requires a non-empty sshKeyPath because the AES key is derived from HMAC(SHA256(sshKeyBytes), passphrase). Encrypt/resolveEncrypted resolve the path via pickSSHKeyPath (explicit argument, then PICOCLAW_SSH_KEY_PATH, then auto-detected ~/.ssh/picoclaw_ed25519.key). This error means all three sources came up empty, so no KDF input beyond the passphrase exists and the library refuses to encrypt or decrypt.","triggerScenarios":"Calling Encrypt (or resolving an ENC- credential) when PICOCLAW_SSH_KEY_PATH is unset AND ~/.ssh/picoclaw_ed25519.key does not exist (findDefaultSSHKey's os.Stat fails). Also fires when PICOCLAW_SSH_KEY_PATH is explicitly exported as the empty string - pickSSHKeyPath honors the set-but-empty value and returns \"\".","commonSituations":"Fresh install where the keygen bootstrap step was never run; daemon running as root while the key was generated for a normal user (HOME points to /root); containers or CI images with no ~/.ssh; HOME unset so DefaultSSHKeyPath fails inside findDefaultSSHKey; key deleted after credentials were created.","solutions":["Generate the key: run the picoclaw keygen command or call credential.GenerateSSHKey on the default path (creates ~/.ssh/picoclaw_ed25519.key with 0600)","Or point to an existing key: export PICOCLAW_SSH_KEY_PATH=/path/to/private_key in the daemon's environment","If the key was created under another account, copy it to the running user's ~/.ssh/picoclaw_ed25519.key (keep 0600)","Check `echo $HOME` for the service user and that $HOME/.ssh is where you think it is"],"exampleFix":"// before: Encrypt on a machine with no key configured\nenc, err := credential.Encrypt(passphrase, \"\", secret)\n\n// after: bootstrap the key once, then encrypt\nif _, err := os.Stat(keyPath); os.IsNotExist(err) {\n    keyPath, err = credential.DefaultSSHKeyPath()\n    if err != nil {\n        return err\n    }\n    if err := credential.GenerateSSHKey(keyPath); err != nil {\n        return err\n    }\n}\nenc, err := credential.Encrypt(passphrase, keyPath, secret)","handlingStrategy":"validation","validationCode":"func ensureSSHKey() (string, error) {\n    if p := os.Getenv(\"PICOCLAW_SSH_KEY_PATH\"); p != \"\" {\n        if _, err := os.Stat(p); err != nil {\n            return \"\", fmt.Errorf(\"configured key missing: %w\", err)\n        }\n        return p, nil\n    }\n    p, err := credential.DefaultSSHKeyPath()\n    if err != nil {\n        return \"\", err\n    }\n    if _, err := os.Stat(p); os.IsNotExist(err) {\n        if err := credential.GenerateSSHKey(p); err != nil {\n            return \"\", err\n        }\n    }\n    return p, nil\n}","typeGuard":null,"tryCatchPattern":"if _, err := credential.Encrypt(pass, keyPath, secret); err != nil {\n    if strings.Contains(err.Error(), \"SSH private key is required\") {\n        // bootstrap: generate the default key and retry once\n        if p, gerr := credential.DefaultSSHKeyPath(); gerr == nil {\n            if gerr = credential.GenerateSSHKey(p); gerr == nil {\n                _, err = credential.Encrypt(pass, p, secret)\n            }\n        }\n    }\n    if err != nil {\n        return err\n    }\n}","preventionTips":["Run the keygen bootstrap during install/provisioning, before any credential is encrypted","Set PICOCLAW_SSH_KEY_PATH explicitly in service definitions instead of relying on home-dir auto-detection","Include the key in machine provisioning (config management) so fresh hosts are never keyless","Do not export PICOCLAW_SSH_KEY_PATH as an empty string - pickSSHKeyPath treats set-but-empty as final"],"tags":["credentials","ssh","configuration","encryption","bootstrap"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}