Lynis: Auditing Your Server Like an Attacker Would

Turning a wall of warnings into an actual to-do list

Contents

I ran Lynis on a box I thought I’d hardened properly last year and got 34 suggestions back. Most were minor — set a password expiry policy, install a PAM module I’d never heard of — but two were things I’d have genuinely wanted to know about sooner: a world-readable /etc/shadow backup left over from a botched migration, and a kernel module still loaded that I’d disabled the service for months earlier but never actually blacklisted. That’s the value proposition in one incident. Not a compliance stamp, a list of specific things worth fixing on this specific machine.

Lynis is a shell-script auditing tool that’s been around since 2007 and runs entirely locally — no agent, no phone-home, no daemon left behind. It walks through hundreds of checks across authentication, file permissions, kernel hardening, installed software, logging, and more, then spits out a report with a hardening index and a list of warnings and suggestions. The trap is treating that list as a checkbox exercise: work through it top to bottom, silence every warning, declare victory. Half the value is in deciding what not to fix, because Lynis doesn’t know your threat model — it knows generic best practice, and some of that genuinely doesn’t apply to a homelab box behind a firewall that nothing external ever touches directly.

Why a local audit tool beats “I think I did the hardening guide right”

Advertisement

Following a hardening checklist by hand has an obvious failure mode: you do the fifteen things the guide mentions and miss the sixteenth, because no guide covers everything and yours is inevitably a few versions stale. Worse, checklists don’t adapt to what’s actually installed on your box. A guide written for a bare Debian install has nothing useful to say about the Docker socket permissions on your box, or the stray SUID binary a package pulled in as a dependency eighteen months ago and you never noticed.

Lynis instead inspects the live system: what’s actually running, what’s actually listening, what the actual file permissions are, right now. It’s the difference between reading a fire-safety pamphlet and having someone walk your actual house and point at the specific extension cord under the specific rug. That’s also precisely how an opportunistic attacker approaches a box they’ve gained a foothold on — enumerate what’s there, look for the specific weak point, not the generic one. Running the same kind of sweep against yourself, on your own schedule, before anyone else does, is the whole pitch.

It’s also worth being honest about what Lynis isn’t. It’s not a vulnerability scanner in the CVE-matching sense — it doesn’t fingerprint package versions against a database of known exploits the way something like OpenVAS or Nessus does, and it’s not a replacement for keeping packages patched. It’s closer to a configuration and posture auditor: it tells you about weak settings, missing hardening, forgotten leftovers and risky defaults, all of which matter independently of whether any specific package has a known CVE right now. The two categories of tool are complementary rather than overlapping, and treating Lynis output as a full security assessment on its own would be a mistake — it’s one layer of a sensible audit routine, not the whole routine.

What the hardening index actually measures

The number Lynis prints at the end of a run is a composite score built from weighting each passed, failed and skipped test against a rough sense of how much that category matters to overall posture — things like authentication configuration, kernel hardening flags, logging coverage and software patch state all contribute, but not evenly, and the exact weighting isn’t something you’re meant to reverse-engineer and chase point by point. I’ve seen people treat a jump from 68 to 74 as meaningful progress and a plateau at 74 as a sign something’s wrong, when in practice the last several points on any real box tend to be suggestions that don’t apply — installing a file-integrity tool on an immutable, container-only host, say, or a boot-loader password on a system that boots from a cloud provider’s own hypervisor and has no local boot menu to protect. Use the index as a rough same-machine-over-time indicator, not a leaderboard score.

Running it and reading the output

Advertisement

Install from the distro repo or, for a more current version, the vendor’s own repo (CISOfy maintains it):

1
2
$ sudo apt install lynis
$ sudo lynis audit system

The scan takes a couple of minutes and streams output as it goes, grouped by test category — Boot and Services, Kernel, Memory and Processes, Users/Groups/Authentication, Shells, File Systems, USB devices, Storage, Software: Firewalls, and on. Each finding is tagged OK, WARNING, or SUGGESTION, and at the end you get a hardening index out of 100 and a full report saved to /var/log/lynis-report.dat.

The index number is the least useful part of the output and the part everyone fixates on. It’s a rough heuristic, not a security score you should chase to 100 — some suggestions genuinely don’t apply to your setup (a suggestion to install AIDE file-integrity monitoring on a box that’s rebuilt from Ansible weekly is close to noise), and chasing every point pushes you toward changes that don’t reduce real risk. Treat it as a rough sense of scale between runs on the same machine, not an absolute grade.

What matters is the warnings and suggestions list, which you can pull straight from the report file for scripting or just read at the end of the terminal run:

1
$ sudo lynis show details | tail -60

or check the plaintext log directly:

1
2
3
4
5
$ grep -E "^(warning|suggestion)" /var/log/lynis-report.dat | head -20
suggestion[]=AUTH-9328|Default umask in /etc/login.defs could be more strict like 027|
suggestion[]=KRNL-5820|One or more sysctl values differ from the scan profile and could be tweaked|
warning[]=SSH-7408|Root can with SSH, consider disabling root ssh access|-|-|
suggestion[]=BOOT-5122|Set a password on GRUB boot loader to prevent altering boot configuration|-|-|

Each finding gets an ID (SSH-7408, KRNL-5820) that maps to a category and, for most, a documented rationale you can look up. That warning about root SSH access, for instance, is exactly the kind of thing that should already be closed if you followed the SSH hardening walkthrough — seeing it flagged on a box where you thought you’d already done that is a genuinely useful signal that something regressed, maybe a reimage that skipped a step, or a config that got reverted by a package update.

Triaging: what to fix, what to skip, and how to say so

The single best habit for turning Lynis output into an actual to-do list is deciding, deliberately, for each finding: fix it, or explicitly accept it — and write down which. Lynis supports this with a profile file where you can pin tests to skip with a reason attached:

1
2
3
4
5
6
7
8
# /etc/lynis/custom.prf
# Homelab box behind NAT + firewall, no external exposure.
# USB storage checks not relevant — no removable media policy needed here.
skip-test=STRG-1840

# File integrity monitoring skipped — rebuilt from Ansible weekly, drift is
# caught by the playbook diff instead.
skip-test=FINT-4350

Run with sudo lynis audit system --profile /etc/lynis/custom.prf and those specific checks stop cluttering the report. This matters because a report full of accepted-risk noise trains you to stop reading it carefully — better to explicitly prune to the findings you actually intend to act on, with the reasoning captured in the profile file itself so future-you (or the next person touching the box) knows it was a decision, not an oversight.

For the ones you do fix, most map to a one-line change. The umask suggestion above, for example:

1
$ sudo sed -i 's/^UMASK.*/UMASK 027/' /etc/login.defs

And GRUB password protection, which is worth doing on anything with physical or console access you don’t fully control:

1
2
3
$ grub-mkpasswd-pbkdf2
# paste the resulting hash into /etc/grub.d/40_custom as a password_pbkdf2 line
$ sudo update-grub

Automating it so the audit actually happens again

A one-off audit tells you about today. The value compounds when you run it on a schedule and diff the results, because that’s how you catch drift — a package update that re-enables a service, a config edit that loosens a permission, a cron job someone added that Lynis correctly flags as a SUID oddity. A simple systemd timer beats a cron entry here because you get logging and dependency ordering for free:

1
2
3
4
5
6
7
# /etc/systemd/system/lynis-audit.service
[Unit]
Description=Lynis security audit

[Service]
Type=oneshot
ExecStart=/usr/sbin/lynis audit system --cronjob --profile /etc/lynis/custom.prf
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# /etc/systemd/system/lynis-audit.timer
[Unit]
Description=Run Lynis audit weekly

[Timer]
OnCalendar=Sun 03:00
Persistent=true

[Install]
WantedBy=timers.target

--cronjob suppresses the interactive colour output and warnings prompts, writing straight to the log file, which is what you want for something running unattended. Enable and start the timer:

1
$ sudo systemctl enable --now lynis-audit.timer

From there, a small script comparing this week’s warning count against last week’s — or just mailing yourself the diff of /var/log/lynis-report.dat — turns the audit from “a thing I ran once” into an actual monitoring signal. If you’re already running Grafana and Prometheus for the rest of the homelab, exporting the warning count as a metric via a node-exporter textfile collector is a natural extension and means a regression shows up on the same dashboard as everything else.

Comparing runs across a whole fleet

Advertisement

A single scan on a single machine is a snapshot. The more useful habit, once you’re running more than one or two servers, is comparing results across the fleet — because two boxes built from the same base image drifting apart over time is itself a signal something changed outside your normal deployment process.

Lynis writes a machine-readable report alongside the human-readable one, which makes this easy to script:

1
2
3
4
$ sudo lynis audit system --cronjob --report-file /var/log/lynis-nas01.dat
$ sudo lynis audit system --cronjob --report-file /var/log/lynis-vm02.dat
$ diff <(grep -E "^(warning|suggestion)" /var/log/lynis-nas01.dat) \
       <(grep -E "^(warning|suggestion)" /var/log/lynis-vm02.dat)

A finding present on one box and absent on an otherwise-identical sibling is usually more informative than any single warning on its own — it points at a specific, recent divergence (a manual fix that wasn’t captured in configuration management, a package version skew, a service someone enabled by hand at 11pm and forgot about) rather than a generic best-practice gap you’d have found on day one anyway. If you’re managing these boxes with Ansible or similar, feeding that diff back into the playbook is how the fleet actually converges instead of slowly drifting in twenty small, undocumented ways.

There’s a related habit worth adopting once you’re running the fleet comparison regularly: tag each report with the machine’s role, not just its hostname, so the diff you’re reading compares like with like. Comparing a bare metal hypervisor against a stripped-down container host will surface a long list of “differences” that are actually just two different classes of machine having different jobs — that’s noise, not drift. The genuinely useful comparisons are within a role: every web-facing VM against every other web-facing VM, every NAS-adjacent box against its siblings. Grouping reports that way before diffing them turns what would otherwise be a wall of expected differences into a short, legitimately actionable list.

Troubleshooting

Scan finishes fast and finds almost nothing. Lynis skips deeper checks it can’t run without elevated privileges or that need packages it doesn’t find (auditd, apparmor-utils, etc.). Always run with sudo — a non-root scan is a shallow scan, and the report itself notes which tests were skipped and why near the top of the terminal output.

A finding I fixed keeps reappearing. Check whether a config management tool (Ansible, Puppet, a golden image) is reasserting the old value on every run. This is common with SSH config and sysctl settings, where a base role sets a default that your manual fix then gets reverted on the next converge. Fix it at the source — the playbook or template, not merely the live box — or it’ll keep coming back.

--cronjob mode produces no output at all. That’s by design; it’s meant for unattended runs writing to the log rather than the terminal. Check /var/log/lynis.log and /var/log/lynis-report.dat rather than expecting terminal output from the systemd service.

Report flags something that seems wrong for a container or minimal image. Some tests assume a traditional full OS layout — a bootloader, a full logging stack, USB subsystem — and don’t make sense inside a container or a minimal cloud image with no GRUB. Skip those tests explicitly in the profile rather than trying to satisfy checks that don’t apply to the environment; forcing a container to have a GRUB password is a waste of effort.

Is it worth it

Yes, and it costs almost nothing to try — a five-minute install and a scan that runs itself. The value isn’t the number at the end, it’s the specific, actionable list of things about your box that a generic hardening guide would never have known to mention, plus a repeatable way to catch drift over time instead of assuming a config you set once is still set. The discipline that makes it worth keeping, though, is the triage step: decide deliberately what to fix and what to explicitly accept, write the reason down in the profile, and don’t let an unread wall of warnings quietly become the reason you stop paying attention to any of it. Pair it with something that actively responds to abuse in real time, like Fail2ban or CrowdSec — Lynis tells you what’s structurally weak, Fail2ban and CrowdSec deal with who’s currently knocking on the door.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.