ssherlock
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 box —
linux,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 -Gconfig resolution, optionalknown_hostsverification. - 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: neverunless you opt a host intoknown_hosts— convenient for a trusted fleet, unsafe on untrusted networks. Withknown_hosts, ssherlock fails fast when~/.ssh/known_hostsis 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. Thessh2.crsession 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-sidetimeoutwrapping (wrap: true) is the primary guard;wrap: falsepresets (ESXi) rely on the caller-side guard alone.
ssherlock
- 0
- 0
- 0
- 0
- 6
- about 3 hours ago
- July 17, 2026
MIT License
Sat, 18 Jul 2026 14:07:14 GMT