Every hardened server drifts, because every configuration change is an opportunity to leave a door ajar, and because the baseline that was strict two years ago reads as loose today. Lynis is the tool that measures the drift: an open-source auditing program that runs hundreds of tests against a live system, prints what it finds in the language of warnings and suggestions, and leaves a structured report that turns the vague ambition of hardening into an ordered to-do list. This article builds the working rhythm around the tool: running the first audit, reading the report the way an engineer reads it, triaging findings by the effort they deserve, fixing the recurring classics, and wiring the audit into the calendar so that drift gets caught quarterly instead of archaeologically.

What Lynis is and what a run looks like

Lynis ships from the CISOfy project in two editions, the open-source core that this article uses and an enterprise variant with centralized reporting, and the community edition is a single package with no agent, no service and no phone-home behavior. Installation is one command on Debian-family systems, apt install lynis, or a repository from the vendor for the current release, since distribution packages sometimes lag a version behind, and version matters for a tool whose test suite is its product.

The main invocation is one line, run as root because the tests read files only root can see:

lynis audit system

The run takes a minute or two, printing a live stream of test identifiers and their verdicts, and the value of the live view is the first surprise for new users: the stream is readable, each test names what it checks, and the pace makes it obvious the tool is doing far more than any single checklist could. At the end, a summary block prints the counts of warnings and suggestions, the hardening index, and the locations of the full log and the machine-readable report, /var/log/lynis.log and /var/log/lynis-report.dat.

The hardening index, a number from zero to one hundred, deserves its own paragraph before anyone chases it. It is a summary of how many tests passed, not a measurement of actual risk, and the honest way to treat it is as a trend line: a server moving from 58 to 71 over two quarters is a story of deliberate work, while a server held artificially at 95 by disabling tests rather than fixing findings is a story that the index was never designed to tell. The number earns its keep as a one-glance health check across a fleet, and the sections that follow treat the report itself as the real product.

Reading the report the way an engineer reads it

The human-readable log is organized around findings, and each finding has three parts: the warning or suggestion, the test that produced it, and the details section that explains what the test wanted to see. A typical warning reads like this:

-[ WARN ] - SSH option AllowTcpForwarding is enabled [test: SSH-7408] [details: /etc/ssh/sshd_config]
Suggestion: set AllowTcpForwarding to 'no' or any other allowed value

The structure invites the workflow that follows: each test id names the area, the details name the exact file, and the suggestion names the shape of the fix, which means the gap between a warning and a working change is usually one config line and one service reload. Suggestions, marked with a lower urgency than warnings, follow the same format, and the report typically contains dozens of them, everything from a missing NTP configuration to password aging parameters in login.defs.

The command that turns a finding into full context is lynis show details with the test id:

lynis show details SSH-7408

which prints the test description, the reason it matters, the files examined and every related suggestion, the paragraph of documentation that turns a checklist item into an informed decision. The habit worth building during the first read of any report is exactly this one: for each warning, run the details view before deciding anything, because a nontrivial share of suggestions are environment-dependent, and the tool's job is to flag the question, not to answer it for every possible server.

Triage by effort and blast radius

The report arrives as a flat list, and the first engineering act is to sort it into three buckets. The first bucket holds the warnings, which represent findings the tool considers genuinely weak spots, and these get first claim on attention regardless of the index. The second bucket holds the suggestions that are cheap, single-line changes in files the team already manages with no behavioral impact on real users, sysctl values, login.defs parameters, file permissions on banners. The third bucket holds everything that changes behavior, password complexity policies that affect real humans, mount options that break an application expecting an executable /tmp, and these get scheduled like the changes they are, with a test window and a rollback plan.

Two sorting keys do the real work inside the buckets. Effort, how many minutes a fix costs, orders the work within a maintenance window. Blast radius, what breaks if the suggestion is wrong, decides which fixes need a staging pass. A cheap fix with a small blast radius is applied on the spot, and most findings in a first report belong to exactly that category. An expensive or risky fix lands in the change queue with a ticket that links to the test id, which is how the audit report turns into project management instead of a forgotten file in /var/log.

The anti-pattern that the triage prevents deserves a name: fixing the report to satisfy the tool. Disabling a test because its suggestion is inconvenient, or marking a finding as accepted in the hope that the tool stops nagging, are both legitimate mechanisms in the right context and both are habits that rot the value of the audit when done reflexively. Every suppressed finding gets a reason in the runbook, and every reason gets revisited at the yearly review, because a suppressed warning is a bet that the environment will never change, and environments rarely honor such bets.

The recurring classics and their standard fixes

Across distributions and years, the same findings dominate first reports, and their standard fixes are worth collecting into the runbook once so that they cost seconds forever after. The SSH family is the perennial leader: PermitRootLogin no, PasswordAuthentication no once keys are universal, MaxAuthTries down from the default, and the ClientAliveInterval that ends idle sessions, four lines in sshd_config that close a disproportionate share of the warnings. The kernel family follows: a short set of sysctl lines covering source-routed packet rejection, ICMP redirect acceptance, and the dmesg restriction, gathered into a dropped file under /etc/sysctl.d/ and applied with sysctl --system.

The account policy family covers the aging parameters in /etc/login.defs, the password quality library configuration, and the often-forgotten umask default. The logging family recommends rsyslog or journald settings, log file permissions, and the presence of a time synchronization service, because timestamps are the skeleton of every other forensic capability. Each family is ten minutes of configuration work the first time, and thereafter a single configuration management module that every new server inherits, which is the point where the quarterly audit stops finding anything and becomes the boring health check it was always meant to be.

Customization, scheduling and reading the trend

The test suite is tunable on two levels. The profile file, /etc/lynis/default.prf or a custom profile passed with --profile, controls which tests run and adjusts expected values where the site policy differs from the default, and a custom profile is the sanctioned way to encode local decisions without editing the tool. Individual skips are expressed in the profile, one line per test id, and each skip lives next to a comment explaining why, keeping the divergence between the tool's opinion and the site's policy documented instead of tribal.

Scheduling turns the audit from an event into an instrument, and the native quiet mode is built for it:

lynis audit system --cronjob

which suppresses interactive output and colors, prints the summary block for the log, and exits with codes that monitoring understands, nonzero on warnings in the strict configurations. A quarterly cron job or systemd timer writes each run's report to a timestamped location, and the two-line comparison that closes the loop is a diff between the current and previous suggestion counts, which catches both regressions, a fix undone by a reinstalled package, and new surface, a suggestion that appeared after an upgrade changed a default.

The trend view across quarters is the quiet payoff of the whole discipline: the same server audited four times with fixes applied between runs shows a suggestion count that falls and stays fallen, and the rare climb in the curve points at the exact change that caused it. Fleet-wide, the comparison of indexes between servers of the same role exposes the one machine that drifted from its template, which is the finding that no single-run report can ever show and the reason the schedule matters as much as the tool.

The remediation loop and the working checklist

The rhythm that makes Lynis worth its hour is a loop, not an event. Run the audit, read the report, triage into the three buckets, fix the cheap and safe on the spot, ticket the expensive and risky, apply the fixes, and run the audit again before closing the tickets, because the second run is the only proof that the first round worked. The report that comes out of the second run is the artifact worth attaching to the maintenance record, a before-and-after pair that shows the delta instead of asserting it.

The checklist compresses the article into the order of a first week with the tool. Install the current release, run the first audit, and read every warning with show details before touching anything. Triage into warnings, cheap suggestions and behavioral changes, fix in that order, and re-audit to prove each bucket closed. Collect the recurring fixes into configuration management so new servers inherit them, and suppress any test only with a written reason. Schedule the quiet audit quarterly, keep the timestamped reports, and read the trend rather than the single number. A tool that measures drift earns its keep only while someone watches the measurements, and the loop above is the smallest version of watching that survives contact with a busy quarter.
## A worked pass through the usual warning list

The abstract triage reads better with concrete output, so a short walk through a real first report anchors it. The SSH warning about root login, test SSH-7408 in the details view, names /etc/ssh/sshd_config, and the fix is the single directive followed by a reload:

PermitRootLogin no

systemctl reload sshd

The kernel family arrives as a block of suggestions about networking parameters, and the standard reply is one file, /etc/sysctl.d/60-hardening.conf, collecting the lines the report wants to see:

net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.tcp_syncookies = 1
kernel.dmesg_restrict = 1
fs.suid_dumpable = 0

applied with sysctl --system and verified on the next audit run, which now passes each line it flagged a quarter earlier. The account family points at PASS_MAX_DAYS and the password minimum length in /etc/login.defs, values that decide whether the aging warnings return, and the permissions family flags world-readable files in /etc that deserve a chmod the suggestion names explicitly.

The point of the worked pass is the shape rather than the content: every finding collapses into a named file, a small change, and a reload or reapply, and the report's second run confirming each fix is what separates a remediated server from a server whose report was merely read. Teams that keep the fixed config snippets in the runbook find that the third audit takes twenty minutes, and the fifth takes ten, and the quarterly run becomes the quiet ritual it was always designed to be.