From source to verdict, then to proof

Install the verified binary, run your first A to E scan, then harden a target and re-scan. From download to verdict in a few commands, with no toolchain to install.

In short: from zero to a verdict

The shortest path, on the current host (effective config needs root). Three things, in this order: the binary, the scan engine, the scan. Nothing to compile, the binary embeds its rule corpus.

# 1. the binary, verified
BASE=https://github.com/stephrobert/pavois/releases/download/v0.1.1
curl -fsSLO $BASE/pavois-linux-amd64 && curl -fsSLO $BASE/checksums.txt
sha256sum --ignore-missing --check checksums.txt   # integrity
sudo install -m 0755 pavois-linux-amd64 /usr/local/bin/pavois
pavois doctor                            # corpus embedded, ready to scan

# 2. the engine: Pavois runs its controls with CINC Auditor
META="https://omnitruck.cinc.sh/stable/cinc-auditor/metadata?p=el&pv=9&m=x86_64"
URL=$(curl -sS "$META" | awk '/^url/{print $2}')     # the package for that platform
SUM=$(curl -sS "$META" | awk '/^sha256/{print $2}')  # and its published checksum
curl -fsSLO "$URL"
echo "$SUM  ${URL##*/}" | sha256sum --check         # integrity, before anything runs
sudo dnf install -y "./${URL##*/}"                  # Debian/Ubuntu: sudo apt install ./<file>
pavois doctor                                        # CINC engine (native): /usr/bin/cinc-auditor

# 3. the scan
pavois scan local --sudo --format html   # audit this host, A to E grade
pavois serve                             # http://localhost:8098

Step 2 is not optional and no package manager can do it for you: CINC Auditor is in no distribution repository, so even Pavois’s own .deb and .rpm cannot declare it as a dependency. Without it the first scan stops on no native CINC engine found, which reads as a missing dependency when nothing is missing from the package. The detail, the other platforms and the Docker fallback: the scan engine.

Downloading a file and then verifying its checksum is not piping a script into a shell. Pavois will never ask you to curl | sh: a hardening tool whose first line is that has already lost the argument. Here nothing runs until sha256sum has confirmed the published checksum, and the installation page adds the SLSA provenance check, which additionally proves WHO built the file. .deb and .rpm packages are in the same place.

Prerequisites

To install Pavois: curl, and nothing else. The binary is static and embeds its rule corpus, so there is no toolchain, no Go and no Python to set up. mise only comes in if you are changing Pavois, which is covered at the end of this page.

To scan, you also need the engine, and it is the one prerequisite nothing installs for you. Pavois runs its controls with CINC Auditor (the open-source build of Chef InSpec), which is in no distribution repository: no package manager can fetch it, and Pavois never installs it on its own. The command is in step 1 below. Without it, remote targets fall back to a Docker container and local refuses (a container cannot audit its host). On a remote target, --on-target installs the engine only when you pass --bootstrap-cinc. Finally, reading a service’s effective config needs root, so sudo, and a remote target needs an SSH key.

1. Install Pavois and its scan engine

Each release ships a static binary per platform (Linux and macOS, amd64 and arm64), .deb and .rpm packages, a checksums.txt, an SBOM and a SLSA provenance attestation. The binary is self-contained: the rule corpus is inside it, there is nothing to generate.

BASE=https://github.com/stephrobert/pavois/releases/download/v0.1.1
curl -fsSLO $BASE/pavois-linux-amd64 && curl -fsSLO $BASE/checksums.txt
sha256sum --ignore-missing --check checksums.txt   # integrity
sudo install -m 0755 pavois-linux-amd64 /usr/local/bin/pavois
pavois doctor                            # corpus embedded, ready to scan

sha256sum confirms integrity against the published checksums. It does not say WHO built the file: someone replacing both the binary AND checksums.txt produces a perfectly consistent pair. Answering that needs the SLSA provenance attestation, and it is the only thing GitHub CLI adds here, on a binary you have already downloaded:

gh attestation verify pavois-linux-amd64 --repo stephrobert/pavois   # who built it

Optional but recommended. It queries a public transparency log, so it needs no access to the repository. The .deb and .rpm packages, the other platforms and troubleshooting: installation page.

Then the scan engine

The binary alone can re-read an archived report, not audit a machine. For that it needs CINC Auditor, and installing it is on you: it is in no distribution repository, so no package manager can pull it, and Pavois’s own .deb and .rpm cannot declare it as a dependency. Omnitruck’s metadata endpoint publishes the package URL and its checksum, so you download, verify, then install with the system package manager:

META="https://omnitruck.cinc.sh/stable/cinc-auditor/metadata?p=el&pv=9&m=x86_64"
URL=$(curl -sS "$META" | awk '/^url/{print $2}')     # the package for that platform
SUM=$(curl -sS "$META" | awk '/^sha256/{print $2}')  # and its published checksum
curl -fsSLO "$URL"
echo "$SUM  ${URL##*/}" | sha256sum --check         # integrity, before anything runs
sudo dnf install -y "./${URL##*/}"                  # Debian/Ubuntu: sudo apt install ./<file>
pavois doctor                                        # CINC engine (native): /usr/bin/cinc-auditor

The p and pv keys are omnitruck’s own: el/8, el/9, debian/12, debian/13, ubuntu/22.04, ubuntu/24.04; AlmaLinux and Rocky declare as el. Upstream also documents a script piped into a root shell, which this site will not ask you for. The Docker fallback and the edge cases: the scan engine.

doctor confirms everything is ready and prints the exact command for each gap:

# all set
  [OK  ] CINC engine (native)   /usr/bin/cinc-auditor
  [OK  ] sudo                   /usr/bin/sudo
  [OK  ] ssh                    /usr/bin/ssh
  [OK  ] rule corpus            embedded, 5928 controls across 9 OS profile(s)
  [OK  ] local OS detected      debian 12
ready: try: pavois scan local --sudo

# gaps it flags (with the fix)
[FAIL] CINC engine            no scan engine: install cinc-auditor (https://pavois.dev/en/installation/#engine) or docker
[WARN] sudo                   not found; effective-config reads need root, pass --sudo

2. First scan (A to E grade)

Simplest path: audit the current host with sudo (effective config needs root):

pavois scan local --sudo

For a remote target over SSH:

pavois scan user@host --key ~/.ssh/id_ed25519 --sudo

--key is not optional, even when ssh user@host works. Pavois reaches the target through the engine's SSH transport, which does not read ~/.ssh/config and does not fall back to ~/.ssh/id_ed25519 the way the ssh command does. Omit it and the run stops at "could not reach or identify user@host", on a host you can log into by hand a second later. Pass --key <path>, or add the key to ssh-agent.

The OS profile is auto-detected; --profile is only for a custom profile.

# (custom profile, optional)
# pavois scan user@host --key ~/.ssh/id_ed25519 --sudo --profile profiles/linux/debian12

The output lists deviations by severity and prints the A to E grade as a large letter, plus the HTML report path. To serve reports: pavois serve (http://localhost:8098).

See a sample report (real scan) + before/after campaign →

3. Harden, then prove

Plan the fixes, opt into the ones you want, converge a native Chef run, and re-scan:

pavois harden plan user@host --key ~/.ssh/id_ed25519 --sudo
# edit the plan: flip rules to apply: true
pavois harden apply hardening-plan-debian12.yml --scan

Add --reboot to reboot the target and re-scan after boot: a PASS there is reboot-proven. See the qualified verdict.

4. Converge (an apply is not enough)

A plan is a snapshot of the state BEFORE hardening, and hardening changes that state. Pavois installs the packages a control needs to be meaningful, and a package brings its own files with it: installing at creates /etc/at.deny, which another control requires to be absent. It was absent when the plan was written, so nothing ever deleted it. A single-pass apply therefore cannot close the gaps it creates itself.

Measured on a fresh Debian 12: the first pass arms 211 gaps, the second arms 37, of which 10 existed only because the first had installed the software they audit. postfix arrives as a dependency and brings a banner naming the distribution; sssd ships an AppArmor profile in complain mode; installing at creates /etc/at.deny. None of those gaps was visible when the first plan was written, for the simple reason that none of them existed yet.

The loop: re-scan, re-plan, re-apply, until a pass has nothing left to do.

# until the fixpoint (2 to 3 passes in practice)
pavois scan user@host --key ~/.ssh/id_ed25519 --sudo
pavois harden plan user@host --key ~/.ssh/id_ed25519 --sudo --from reports/<last>.json
pavois harden apply --target user@host --key ~/.ssh/id_ed25519 --sudo-prompt plan.yml

Watch for controls that contradict each other: a package one installs, another removes. Fix the pair before looping, or the loop oscillates.

5. What an apply cannot close

Every rule carries a remediation class. Three of them are not something an apply can close:

  • install-time: a separate filesystem (/var, /var/log, /tmp) is decided at install time. Pavois ships an LVM partition recipe, not a sleight of hand.
  • kernel-build: a KSPP option missing from the distribution kernel requires a rebuilt kernel. Pavois ships the build recipe.
  • dangerous: a GRUB password, a default-deny firewall, kernel.modules_disabled. These can lock you out: they require an explicit acknowledgement in the plan and never apply on their own.

That is why the report prints TWO grades: the raw grade, and the remediable posture (what pavois can reach on its own, excluding install-time and kernel-build). On a debian 12 straight out of a cloud image, the full campaign (KSPP kernel, LVM partitions, converged apply) lands at grade B and remediable A, measured on 13 July 2026 on a fresh VM. Running lynis afterwards on the same host reports a hardening index of 89: an independent tool cross-checking the result, not a compliance verdict (lynis does not produce one).

6. Prove the host survived

A hardened host that lost a vital function is a failure, not a grade. After an apply, check that the firewall is up, sshd answers, the package manager is healthy and the machine still logs. A host can show 560 green controls and no longer write a single line to /var/log/syslog.

# reboot, re-scan: a PASS after reboot is reboot-proven
pavois harden apply --target user@host --key ~/.ssh/id_ed25519 --sudo-prompt --reboot --scan plan.yml

# behavioural validation: attempt the forbidden action
pavois verify user@host --key ~/.ssh/id_ed25519

pavois verify does not read the configuration: it attempts the forbidden actions and watches what happens. Eight probes, including the root SSH login (which must be refused), the absence of cleartext clients, and four that trigger a real event (a chmod, a deletion, a denied read of /etc/shadow) then confirm auditd actually RECORDED it. That is the one proof that depends on nobody's interpretation.

7. In CI

The exit code carries the verdict, and the output format fits your tooling:

# fail the build under 80/100; SARIF for GitHub code scanning
pavois scan user@host --key ~/.ssh/id --sudo --fail-under 80 --format sarif --out ./out

# a timestamped evidence package, with a manifest and checksums
pavois bundle before.json after.json --plan plan.yml --out evidence/

Exit codes: 0 compliant, 1 below the threshold, 2 technical error. Formats: table, json, sarif, junit, csv, html.

8. Reverting a change

Pavois can undo its own hardening run. Among the tools that remediate (OpenSCAP, Ubuntu’s USG, the CIS Build Kits, ansible-lockdown), none ships the inverse of its own remediation. It is possible here for one precise reason: the plan is declarative, so what the run will touch is known BEFORE it runs. Where you have snapshots (NixOS, rpm-ostree, ZFS, LVM, a VM), those are strictly better: use them. The rollback chapter covers the prior art (Bastille Linux, 2003) and the limits.

Before it converges, harden apply photographs the prior state: the content of every file the recipe will write, the list of those that did not exist yet (a rollback deletes them), and the installed/enabled state of every package and service it touches.

# the apply writes a restore point, then converges
pavois harden apply --target user@host --key ~/.ssh/id --sudo-prompt --yes plan.yml
# pavois: 📸 restore point → restore-points/user_192-0-2-10-20260714-1505

# undo: files restored, packages purged, services restored, daemons reloaded
pavois harden rollback restore-points/user_192-0-2-10-20260714-1505 --key ~/.ssh/id --sudo-prompt --yes

Measured on a fresh debian 12, a full 268-item apply then a rollback: 597 of the 620 controls return to an identical state, that is 96%. The rollback also reloads the daemons: a restored sshd_config nobody reloaded has undone nothing.

It is not a time machine, and the manifest says so. The 23 that do not return are published one by one: twelve hardenings survived the rollback (a /tmp already mounted, a live sysctl value), seven controls became applicable (the logs the run created still exist), one stopped being applicable, and three controls that PASSED now fail (purging a package deletes its system user, and its files become unowned). A rollback is not a null operation, and the manifest says so before you confirm.

The prudent rule still stands: take a snapshot before the first apply in production, and make your first run on a clone. A 96% rollback is a safety net, not a licence for recklessness.

9. A whole fleet

One pavois run audits ONE target. There is no built-in fleet aggregation: that is a stated gap, not a hidden feature. In practice a loop and a report directory are enough, because every scan emits a complete, self-contained JSON.

# one scan per host, one JSON per host
while read -r host; do
  pavois scan "$host" --key ~/.ssh/id_ed25519 --sudo --on-target \
     --format json --out reports/ --fail-under 70 || echo "$host below threshold"
done < inventory.txt

# each host grade and failures, one line each
jq -r '.grade + " " + (.failed|tostring) + " " + .target' reports/*.json

Hardening follows the same shape: one plan per host, reviewed, then applied. Nothing stops you driving the loop from Ansible or Rundeck: pavois is an agentless binary that uses your ssh_config and your sudo.

Build from source (contributors)

Everything above works with the published binary. This section is for people changing Pavois, and for nobody else: the build pulls a pinned Go, Node and Python toolchain, then regenerates the rule corpus and the OSCAL bundle that a release binary already carries inside it.

sudo apt install -y extrepo && sudo extrepo enable mise   # a signed repository, never a piped script
sudo apt update && sudo apt install -y mise
# Fedora / RHEL: dnf copr enable jdxcode/mise && dnf install mise
# macOS: brew install mise

git clone https://github.com/stephrobert/pavois.git && cd pavois
mise trust && mise install   # pinned Go, Node, Python
mise run build               # -> go/pavois
mise run regen               # rule corpus + OSCAL from the reference
./go/pavois doctor           # check CINC, sudo, SSH, OS, corpus

On Fedora or RHEL: dnf copr enable jdxcode/mise && dnf install mise; on macOS: brew install mise. Details and troubleshooting: Installation.

Key takeaways

  • Three commands: build, scan, harden.
  • No agent: Pavois uses your ~/.ssh/config and your sudo, like oscap/lynis.
  • A clean A requires a reboot-proof posture, not just a live one.
  • One apply is not enough: hardening changes the very state the plan was computed from. Converge to a fixpoint.
  • Partitions and the kernel are not fixable in place: they are recipes, and the remediable grade says what pavois can reach without them.
  • Always check a hardened host still works: firewall, sshd, package manager, logging.

Next steps