Handbook sections▾
Foundations
Understanding the threats to a Linux hostWhy we hardenThe defense principles behind every ruleEffective configuration: the truth no file holdsThe standards Pavois maps toSOCLE control ID modelEvidence & exportsHow the A-E grade is computedWhat a PASS proves: the qualified verdictTrust model for the evidence bundleIs SOCLE just your own norm? (governance & the circularity question)What Pavois covers, and what it doesn'tDomains
Hardening SSHHardening PAMMandatory Access Control (SELinux / AppArmor)Hardening the host firewallHardening sudoFile Permissions & OwnershipMount & filesystem hardeningHardening kernel modulesHardening the kernel & network with sysctlAudit logging with auditdHardening logging with journald and rsyslogHardening systemd servicesPackage hygieneHardening the bootloader (GRUB)Synchronizing timeLogin banners and MOTDControlling cron and at accessHardening the GNOME desktop (dconf)Tooling
Undo a hardening run: restore pointsOperating Pavois: privileges, air-gap, timing, exceptionsRun Pavois in CI (GitHub Actions)Feature status: delivered, partial, roadmapGovernance: licence, versioning, provenance, securityRun Pavois in CI (GitHub Actions)
Last reviewed
Every scan returns an exit code a pipeline can gate on and emits SARIF/JUnit/JSON/CSV/HTML. Scan a disposable container or an SSH target, fail the job under a grade, and publish findings, secrets handled safely.
Pavois is built for CI: every scan returns an exit code a pipeline can act on, and emits SARIF, JUnit, JSON, CSV or HTML. The patterns below scan a target, fail the job under a chosen grade, publish findings to GitHub code scanning, and archive the full report.
Exit codes
The process exit code is the gate:
0: every scored control passed (and the grade is at or above--fail-under).1:--fail-underwas not met, so the job fails. The threshold is the points value (0 to 100), not the letter.2: a technical error (pavois or the engine could not complete the run).
CINC Auditor's own 100 / 101 are handled INSIDE pavois (a failing control is a normal outcome, not an error) and never reach your shell: a pipeline testing for 100 would never fire. The reference is generated from the binary.
Workflow 1: a disposable container target
name: hardening
on: [push, pull_request]
jobs:
pavois:
runs-on: ubuntu-latest
permissions:
security-events: write # upload SARIF to code scanning
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v2 # pinned Go/Node/Python toolchain
- run: mise run build # -> go/pavois
- run: docker run -d --name target --privileged debian:12 sleep infinity
- run: |
./go/pavois scan target \
--profile profiles/linux/debian12 \
--format sarif --fail-under 70 > pavois.sarif
- if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: pavois.sarif
- if: always()
uses: actions/upload-artifact@v4
with:
name: pavois-report
path: reports/--fail-under 70 blocks a merge whose effective posture scores below 70/100. Because the SARIF upload and artifact steps use if: always(), the findings are published even when the gate fails, so a reviewer sees exactly which controls regressed.
Workflow 2: a remote SSH target (with secrets)
To scan a real host over SSH, inject the key and the sudo password from GitHub secrets, never as command-line arguments:
- run: |
install -m 600 /dev/null ~/.ssh/id
printf '%s' "${{ secrets.TARGET_SSH_KEY }}" > ~/.ssh/id
- env:
PAVOIS_SUDO_PASSWORD: ${{ secrets.TARGET_SUDO_PASSWORD }}
run: |
./go/pavois scan deploy@server1 --key ~/.ssh/id \
--sudo-prompt --on-target \
--profile profiles/linux/ubuntu2404 \
--format sarif --fail-under 70 > pavois.sarif--sudo-prompt reads PAVOIS_SUDO_PASSWORD from the environment (no echo, never argv); --on-target runs the scan on the host (far fewer SSH round-trips, and it sidesteps the Debian Defaults use_pty TTY issue with native-SSH sudo).
Secrets, handled safely
- Never pass
--ssh-pass/passwords on the command line: they leak viapsand shell history. Use--ssh-prompt(readsPAVOIS_SSH_PASSWORD) or--sudo-prompt(readsPAVOIS_SUDO_PASSWORD), or a key file written from a secret with mode0600. - Scope the GitHub token to
security-events: write; nothing more is needed to upload SARIF. - Prefer a key over a password, and a short-lived deploy account over root.
Install: build or released binary
The workflows above build from source with mise run build. On a tagged release you can instead install the published .deb/.rpm from the project's GitHub Releases rather than building, which is faster and pins a signed artifact. Either way the same pavois scan runs.
Other formats
Swap --format for the consumer you need: junit for test reporting, json for a custom gate, html for a human-readable artifact, csv for a spreadsheet. See evidence and exports.
FAQ
How do I fail the build on a bad grade? --fail-under N sets exit code 1 when the grade is below N points (0 to 100). Pair it with if: always() on the SARIF/artifact steps so findings still publish on failure.
How do I pass the SSH key and sudo password safely? Write the key from a secret to a 0600 file and use --key; set PAVOIS_SUDO_PASSWORD from a secret and use --sudo-prompt. Never put a password in argv (it leaks via ps).
Does Pavois install an agent on the target? No. It scans a throwaway container or an SSH target; --on-target runs the checks on the host for speed but leaves nothing behind.
Next steps
- Get started: build and run your first scan locally.
- How the grade is computed: pick a sensible
--fail-under. - Evidence and exports: every output format.