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, nil

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Move the key under ~/.ssh/ (e.g. ~/.ssh/picoclaw_ed25519.key, mode 0600) - the simplest always-allowed location
  2. Or export PICOCLAW_SSH_KEY_PATH=/exact/path/to/key - an exact match with the env var is allowed
  3. Or place the key anywhere under PICOCLAW_HOME and keep that env var set for the daemon
  4. 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

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


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/8a2c590fde21232e. Report an issue: GitHub.