cfgaudit

Rules / CFG044

CFG044 permissions.deny does not restrict SSH private keys

Severity: error OWASP: LLM02:2026 – Sensitive Information Disclosure

What cfgaudit checks

SSH private keys grant access to remote systems and are a prime exfiltration target. cfgaudit flags a permissions.deny block that exists but does not cover them. Coverage is satisfied by either:

A completely absent permissions.deny block is reported by CFG006, not here.

A deny-all entry suppresses this finding entirely: Read(*) / Read(**) / Read(**/*) (any version), or the bare "*" deny-all-tools glob on Claude Code ≥ 2.1.166 (a no-op on older releases, so the finding still fires there; treated as covering when the version is unknown).

An exception rule (Read(!<glob>)) denies nothing and removes the paths it matches from the deny rules before it, so it does not count as coverage; a deny-all with the SSH key paths carved back out leaves them readable and the finding fires.

Trigger example

{
  "permissions": {
    "deny": ["Read(.env)", "Read(**/*.pem)"]
  }
}

→ error (no .ssh / id_* coverage).

Anchor the pattern at the filesystem root

A path rule's anchor decides what it can reach, and the bare **/ form does not reach the home directory. Measured against Claude Code 2.1.231 with a marker file placed both inside and outside a project:

Rule in the project's .claude/settings.json <project>/zzsecret/marker <outside>/zzsecret/marker
Read(**/zzsecret/**) blocked read succeeded
Read(//**/zzsecret/**) blocked blocked

So Read(**/.ssh/**) in a committed project file protects a .ssh directory that happens to sit inside the repository, and leaves ~/.ssh/id_rsa readable, which is the file the rule exists for. Upstream states the same rule: "A deny rule such as Read(/secrets/**) in user settings blocks ~/.claude/secrets/**, not a secrets directory in your project. To write a rule in user settings that applies inside every project, use a // absolute path or a ~/ home-relative path instead."

cfgaudit still accepts the bare form as coverage, because it does restrict something and reporting it would be a second finding on a file that already tried. The suggestion it prints is the anchored one.

Safe alternative

{ "permissions": { "deny": ["Read(//**/.ssh/**)"] } }

or explicitly:

{
  "permissions": {
    "deny": ["Read(**/id_rsa)", "Read(**/id_ed25519)", "Read(**/id_ecdsa)", "Read(**/id_dsa)"]
  }
}

References

Documentation for cfgaudit 1.18.1. Source of this page · Rule implementation