Grade a host against CIS¶
Every host in this estate owes the CIS Level 1 Server benchmark of its own distribution, and the
hosts that carry a site or a runner owe a Level 2 subset on top of it. Two separate instruments
measure whether a host actually holds a control, and nerthus-config hardening report prints what
each of them said, side by side, control by control.
nerthus-config hardening status nerthus-host-2 # every step's verdict, nothing applied
nerthus-config hardening report nerthus-host-2 # the grade, both instruments, per control
nerthus-config --json hardening report nerthus-host-2 # the same, as data
nerthus-config hardening report --world lab.yaml --family debian --role hardening --arm container
Why two instruments¶
The first instrument is the estate's own: the hardening cookbook's steps. A step that sets
net.ipv4.conf.all.rp_filter also declares that it is the control
sysctl_net_ipv4_conf_all_rp_filter, so planning the cookbook against a host already says whether
that control holds. This is fast, it runs on every converge, and it has one weakness that no amount
of care removes: it is a guard inside the thing it guards. The people who wrote the check also
wrote the change it checks, from the same reading of the benchmark. If that reading was wrong, both
halves are wrong together and the report is confidently green.
The second instrument reads the host from outside that opinion. On Debian, Ubuntu and AlmaLinux it
is OpenSCAP evaluating the CIS profile of the SCAP Security Guide; on Alpine, which has no CIS
benchmark at all, it is Lynis. Neither knows what our cookbook intended. It runs nightly, from a
cron drop-in the cookbook writes, and leaves its answer in /var/lib/nerthus/scan/.
Two instruments that always agreed would be one instrument. The value is in the rows where they do not.
The six grades¶
| Grade | What it means |
|---|---|
pass |
an instrument read the control and the host holds it, and no instrument says otherwise |
fail |
an instrument says the host does not hold it, or could not read it at all |
disagree |
both instruments read the control and they say opposite things |
exempt |
a live row in hardening/exemptions.yaml covers it, with a reason and a date |
unmeasured |
nothing reads this control yet on this host |
not-provable-here |
the reading came from somewhere that cannot answer for this control |
not-provable-here has two causes and they are the same statement. A machine container shares its
host's kernel, so a container reporting ok for a sysctl is reporting the lab's kernel and not its
own — the catalogue's provable_in says which controls that covers. And a scanner check that
reported notapplicable, notchecked, notselected or informational did not run, so it has said
nothing about the host. Neither is ever promoted to a pass. A check that did not happen is not
evidence that a host is correct, and a report that blurred the two would be worse than no report.
Reading a disagreement¶
A disagree row is not a failing control and not a passing one. It is the report saying that two
checks of one subject cannot both be right, and that a person has to look. The row quotes both
readings — the step's verdict with its own reason, and the scanner's rule id with its result — so
looking usually takes a minute.
There are exactly three honest ways to close one:
- Fix the host. The scanner is right and the step was measuring the wrong thing, or measuring
the right thing too loosely. Correct the step in the
hardeningcookbook. This is the common case and it is why the second instrument earns its keep. - Correct the catalogue. The two are measuring genuinely different subjects and the
correspondence between them was wrong. The control's
measured_byrow is regenerated from the content bytools/cis-catalogue.py; on Alpine the row may instead be wrong inhardening/scanner/lynis-map.yaml, which is hand-written. - Exempt the control. The host cannot hold it without losing the function it exists for. Write
the row in
hardening/exemptions.yamlwith the control, the scope, the reason, arulingand anexpiresdate at most ninety days out. An exemption with no date, or a date further out than that, is refused byvalidate; an exemption that has run out is a finding at the next report.
What is not on that list is deciding which instrument you prefer. If a disagreement is closed by believing one side without establishing why the other was wrong, the next disagreement on the same subject will be closed the same way and the second instrument has been switched off by habit.
What the scanner reads¶
The SCAP content is the ComplianceAsCode release the catalogue itself was cut from, fetched once per
host from a URL pinned by digest in pins/<family>.yaml. It is deliberately not the
distribution's own scap-security-guide package: those packages lag the release they ship on, so
far that two of this estate's three benchmarked families package no content for their own version at
all, and a scanner reading them would grade each host against the benchmark of the release before
its own. One release across the estate also means the scanner's rule ids and the catalogue's control
ids are the same strings, so nothing has to be translated between them.
Alpine is the exception twice over. It has no CIS benchmark, so its catalogue is derived — the
intersection of the other three, less the subjects Alpine has not — and there is no datastream for
OpenSCAP to read. Its second instrument is Lynis, whose test ids are its own namespace, so
hardening/scanner/lynis-map.yaml says which control each test answers for. That map is short on
purpose: Lynis answers with one compound verdict where CIS asks per control — a single result for
every sshd option at once, another for every umask file at once — and hanging twenty controls on
one such verdict would turn one suggestion into twenty disagreements, none of which names what is
actually wrong. A control whose only Lynis counterpart is a compound verdict is left to the steps
alone.
The scan on the host¶
The cookbook installs the scanner, writes /usr/local/lib/nerthus-hardening/scan.sh and schedules
it nightly — a /etc/cron.d drop-in on the three families that have one, /etc/periodic/daily on
Alpine. The script reduces the tool's output to {control: result} and writes
/var/lib/nerthus/scan/<date>.json, keeping a fortnight of them, with latest.json beside them.
hardening report does not open a second connection to fetch it: the cookbook reports the latest
scan as the hardening_scan fact, so the reading arrives on the plan the report was already
running. A host converged a minute ago has not reached its first nightly run, and the report says
so plainly rather than treating an absent scan as an agreeing one — the instrument counts it prints
are what each instrument actually read, never what the catalogue says it would read.
To grade against a scan you hold on disk — a lab run, or a file copied off a host — pass it with
--scan.
Exit codes¶
report exits 1 on a failing control, on a disagreement, or on an exemption past its date, and
0 otherwise. status exits 0 when every step holds, 2 when a step would change the host, and
1 on a problem or a probe that could not be read. Both change nothing, ever.