sipeed/picoclaw · error
credential: SSH key path %q is not in an allowed location (P
Error message
credential: SSH key path %q is not in an allowed location (PICOCLAW_SSH_KEY_PATH, PICOCLAW_HOME, or ~/.ssh/)
What it means
Before reading the key file, deriveKey enforces allowedSSHKeyPath: the path must exactly match PICOCLAW_SSH_KEY_PATH, or sit inside PICOCLAW_HOME, or sit inside ~/.ssh/. Any other path is rejected without a file read. This is a deliberate security control (path whitelist) so key material never gets mixed from arbitrary attacker-influenced locations.
Source
Thrown at pkg/credential/credential.go:294
}
}
return false
}
// deriveKey derives a 32-byte AES-256 key from passphrase and SSH private key.
//
// 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, nilView on GitHub (pinned to 49183d7e8d)
Solutions
- Move the key under ~/.ssh/ (e.g. ~/.ssh/picoclaw_ed25519.key, mode 0600) - the simplest always-allowed location
- Or export PICOCLAW_SSH_KEY_PATH=/exact/path/to/key - an exact match with the env var is allowed
- Or place the key anywhere under PICOCLAW_HOME and keep that env var set for the daemon
- Update your code/config to stop passing paths outside these roots; the whitelist is intentional and not bypassable
Example fix
# before: key outside the whitelist export PICOCLAW_SSH_KEY_PATH=/opt/secrets/picoclaw.key # Encrypt(pass, "/opt/secrets/other.key", s) -> not in an allowed location # after: either move the key mv /opt/secrets/picoclaw.key ~/.ssh/picoclaw_ed25519.key # or make the passed path an exact match of the env var export PICOCLAW_SSH_KEY_PATH=/opt/secrets/picoclaw.key # Encrypt(pass, "/opt/secrets/picoclaw.key", s) -> allowed
Defensive patterns
Strategy: validation
Validate before calling
// Mirror of the library's whitelist: exact PICOCLAW_SSH_KEY_PATH match,
// inside PICOCLAW_HOME, or inside ~/.ssh/.
func keyPathAllowed(path string) bool {
if p := os.Getenv("PICOCLAW_SSH_KEY_PATH"); p != "" &&
filepath.Clean(path) == filepath.Clean(p) {
return true
}
if h := os.Getenv("PICOCLAW_HOME"); h != "" {
if rel, err := filepath.Rel(filepath.Clean(h), filepath.Clean(path)); err == nil && filepath.IsLocal(rel) {
return true
}
}
home, err := os.UserHomeDir()
if err != nil {
return false
}
sshDir := filepath.Join(home, ".ssh")
rel, err := filepath.Rel(sshDir, filepath.Clean(path))
return err == nil && filepath.IsLocal(rel)
} Type guard
func isSSHKeyPathRejected(err error) bool {
return err != nil && strings.Contains(err.Error(), "not in an allowed location")
} Prevention
- Standardize on one key location per fleet (~/.ssh/picoclaw_ed25519.key) and symlink if needed
- Never pass ad-hoc /tmp or cwd-relative key paths to Encrypt
- Document the three allowed roots wherever deployment docs mention SSH keys
- Remember the whitelist is a security control - do not try to work around it in tests by weakening it
When it happens
Trigger: Passing an explicit sshKeyPath to Encrypt that lives outside the whitelist (e.g. /tmp/key, /etc/picoclaw/key, ./keys/id_ed25519 relative to cwd); or setting PICOCLAW_HOME to a directory that does not contain the key you pass. Note allowedSSHKeyPath compares filepath.Clean values, so traversal tricks and unclean paths still get normalized before the check.
Common situations: Storing the key next to the app binary or in /var/lib/<app>/ and passing that path directly; team workflows that keep keys in a secrets dir like /opt/secrets; moving a deployment to a container where the key is mounted at /keys/picoclaw_ed25519.key.
Related errors
- credential: enc:// passphrase required
- credential: enc:// decryption failed (wrong passphrase or SS
- credential: SSH private key is required but not found (set P
- credential: file:// path escapes config directory
- credential: cannot read SSH key %q: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/8a2c590fde21232e.
Report an issue: GitHub.