ssherlock

Read-only SSH fleet audit collector — runs preset-based command catalogues over SSH and writes one JSON report per host. Single self-contained binary.

ssherlock

Point it at your fleet. It logs in read-only over SSH, runs a curated catalogue of inspection commands, and hands you one JSON file per host.

A single self-contained binary — no agent to deploy, nothing to install on the targets. ssherlock connects over SSH (agent / ssh_config / known_hosts aware), runs preset-based command catalogues tuned per platform (Linux, Proxmox, VMware ESXi), and writes structured JSON you can diff, grep, or feed to whatever comes next.

Everything it runs is read-only inventory and posture collection — lscpu, ss -tlnp, pveversion, esxcli … — never a mutation.

Highlights

  • One static binary. Presets are baked in; drop it on a jump host and go.
  • Platform presets out of the boxlinux, linux-full, proxmox, vmware.
  • RuboCop-style check management — disable, retune or add checks per preset straight from your fleet file, without ever touching the presets.
  • Fleet-parallel with bounded concurrency and per-host / per-command timeouts.
  • Bastion / jump-host relay, ssh -G config resolution, optional known_hosts verification.
  • Secret redaction (--redact) masks PEM keys, password=… pairs and bearer tokens before anything hits disk.

Usage

ssherlock run                      # collect the whole fleet from ./ssherlock.yml
ssherlock run -c infra.yml         # use a different config file
ssherlock run web01 db01           # only these machines (by label or host)
ssherlock run --redact             # mask secrets in the output
ssherlock presets                  # list the baked presets
ssherlock presets linux            # dump a resolved preset as YAML
ssherlock licenses                 # print bundled third-party licenses

The config defaults to ssherlock.yml in the working directory; point elsewhere with --config/-c. Positional arguments are targets — each matched against a server's label or host — so you can audit a single machine without editing the fleet file; an unknown target is an error. Output lands in output_dir (default audit_<date>/): <label>.json per host plus an aggregated all.json.

The fleet file

output_dir: audit_2026-07-17
concurrency: 6             # parallel hosts, clamped to 1..32

# Applied to every server, then overridden per entry.
defaults:
  inherit: linux           # which preset a host runs
  ssh:
    user: root
    key: ~/.ssh/id_rsa
    config: true           # honour ~/.ssh/config via `ssh -G` (default true)
    verify_host_key: never # or `known_hosts`
    timeout: 10            # connect timeout, seconds
  cmd_timeout: 20          # per-command timeout, seconds
  sudo: false

servers:
  - { host: web01, label: web01 }
  - { host: pve01, label: pve01, inherit: proxmox }
  - { host: esx01, label: esx01, inherit: vmware, ssh: { user: admin } }
  - { host: db01,  label: db01,  inherit: linux-full, sudo: true, ssh: { bastion: jump@edge } }

Each server needs a host and a unique label; everything else falls back to defaults. inherit picks the preset the host runs.

Checks, RuboCop-style

Think of it the way RuboCop thinks about cops. The presets shipped in the binary are the default rule packs; your fleet file's overrides: block is your .rubocop.yml — it layers on top of a preset, keyed by preset name, to disable, retune or add individual checks. The presets themselves stay untouched.

A preset is a two-level catalogue — section → check — where each check is a description plus a command:

checks:
  cpu:
    lscpu:
      description: CPU topology and instruction-set flags.
      command: lscpu

Disable a check

Set it to null (~) to knock it out — the analogue of Cop: { Enabled: false }:

overrides:
  linux:
    services:
      packages: ~        # drop the linux/services.packages check

Retune a check

Redeclare it — your keys deep-merge over the preset's, so you can swap the command or reword the description while keeping everything else:

overrides:
  linux:
    storage:
      df:
        command: df -hT -x tmpfs -x devtmpfs -x overlay

Add your own check

Declare a check the preset doesn't have; missing sections and checks are created on the fly. A command is required, description is optional:

overrides:
  linux:
    cpu:
      logical_count:                 # new check in an existing section
        description: All-logical-CPU count.
        command: nproc --all
    custom:                          # brand-new section
      reboot_pending:
        description: Kernel reboot flag.
        command: '[ -f /var/run/reboot-required ] && echo yes || echo no'

Overrides are scoped to the preset name, so overrides: { proxmox: { … } } only touches hosts that inherit: proxmox.

Preset inheritance

Presets can inherit from one another with a top-level inherit: — that's how linux-full extends linux with SSH-hardening, firewall, update and log checks. Child checks deep-merge over the parent, and a null child knocks a parent check out. Same merge rules as your overrides, one level up.

Defining your own preset

A preset is a full named catalogue (options + checks) you can inherit: and select per host — the level above a per-check override. You can define one without rebuilding, in either of two places, and ssherlock resolves a preset name through this chain, first source wins:

inline presets: > external directory > baked catalogue.

Inline, straight in the fleet file — handy for a small derived preset:

presets:
  webnode:
    inherit: linux          # extend a baked (or dir, or inline) preset
    options: { wrap: true }
    checks:
      web:
        nginx_version:
          description: Installed nginx build.
          command: nginx -v 2>&1

servers:
  - { host: web01, label: web01, inherit: webnode }

Or in an external directory of <name>.yml files, pointed at by the presets_dir: key or the --presets-dir DIR flag (the flag wins over the key). A relative path resolves against the config file's directory; an absolute path is used as-is:

presets_dir: ./presets     # ./presets/freebsd.yml → inherit: freebsd
ssherlock run -c infra.yml --presets-dir /etc/ssherlock/presets

A preset that shadows an existing name (inline or dir preset named like a baked one) replaces it — but it must not inherit: that same name, or the resolver rejects the self-cycle. To extend a baked preset, inherit it under a new name.

The baked catalogue itself lives in config/presets/*.yml and is embedded at build time; changing the shipped defaults still means a rebuild, but everyday customisation belongs in your overrides, inline presets: or a presets_dir.

Known limitations

  • Host key verification defaults to off. Hosts connect publickey-only with verify_host_key: never unless you opt a host into known_hosts — convenient for a trusted fleet, unsafe on untrusted networks. With known_hosts, ssherlock fails fast when ~/.ssh/known_hosts is missing rather than letting every host fail auth one by one.
  • A frozen command leaves a background fiber until the host disconnects. Each check is guarded by a caller-side timeout (cmd_timeout + 5s), but Crystal cannot cancel a fiber blocked on a frozen SSH read; the fiber parks until the host's session closes at the end of its checks. The ssh2.cr session releases its lock while waiting, so the remaining checks on that host still run — but one hung read costs a parked fiber and an open channel for the rest of that host's collection. Server-side timeout wrapping (wrap: true) is the primary guard; wrap: false presets (ESXi) rely on the caller-side guard alone.
Repository

ssherlock

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 6
  • about 3 hours ago
  • July 17, 2026
License

MIT License

Links
Synced at

Sat, 18 Jul 2026 14:07:14 GMT

Languages