<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Lynis – Serverküche</title><link>https://serverkueche.de/en/tags/lynis/</link><description>Lynis – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>en-US</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Sat, 17 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/en/tags/lynis/index.xml" rel="self" type="application/rss+xml"/><item><title>How Secure Is My Server? A Self-Audit With Lynis</title><link>https://serverkueche.de/en/tutorials/lynis-security-audit/</link><pubDate>Sat, 17 Oct 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/lynis-security-audit/</guid><description>Lynis 3.1.7 on Debian 13: audit your server, understand the hardening index, fix real findings – and spot the suggestions that would wreck your Docker host.</description><content:encoded><![CDATA[<p>You have hardened SSH, set up a firewall, got fail2ban running – and still have no idea what you
missed. Lynis answers exactly that question: it walks through your server from the inside and tells
you what is still open. The harder part comes afterwards, namely deciding which of its suggestions
you actually want to follow.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>We install <strong>Lynis 3.1.7</strong> on a Debian 13 box and run a full audit. From the result – on a fresh
netcup server with Docker initially <strong>hardening index 64, one warning and 44 suggestions</strong> – we
work through the points that genuinely help and deliberately leave the rest alone. At the end we
have a reproducible audit run driven by a systemd timer, a documented exception list and an index
of 77.</p>
<p>What we do <strong>not</strong> do matters just as much: one of the suggestions instantly cuts every container
off the network, a second plants a time bomb that goes off with your next new stack, and a third
complains about a setting that is already stricter than required. Lynis does not know your server –
you do.</p>
<h2 id="requirements">Requirements</h2>
<ul>
<li>A <a href="/en/tutorials/harden-ssh/">hardened server</a> running Debian 13 with root or sudo access</li>
<li>Basic familiarity with <a href="/en/tutorials/systemd-basics/">systemd units and timers</a> – for the
automation at the end</li>
<li>About an hour; the audit itself only takes one to two minutes</li>
<li>Optional but handy: <a href="/en/tutorials/unattended-upgrades-automatic-updates/">unattended-upgrades</a>
is already in place – then one of the findings disappears on its own</li>
</ul>
<div class="not-prose my-6 overflow-hidden rounded-xl border border-paprika-200 bg-paprika-50 dark:border-paprika-800 dark:bg-paprika-900/20"
     data-track-content data-content-name="Affiliate-Box · /en/tutorials/lynis-security-audit/" data-content-piece="VPS 1000 G12.5">
  <div class="flex items-center justify-between border-b border-paprika-200 bg-paprika-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-paprika-700 dark:border-paprika-800 dark:bg-paprika-900/40 dark:text-paprika-300">
    <span>🍳 Recommendation</span>
    <span title="Links marked with * are affiliate links.">Ad</span>
  </div>
  <div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
      <p class="text-lg font-bold text-slate-900 dark:text-white">VPS 1000 G12.5</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">4 vCores · 8 GB RAM · 128 GB SSD</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">from €14.50/month</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Lynis runs on the smallest of servers – a VPS 1000 easily handles the audit and a Docker stack.</p>
    </div>
    <a href="https://www.netcup.com/en/server/vps/vps-1000-g12.5-iv-24m-eu?ref=44083#vps-1000-g12.5-iv-12m-eu" rel="sponsored noopener" target="_blank"
   data-track-event="Affiliate|netcup: Affiliate-Box|VPS 1000 G12.5 · {page}"
   class="inline-flex shrink-0 items-center justify-center rounded-lg bg-paprika-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-paprika-700">
  Go to netcup →
</a>

  </div><div class="px-4 pb-4"><div class="not-prose my-3 rounded-lg border border-herb-500/40 bg-herb-50 px-3 py-2 text-sm text-slate-700 dark:bg-herb-900/20 dark:text-slate-200"
     data-voucher-api="vps-1000" data-voucher-msg-empty="No code is available for this plan right now – use the €5.95 code." data-voucher-msg-limit="No more codes today – use the €5.95 code or try again tomorrow." data-voucher-msg-error="The code couldn&#39;t be fetched right now – use the €5.95 code."
     data-voucher-hint="Empty the cart first – the code adds the server itself.">
  <p class="flex flex-wrap items-center gap-x-2 gap-y-1"><span>💶 <span class="font-semibold">1 month free</span>:</span>
    <button type="button" data-voucher-reveal data-copy-title="Click to copy"
            class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">Show code</button>
    <button type="button" data-voucher-next hidden
            class="rounded border border-herb-500/40 px-2 py-0.5 text-xs text-herb-800 hover:bg-white dark:text-herb-400 dark:hover:bg-slate-800">another code</button>
  </p><p class="mt-1 text-xs text-slate-500 dark:text-slate-400" data-voucher-hint-target>
    Single use, valid for VPS 1000 G12.5.
    <a href="https://www.netcup.com/en/checkout/cart?ref=44083" rel="sponsored noopener" target="_blank"
       data-track-event="Affiliate|netcup: Gutschein einlösen|VPS 1000 G12.5 · {page}"
       class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">Redeem in the cart →</a>
  </p><p class="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 border-t border-herb-500/30 pt-2">
    <span>💶 <span class="font-semibold">€5.95 voucher</span> for new netcup customers:</span>
    <button type="button" data-voucher-code data-track-voucher="36nc17844976032"
            title="Click to copy" class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</button>
    <span class="text-xs text-slate-500 dark:text-slate-400">always valid · not for domains or VPS Lite</span>
  </p><p class="mt-1 text-xs"><a href="/en/netcup-voucher/" class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">All netcup vouchers →</a></p>
</div></div>
</div>

<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-install-lynis-from-the-cisofy-repository">Step 1: Install Lynis from the CISOfy repository</h3>
<p>Debian ships Lynis, but an older release:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">apt-cache policy lynis</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">lynis:
</span></span><span class="line"><span class="cl">  Installed: (none)
</span></span><span class="line"><span class="cl">  Candidate: 3.1.4-1</span></span></code></pre></div>
</div>
<p>That is not cosmetic. Lynis consists almost entirely of <strong>tests</strong>, and those age faster than the
program around them – new kernel parameters, new services, new defaults. An audit with an old test
collection misses exactly the things that were recently added. So we use the repository from
CISOfy, the Lynis developers.</p>
<p>We store the signing key as an <strong>ASCII-armored file</strong> (<code>.asc</code>). That is the trick that saves you
from installing <code>gnupg</code> – apt reads plain-text keys directly when the filename ends in <code>.asc</code>:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">install -d -m <span class="m">0755</span> /etc/apt/keyrings
</span></span><span class="line"><span class="cl">curl -fsSL https://packages.cisofy.com/keys/cisofy-software-public.key <span class="se">\
</span></span></span><span class="line"><span class="cl">  -o /etc/apt/keyrings/cisofy.asc</span></span></code></pre></div>
</div>
<p>Check briefly that you actually received a key and not an error page:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">head -1 /etc/apt/keyrings/cisofy.asc</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">-----BEGIN PGP PUBLIC KEY BLOCK-----</span></span></code></pre></div>
</div>
<p>Now add the package source. The <code>signed-by</code> binds the source to exactly this key – CISOfy can only
sign Lynis packages with it, not replace half your system:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;deb [signed-by=/etc/apt/keyrings/cisofy.asc] https://packages.cisofy.com/community/lynis/deb/ stable main&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  &gt; /etc/apt/sources.list.d/cisofy-lynis.list
</span></span><span class="line"><span class="cl">apt-get update
</span></span><span class="line"><span class="cl">apt-get install -y lynis</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">lynis --version</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">3.1.7</span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-sky-300 bg-sky-50 dark:border-sky-800 dark:bg-sky-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">ℹ️</span>Note
  </p>
  <div class="prose-kitchen text-sm">If <code>apt-cache policy lynis</code> still only offers 3.1.4, <code>apt-get update</code> did not pick up the new
source – usually because of a typo in the filename. The list must end in <code>.list</code> and live in
<code>/etc/apt/sources.list.d/</code>.</div>
</div>
<h3 id="step-2-the-first-audit-run">Step 2: The first audit run</h3>
<p>Lynis needs no daemon and no configuration. One invocation is enough:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">lynis audit system --quick --no-colors <span class="p">|</span> tee /root/lynis-run1.txt</span></span></code></pre></div>
</div>
<p><code>--quick</code> skips the pauses where Lynis would otherwise wait for a keypress – otherwise identical to
a normal run. <code>--no-colors</code> makes the output copyable, and <code>tee</code> keeps it so you can compare later.</p>
<p>After a minute or two the summary appears at the end:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">  Details:
</span></span><span class="line"><span class="cl">  Hardening index : 64 [############        ]
</span></span><span class="line"><span class="cl">  Tests performed : 251
</span></span><span class="line"><span class="cl">  Plugins enabled : 0</span></span></code></pre></div>
</div>
<p>Above it are the actual results, split into two categories:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">  Warnings (1):
</span></span><span class="line"><span class="cl">  ----------------------------
</span></span><span class="line"><span class="cl">  ! Found one or more vulnerable packages. [PKGS-7392]
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  Suggestions (44):</span></span></code></pre></div>
</div>
<p>The difference matters. A <strong>warning</strong> is a state Lynis considers wrong right now – an unpatched
package, a service not doing what it should. A <strong>suggestion</strong> is a recommendation that may or may
not make sense for your use case. You always look at warnings; you triage suggestions.</p>
<p>The <strong>hardening index</strong> is the most widely misunderstood number here. It is not a percentage of
&ldquo;security&rdquo; but a plain point ratio: every test awards earned and possible points, and at the end
they are divided. A server without a web server earns neither points nor maximum for the web server
tests – so a server that <em>does</em> run one can score lower with the same amount of care.
<strong>Only compare the index against your own past runs, never against someone else&rsquo;s.</strong></p>
<h3 id="step-3-read-the-report-not-the-screen-output">Step 3: Read the report, not the screen output</h3>
<p>The console output is the worst of the three information sources. Ten lines of
&ldquo;Consider hardening SSH configuration [SSH-7408]&rdquo; in a row do not tell you <em>which</em> ten options are
meant. Those are in the machine-readable report:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">grep <span class="s2">&#34;^suggestion\[\]=SSH-7408&#34;</span> /var/log/lynis-report.dat</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">suggestion[]=SSH-7408|Consider hardening SSH configuration|AllowTcpForwarding (set YES to NO)|-|
</span></span><span class="line"><span class="cl">suggestion[]=SSH-7408|Consider hardening SSH configuration|ClientAliveCountMax (set 3 to 2)|-|
</span></span><span class="line"><span class="cl">suggestion[]=SSH-7408|Consider hardening SSH configuration|LogLevel (set INFO to VERBOSE)|-|
</span></span><span class="line"><span class="cl">suggestion[]=SSH-7408|Consider hardening SSH configuration|MaxAuthTries (set 6 to 3)|-|
</span></span><span class="line"><span class="cl">suggestion[]=SSH-7408|Consider hardening SSH configuration|MaxSessions (set 10 to 2)|-|
</span></span><span class="line"><span class="cl">suggestion[]=SSH-7408|Consider hardening SSH configuration|PermitRootLogin (set YES to (FORCED-COMMANDS-ONLY|NO|PROHIBIT-PASSWORD|WITHOUT-PASSWORD))|-|
</span></span><span class="line"><span class="cl">suggestion[]=SSH-7408|Consider hardening SSH configuration|Port (set 22 to )|-|
</span></span><span class="line"><span class="cl">suggestion[]=SSH-7408|Consider hardening SSH configuration|TCPKeepAlive (set YES to NO)|-|
</span></span><span class="line"><span class="cl">suggestion[]=SSH-7408|Consider hardening SSH configuration|X11Forwarding (set YES to NO)|-|
</span></span><span class="line"><span class="cl">suggestion[]=SSH-7408|Consider hardening SSH configuration|AllowAgentForwarding (set YES to NO)|-|</span></span></code></pre></div>
</div>
<p>The third field is the concrete instruction. Remember these three files:</p>
<ul>
<li><code>/var/log/lynis-report.dat</code> – the result as a key/value list, ideal for grepping</li>
<li><code>/var/log/lynis.log</code> – the full protocol including the reasoning behind every test</li>
<li>the screen output – for skimming only</li>
</ul>
<p>What exactly Lynis expects in a test is in the log. Kernel parameters, for example:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">grep <span class="s2">&#34;different value than expected&#34;</span> /var/log/lynis.log <span class="p">|</span> sed <span class="s2">&#34;s/.*sysctl key //&#34;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">dev.tty.ldisc_autoload has a different value than expected in scan profile. Expected=0, Real=1
</span></span><span class="line"><span class="cl">fs.protected_fifos has a different value than expected in scan profile. Expected=2, Real=1
</span></span><span class="line"><span class="cl">kernel.kptr_restrict has a different value than expected in scan profile. Expected=2, Real=0
</span></span><span class="line"><span class="cl">kernel.modules_disabled has a different value than expected in scan profile. Expected=1, Real=0
</span></span><span class="line"><span class="cl">kernel.sysrq has a different value than expected in scan profile. Expected=0, Real=438
</span></span><span class="line"><span class="cl">kernel.unprivileged_bpf_disabled has a different value than expected in scan profile. Expected=1, Real=2
</span></span><span class="line"><span class="cl">kernel.yama.ptrace_scope has a different value than expected in scan profile. Expected=1 2 3, Real=0
</span></span><span class="line"><span class="cl">net.core.bpf_jit_harden has a different value than expected in scan profile. Expected=2, Real=0
</span></span><span class="line"><span class="cl">net.ipv4.conf.all.forwarding has a different value than expected in scan profile. Expected=0, Real=1
</span></span><span class="line"><span class="cl">net.ipv4.conf.all.log_martians has a different value than expected in scan profile. Expected=1, Real=0
</span></span><span class="line"><span class="cl">net.ipv4.conf.all.rp_filter has a different value than expected in scan profile. Expected=1, Real=0
</span></span><span class="line"><span class="cl">net.ipv4.conf.all.send_redirects has a different value than expected in scan profile. Expected=0, Real=1
</span></span><span class="line"><span class="cl">net.ipv4.conf.default.accept_redirects has a different value than expected in scan profile. Expected=0, Real=1
</span></span><span class="line"><span class="cl">net.ipv4.conf.default.log_martians has a different value than expected in scan profile. Expected=1, Real=0
</span></span><span class="line"><span class="cl">net.ipv6.conf.all.accept_redirects has a different value than expected in scan profile. Expected=0, Real=1
</span></span><span class="line"><span class="cl">net.ipv6.conf.default.accept_redirects has a different value than expected in scan profile. Expected=0, Real=1</span></span></code></pre></div>
</div>
<p>Sixteen deviations – and three of them you must not adopt. More on that shortly.</p>
<h3 id="step-4-fix-the-findings-that-matter">Step 4: Fix the findings that matter</h3>
<h4 id="update-packages-pkgs-7392">Update packages (PKGS-7392)</h4>
<p>The first warning is the most mundane and the most important. Lynis compares installed packages
against the available security updates:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">apt list --upgradable
</span></span><span class="line"><span class="cl">apt-get -y upgrade</span></span></code></pre></div>
</div>
<p>If this keeps
showing up for you, Lynis is not the problem – the missing automation is. That is what
<a href="/en/tutorials/unattended-upgrades-automatic-updates/">unattended-upgrades</a> is for.</p>
<h4 id="bring-ssh-up-to-date-ssh-7408">Bring SSH up to date (SSH-7408)</h4>
<p>Instead of touching <code>/etc/ssh/sshd_config</code>, we create a drop-in file. Debian 13 loads everything
from <code>/etc/ssh/sshd_config.d/</code>, and a package update leaves your change untouched:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt; /etc/ssh/sshd_config.d/99-hardening.conf <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">LogLevel VERBOSE
</span></span></span><span class="line"><span class="cl"><span class="s">MaxAuthTries 3
</span></span></span><span class="line"><span class="cl"><span class="s">MaxSessions 2
</span></span></span><span class="line"><span class="cl"><span class="s">TCPKeepAlive no
</span></span></span><span class="line"><span class="cl"><span class="s">X11Forwarding no
</span></span></span><span class="line"><span class="cl"><span class="s">AllowAgentForwarding no
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span></span></span></code></pre></div>
</div>
<p><code>LogLevel VERBOSE</code> additionally logs the fingerprint of the key that was used – exactly the
information you need during an incident. <code>MaxAuthTries 3</code> limits failed attempts per connection,
and <code>AllowAgentForwarding no</code> prevents someone with root on the server from riding along on your
local SSH agent.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Warning
  </p>
  <div class="prose-kitchen text-sm"><strong>Test first, then reload.</strong> <code>sshd -t</code> validates the configuration without touching the service. A
typo plus a blind <code>restart</code> locks you out – and opening a second terminal only helps as long as it
is already open.</div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sshd -t <span class="o">&amp;&amp;</span> systemctl reload ssh
</span></span><span class="line"><span class="cl">sshd -T <span class="p">|</span> grep -E <span class="s2">&#34;^(maxauthtries|maxsessions|x11forwarding|loglevel)&#34;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">maxauthtries 3
</span></span><span class="line"><span class="cl">maxsessions 2
</span></span><span class="line"><span class="cl">x11forwarding no
</span></span><span class="line"><span class="cl">loglevel VERBOSE</span></span></code></pre></div>
</div>
<p>The two sharpest items on the list – <code>PermitRootLogin</code> and <code>PasswordAuthentication</code> – are
deliberately missing here. You only set those once a working SSH key is in place, and that is
exactly what <a href="/en/tutorials/harden-ssh/">Harden SSH</a> walks you through. If you have done it,
SSH-7408 is shorter for you from the start.</p>
<h4 id="kernel-parameters--here-is-the-docker-trap">Kernel parameters – here is the Docker trap</h4>
<p>Of the sixteen deviating sysctl values we adopt thirteen:</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/sysctl.d/99-lynis.conf</span>
</span></span><span class="line"><span class="cl"><span class="na">dev.tty.ldisc_autoload</span> <span class="o">=</span> <span class="s">0</span>
</span></span><span class="line"><span class="cl"><span class="na">fs.protected_fifos</span> <span class="o">=</span> <span class="s">2</span>
</span></span><span class="line"><span class="cl"><span class="na">kernel.kptr_restrict</span> <span class="o">=</span> <span class="s">2</span>
</span></span><span class="line"><span class="cl"><span class="na">kernel.sysrq</span> <span class="o">=</span> <span class="s">0</span>
</span></span><span class="line"><span class="cl"><span class="na">kernel.yama.ptrace_scope</span> <span class="o">=</span> <span class="s">1</span>
</span></span><span class="line"><span class="cl"><span class="na">net.core.bpf_jit_harden</span> <span class="o">=</span> <span class="s">2</span>
</span></span><span class="line"><span class="cl"><span class="na">net.ipv4.conf.all.log_martians</span> <span class="o">=</span> <span class="s">1</span>
</span></span><span class="line"><span class="cl"><span class="na">net.ipv4.conf.default.log_martians</span> <span class="o">=</span> <span class="s">1</span>
</span></span><span class="line"><span class="cl"><span class="na">net.ipv4.conf.all.rp_filter</span> <span class="o">=</span> <span class="s">1</span>
</span></span><span class="line"><span class="cl"><span class="na">net.ipv4.conf.all.send_redirects</span> <span class="o">=</span> <span class="s">0</span>
</span></span><span class="line"><span class="cl"><span class="na">net.ipv4.conf.default.accept_redirects</span> <span class="o">=</span> <span class="s">0</span>
</span></span><span class="line"><span class="cl"><span class="na">net.ipv6.conf.all.accept_redirects</span> <span class="o">=</span> <span class="s">0</span>
</span></span><span class="line"><span class="cl"><span class="na">net.ipv6.conf.default.accept_redirects</span> <span class="o">=</span> <span class="s">0</span></span></span></code></pre></div>
</div>
<p>The last four concern ICMP redirects – a router&rsquo;s hint to send a packet via a different path. A
server with a fixed default route needs neither to send nor to accept them, and Docker containers
still reach the network over IPv4 and IPv6 without them.</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sysctl -p /etc/sysctl.d/99-lynis.conf</span></span></code></pre></div>
</div>
<p>Three values are missing from that file, and that is on purpose:</p>
<ul>
<li><strong><code>net.ipv4.conf.all.forwarding = 0</code></strong> – Lynis wants IP forwarding disabled. Docker needs it to
route packets between the container network and the outside world. Set it to 0 and <strong>not a single
container has network access</strong> any more. If you want to know why,
<a href="/en/tutorials/understanding-docker-networks/">understanding Docker networks</a> helps.</li>
<li><strong><code>kernel.modules_disabled = 1</code></strong> – forbids the kernel from loading further modules. Depending on
the stack, Docker loads <code>overlay</code>, <code>br_netfilter</code> or <code>nf_tables</code> on demand. On a container host
that is a time bomb that goes off with your next new stack.</li>
<li><strong><code>kernel.unprivileged_bpf_disabled = 1</code></strong> – here Lynis is simply too narrow. Debian 13 is on
<code>2</code>, which is the <strong>stricter</strong> setting: BPF for unprivileged users is off and cannot be re-enabled
at runtime. Lynis checks for exact equality and therefore complains about an improvement.</li>
</ul>
<p>After applying, verify that your containers can still talk – <code>rp_filter</code> is the value most likely
to cause trouble:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --rm alpine:3.22 sh -c <span class="s2">&#34;wget -qO- -T5 https://example.com &gt;/dev/null &amp;&amp; echo NET-OK&#34;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">NET-OK</span></span></code></pre></div>
</div>
<h4 id="password-policy-and-umask-auth-9286-auth-9328">Password policy and umask (AUTH-9286, AUTH-9328)</h4>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sed -i <span class="s1">&#39;s/^PASS_MAX_DAYS.*/PASS_MAX_DAYS\t365/; s/^PASS_MIN_DAYS.*/PASS_MIN_DAYS\t1/&#39;</span> /etc/login.defs
</span></span><span class="line"><span class="cl">grep -q <span class="s2">&#34;^UMASK&#34;</span> /etc/login.defs <span class="o">||</span> <span class="nb">printf</span> <span class="s1">&#39;UMASK\t\t027\n&#39;</span> &gt;&gt; /etc/login.defs</span></span></code></pre></div>
</div>
<p><code>UMASK 027</code> makes sure newly created files are no longer world-readable. That is sensible – but it
has a side effect that catches up with you at the next configuration step, see
&ldquo;When it doesn&rsquo;t work&rdquo;.</p>
<p>We did <strong>not</strong> adopt <strong>AUTH-9229</strong> and <strong>AUTH-9230</strong>, which ask for <code>SHA_CRYPT_MIN_ROUNDS</code> in
<code>/etc/login.defs</code>. Debian 13 hashes passwords with <strong>yescrypt</strong>:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">grep ^ENCRYPT_METHOD /etc/login.defs</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">ENCRYPT_METHOD YESCRYPT</span></span></code></pre></div>
</div>
<p>The <code>SHA_CRYPT_*</code> values apply exclusively to SHA-512 hashes. Adding them here would make the Lynis
message disappear without changing anything – the classic fake improvement. The test really does
only look for those two lines and ignores <code>ENCRYPT_METHOD</code> entirely.</p>
<h4 id="block-unused-kernel-modules-usb-1000-strg-1846-netw-3200">Block unused kernel modules (USB-1000, STRG-1846, NETW-3200)</h4>
<p>A VPS needs neither USB storage nor FireWire nor the DCCP, SCTP, RDS or TIPC network protocols.
Every loaded module is attack surface:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt; /etc/modprobe.d/99-lynis-blacklist.conf <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">install usb-storage /bin/true
</span></span></span><span class="line"><span class="cl"><span class="s">install firewire-core /bin/true
</span></span></span><span class="line"><span class="cl"><span class="s">install firewire_ohci /bin/true
</span></span></span><span class="line"><span class="cl"><span class="s">install ohci1394 /bin/true
</span></span></span><span class="line"><span class="cl"><span class="s">install dccp /bin/true
</span></span></span><span class="line"><span class="cl"><span class="s">install sctp /bin/true
</span></span></span><span class="line"><span class="cl"><span class="s">install rds /bin/true
</span></span></span><span class="line"><span class="cl"><span class="s">install tipc /bin/true
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span></span></span></code></pre></div>
</div>
<p><code>install &lt;module&gt; /bin/true</code> is stronger than <code>blacklist</code>: <code>blacklist</code> only prevents automatic
loading, a manual <code>modprobe</code> still works. Verify the result with a dry run:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">modprobe -n -v sctp</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">install /bin/true</span></span></code></pre></div>
</div>
<h4 id="a-banner-lynis-can-count-bann-7126-bann-7130">A banner Lynis can count (BANN-7126, BANN-7130)</h4>
<p>Lynis searches <code>/etc/issue</code> for legal buzzwords and wants to find <strong>at least five</strong> of them – from
a hard-coded English list (<code>authori</code>, <code>monitor</code>, <code>log</code>, <code>legal</code>, <code>prohibited</code>, <code>access</code>, <code>system</code>
…). A banner in another language fails the test no matter how clear it is. So make it bilingual:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt; /etc/issue <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">Zugriff nur für autorisierte Nutzer. Alle Zugriffe werden protokolliert.
</span></span></span><span class="line"><span class="cl"><span class="s">Authorized access only. All connections to this system are monitored and
</span></span></span><span class="line"><span class="cl"><span class="s">logged. Unauthorized use is prohibited and may be prosecuted by law.
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">cp /etc/issue /etc/issue.net</span></span></code></pre></div>
</div>
<p><code>/etc/issue</code> shows up on the local console, <code>/etc/issue.net</code> is what SSH displays before login. The
latter only happens if you tell sshd about it – out of the box the banner is <code>none</code>:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Banner /etc/issue.net&#34;</span> &gt;&gt; /etc/ssh/sshd_config.d/99-hardening.conf
</span></span><span class="line"><span class="cl">sshd -t <span class="o">&amp;&amp;</span> systemctl reload ssh
</span></span><span class="line"><span class="cl">sshd -T <span class="p">|</span> grep -i <span class="s2">&#34;^banner&#34;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">banner /etc/issue.net</span></span></code></pre></div>
</div>
<h4 id="package-integrity-and-the-audit-log-pkgs-7370-acct-9622-acct-9628">Package integrity and the audit log (PKGS-7370, ACCT-9622, ACCT-9628)</h4>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">apt-get install -y debsums apt-show-versions acct auditd</span></span></code></pre></div>
</div>
<p><code>debsums</code> compares the checksums of installed files against those from the package – so you notice
when someone swapped out <code>/usr/bin/ssh</code>. Lynis additionally wants the check to run regularly:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sed -i <span class="s1">&#39;s/^CRON_CHECK=.*/CRON_CHECK=weekly/&#39;</span> /etc/default/debsums</span></span></code></pre></div>
</div>
<p><code>auditd</code> records kernel events. Freshly installed it has <strong>no rules at all</strong> – and Lynis promptly
answers with a new finding that was not there before:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">* Audit daemon is enabled with an empty ruleset. Disable the daemon or define rules [ACCT-9630]</span></span></code></pre></div>
</div>
<p>That is not a bug, it is correct: an audit daemon without rules costs resources and delivers
nothing. A small, sensible ruleset for a Docker host:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt; /etc/audit/rules.d/99-serverkueche.rules <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">-w /etc/passwd -p wa -k identity
</span></span></span><span class="line"><span class="cl"><span class="s">-w /etc/shadow -p wa -k identity
</span></span></span><span class="line"><span class="cl"><span class="s">-w /etc/group -p wa -k identity
</span></span></span><span class="line"><span class="cl"><span class="s">-w /etc/sudoers -p wa -k privileged
</span></span></span><span class="line"><span class="cl"><span class="s">-w /etc/sudoers.d/ -p wa -k privileged
</span></span></span><span class="line"><span class="cl"><span class="s">-w /etc/ssh/sshd_config -p wa -k sshd
</span></span></span><span class="line"><span class="cl"><span class="s">-w /etc/ssh/sshd_config.d/ -p wa -k sshd
</span></span></span><span class="line"><span class="cl"><span class="s">-w /var/log/lastlog -p wa -k logins
</span></span></span><span class="line"><span class="cl"><span class="s">-w /usr/bin/docker -p x -k docker
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">augenrules --load
</span></span><span class="line"><span class="cl">auditctl -l</span></span></code></pre></div>
</div>
<p><code>-p wa</code> means &ldquo;on write and attribute change&rdquo;, <code>-k</code> sets the keyword you search for later:
<code>ausearch -k sshd</code>. The last rule logs every <code>docker</code> invocation – on a server where Docker access
effectively means root access, that is the most interesting line of the lot.</p>
<h4 id="the-time-warning-that-comes-sooner-or-later-time-3185">The time warning that comes sooner or later (TIME-3185)</h4>
<p>A second warning shows up on almost every server at some point and looks like a real problem, but is
not one:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">timedatectl timesync-status <span class="p">|</span> grep -E <span class="s2">&#34;Poll|Offset&#34;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Poll interval: 34min 8s (min: 32s; max 34min 8s)
</span></span><span class="line"><span class="cl">       Offset: +1.462ms</span></span></code></pre></div>
</div>
<p>The clock is off by 1.5 milliseconds – it is perfectly in sync. Lynis, however, measures the age of
the file <code>/run/systemd/timesync/synchronized</code> and warns beyond <strong>2048 seconds</strong>. And 2048 seconds is
exactly the default maximum systemd-timesyncd raises its poll interval to once the clock is stable.
So the test inevitably fires on every healthy system sooner or later.</p>
<p>Instead of ignoring the warning, we push the interval just below the threshold. Then this test
means something again:</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/timesyncd.conf.d/99-lynis.conf</span>
</span></span><span class="line"><span class="cl"><span class="k">[Time]</span>
</span></span><span class="line"><span class="cl"><span class="na">PollIntervalMaxSec</span><span class="o">=</span><span class="s">1024</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">chmod <span class="m">0644</span> /etc/systemd/timesyncd.conf.d/99-lynis.conf
</span></span><span class="line"><span class="cl">systemctl restart systemd-timesyncd
</span></span><span class="line"><span class="cl">timedatectl timesync-status <span class="p">|</span> grep Poll</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Poll interval: 1min 4s (min: 32s; max 17min 4s)</span></span></code></pre></div>
</div>
<p>The <code>chmod</code> is not a typo – why it is needed is explained further below.</p>
<h3 id="step-5-measure-again">Step 5: Measure again</h3>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">lynis audit system --quick --no-colors <span class="p">|</span> tee /root/lynis-run2.txt
</span></span><span class="line"><span class="cl">grep -E <span class="s2">&#34;Hardening index|Tests performed&#34;</span> /root/lynis-run2.txt</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">  Hardening index : 77 [###############     ]
</span></span><span class="line"><span class="cl">  Tests performed : 254</span></span></code></pre></div>
</div>
<p>From 64 to 77, and the <code>Warnings</code> section is gone entirely – with zero warnings Lynis simply omits
it. Suggestions shrank from 44 to 22.</p>
<h3 id="step-6-deliberate-exceptions-in-customprf">Step 6: Deliberate exceptions in <code>custom.prf</code></h3>
<p>What remains are suggestions you re-read on every run and discard every time. That is exactly what
<code>/etc/lynis/custom.prf</code> is for. Important: <strong>every line gets a reason</strong>, otherwise in six months you
will not know whether it was a decision or laziness.</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/lynis/custom.prf</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># FILE-6310: separate partitions for /home and /var. A VPS has one disk;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># repartitioning after the fact would be more risk than benefit here.</span>
</span></span><span class="line"><span class="cl"><span class="na">skip-test</span><span class="o">=</span><span class="s">FILE-6310</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># AUTH-9229/9230: ask for SHA_CRYPT_*_ROUNDS. Debian 13 uses yescrypt,</span>
</span></span><span class="line"><span class="cl"><span class="c1"># so those values would have no effect whatsoever.</span>
</span></span><span class="line"><span class="cl"><span class="na">skip-test</span><span class="o">=</span><span class="s">AUTH-9229</span>
</span></span><span class="line"><span class="cl"><span class="na">skip-test</span><span class="o">=</span><span class="s">AUTH-9230</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># BOOT-5122: GRUB password. Anyone reaching the netcup VNC console already has</span>
</span></span><span class="line"><span class="cl"><span class="c1"># access to the server panel and therefore to snapshots and the rescue system.</span>
</span></span><span class="line"><span class="cl"><span class="na">skip-test</span><span class="o">=</span><span class="s">BOOT-5122</span></span></span></code></pre></div>
</div>
<p>The next run is noticeably quieter afterwards – but the index does not move:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">  Hardening index : 77 [###############     ]
</span></span><span class="line"><span class="cl">  Tests performed : 250
</span></span><span class="line"><span class="cl">  Suggestions (17):</span></span></code></pre></div>
</div>
<p>That is not a bug, it is proof the number cannot be gamed: a skipped test removes both its earned
<strong>and</strong> its possible points from the calculation. From 22 down to 17 suggestions, four tests fewer –
index unchanged at 77. <code>custom.prf</code> is a tool against noise, not a tool for your score.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Warning
  </p>
  <div class="prose-kitchen text-sm"><code>skip-test</code> disables the test <strong>permanently</strong> – even once the situation changes. Walk through the
list at every quarterly audit and ask yourself whether the reason still holds. Anything you only
want to hide this once does not belong here.</div>
</div>
<p>We deliberately leave two suggestions <strong>visible</strong> instead of switching them off: <code>HRDN-7230</code>
(malware scanner) and <code>LOGG-2154</code> (central logging). Both are hard to justify on a single small VPS –
ClamAV alone claims around a gigabyte of RAM for its signature database – but the moment a second
server joins, the maths change. A suggestion you re-read and re-reject every time is a reminder; a
disabled one is forgotten.</p>
<h3 id="step-7-automate-the-audit">Step 7: Automate the audit</h3>
<p>A one-off audit is a snapshot. It gets interesting when you can see the trend – especially after
every new stack you deploy. Lynis ships a mode for exactly that:</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/system/lynis-audit.service</span>
</span></span><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Lynis security audit</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/sbin/lynis audit system --cronjob --quiet</span></span></span></code></pre></div>
</div>
<p><code>--cronjob</code> turns off colours and prompts, <code>--quiet</code> suppresses everything but warnings. The timer
next to it:</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/system/lynis-audit.timer</span>
</span></span><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Monthly Lynis audit</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Timer]</span>
</span></span><span class="line"><span class="cl"><span class="na">OnCalendar</span><span class="o">=</span><span class="s">monthly</span>
</span></span><span class="line"><span class="cl"><span class="na">RandomizedDelaySec</span><span class="o">=</span><span class="s">1h</span>
</span></span><span class="line"><span class="cl"><span class="na">Persistent</span><span class="o">=</span><span class="s">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">timers.target</span></span></span></code></pre></div>
</div>
<p><code>Persistent=true</code> catches up on a missed run if the server was off at the scheduled time.</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl daemon-reload
</span></span><span class="line"><span class="cl">systemctl <span class="nb">enable</span> --now lynis-audit.timer
</span></span><span class="line"><span class="cl">systemctl list-timers lynis-audit.timer</span></span></code></pre></div>
</div>
<p>Afterwards you pull a run&rsquo;s index out of the report with a single line – that is the number you
watch across the months:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">grep ^hardening_index<span class="o">=</span> /var/log/lynis-report.dat</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">hardening_index=77</span></span></code></pre></div>
</div>
<p>If you want the result pushed to your phone, append an <code>ExecStartPost</code> line with
<a href="/en/tutorials/ntfy-push-notifications/">ntfy</a> to the service.</p>
<h2 id="when-it-doesnt-work">When it doesn&rsquo;t work</h2>
<div class="troubleshoot not-prose">
<p><strong>After setting <code>UMASK 027</code> a service no longer starts and reports &ldquo;Permission denied&rdquo; on its own configuration file.</strong> The new umask applies from your next login onwards, and everything you create with <code>&gt;</code> or <code>cat &gt;</code> after that gets mode <code>0640</code> instead of <code>0644</code>. Services running under their own user – <code>systemd-timesyncd</code> uses <code>DynamicUser</code>, for instance – can then no longer read the file. The journal shows <code>Failed to open ...: Permission denied</code>. Set the permissions explicitly for configuration files that are not owned by root: <code>chmod 0644 /etc/systemd/timesyncd.conf.d/99-lynis.conf</code>.</p>
<p><strong>After applying the sysctl values none of the containers have network access.</strong> Then <code>net.ipv4.conf.all.forwarding</code> ended up at 0. Lynis suggests it, but without IP forwarding Docker cannot pass packets between the container network and the outside world. Remove the line from <code>/etc/sysctl.d/99-lynis.conf</code> and reset the value immediately with <code>sysctl -w net.ipv4.conf.all.forwarding=1</code> – restarting Docker alone will not help.</p>
<p><strong><code>systemd-timesyncd did not synchronized the time recently</code> persists even though <code>timedatectl</code> reports <code>NTPSynchronized=yes</code>.</strong> Lynis does not check the clock but the age of <code>/run/systemd/timesync/synchronized</code>, and warns beyond 2048 seconds. Since systemd raises its poll interval to exactly 2048 seconds when the clock is stable, the test inevitably runs into the warning. Either lower <code>PollIntervalMaxSec</code> as shown in step 4, or add the test to <code>custom.prf</code> – but do not just ignore it, or you will miss the day time synchronisation really breaks.</p>
<p><strong>Lynis reports <code>Consider hardening SSH configuration</code> ten times and never says what it means.</strong> The screen output drops the details. They are in the third field of <code>grep &quot;^suggestion\[\]=SSH-7408&quot; /var/log/lynis-report.dat</code> – that gives you the parameter and target value for every line.</p>
<p><strong>After installing auditd a finding was added instead of removed.</strong> <code>ACCT-9630</code> reports a running audit daemon with an empty ruleset. That is intended: auditd without rules burns resources without logging anything. Put rules under <code>/etc/audit/rules.d/</code> and load them with <code>augenrules --load</code> – or uninstall auditd again if you are not going to read the log.</p>

</div>
<p class="mt-3 text-sm text-slate-500 dark:text-slate-400">
  Your problem is not listed?
  <a href="/en/errors/" class="font-medium text-paprika-700 hover:underline dark:text-paprika-400">Search all error messages →</a>
</p>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<p>Lynis updates itself through apt; the CISOfy repository gets you new test collections promptly.
After a major update, check briefly whether new findings suddenly appear – those are usually new
tests, not new problems on your server.</p>
<p><strong>Realistic effort:</strong> the first audit including the fixes costs you an evening. After that it is
roughly 20 minutes per quarter – skim the report, triage new findings, re-read <code>custom.prf</code>. The
monthly timer runs without you; you only look when the index moves.</p>
<p><strong>Back up the results of your work.</strong> The real value is not in Lynis but in the files you created.
Add them to your <a href="/en/tutorials/restic-backups/">Restic backup</a>:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/etc/lynis/custom.prf
</span></span><span class="line"><span class="cl">/etc/sysctl.d/99-lynis.conf
</span></span><span class="line"><span class="cl">/etc/modprobe.d/99-lynis-blacklist.conf
</span></span><span class="line"><span class="cl">/etc/ssh/sshd_config.d/99-hardening.conf
</span></span><span class="line"><span class="cl">/etc/audit/rules.d/
</span></span><span class="line"><span class="cl">/etc/systemd/system/lynis-audit.{service,timer}</span></span></code></pre></div>
</div>
<p>After every larger change – a new reverse proxy, a new database, a newly opened port – a manual run
is worth it. Those are exactly the moments that create the gaps a monthly timer only finds weeks
later.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-sky-300 bg-sky-50 dark:border-sky-800 dark:bg-sky-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">ℹ️</span>What Lynis is not
  </p>
  <div class="prose-kitchen text-sm">Lynis audits <strong>from the inside</strong>: it reads configuration files and system state with root
privileges. It does not tell you what an attacker sees from outside, does not check a web
application for vulnerabilities and does not replace a penetration test. For the outside view you
need a port scan from another host and – for publicly reachable services – a look at
<a href="/en/tutorials/traefik-middlewares-hardening/">Traefik middlewares</a>.</div>
</div>
]]></content:encoded></item></channel></rss>