---
title: "The Hardening Score Went Up. The Attack Path Stayed Open."
description: "A practical Linux hardening methodology that uses Lynis as a sensor, established baselines as context, and controlled retesting to prove which changes reduce attack paths without breaking the service."
date: 2026-08-22
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/posts/linux-hardening-attack-path/
---

## Linux hardening in 60 seconds

A Linux hardening review often ends with a reassuring number. The package list is shorter, a few
kernel parameters have changed, and the hardening index is higher. The number can track work. It
cannot show whether an exposed service still runs as root, whether one administrator account can
be reused across the fleet, whether a compromised process can reach a credential, or whether the
system can survive the next reboot.

That difference is the subject of this methodology. I use [Lynis](https://github.com/CISOfy/lynis)
as a broad local sensor and
[The Practical Linux Hardening Guide](https://github.com/trimstray/the-practical-linux-hardening-guide)
as a catalogue of rationale, policy families, and questions. Neither source becomes an automatic
remediation engine. The final decision comes from the host's role, current platform, exposed
authority, operational dependencies, and a retest of the original security claim.

The central rule is simple:

> A hardening change is complete only when the intended workload still works and the dangerous
> behavior that motivated the change now fails.

This note is source-reviewed methodology, not a hidden benchmark result. It does not publish a
before-and-after Lynis score or claim that the three-state experiment below has already been run.
The protocol is designed so that a future lab record can add those measurements without rewriting
the conclusion around invented evidence.

## A score is an observation, not an outcome

Lynis is useful precisely because it looks across many subsystems: boot configuration, kernel
settings, authentication, shells, filesystems, storage, networking, package state, logging,
schedulers, service configuration, and hardening features. It gives each test a stable identifier,
records technical detail in `/var/log/lynis.log`, and writes machine-readable findings to
`/var/log/lynis-report.dat`.

The tool's own documentation preserves the important boundary. An `OK` result does not prove that
the target is safe, and a `WARNING` is not automatically a vulnerability. Suggestions express
possible room for improvement. Applicability and impact still belong to the auditor.

The hardening index has the same limit. It summarizes the controls Lynis tested and how its current
profile scored them. It does not know the complete business role of the server, the trust carried
by the application, the reachability of an administrative interface, the sensitivity of an
attached secret, or the recovery process surrounding the host.

Two systems can therefore have similar scores and materially different risk:

- a public reverse proxy with narrow service authority and a tested rebuild path;
- an internal management server with a reusable deployment key, broad sudo access, and a backup
  agent that can execute privileged recovery actions across the fleet.

The second host may listen on fewer ports. It can still carry more authority.

<figure class="diagram">
<svg viewBox="0 0 780 322" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Three-state Linux hardening experiment. A baseline host has a reachable path into a privileged service. A score-driven host closes several unrelated checklist items but leaves the privileged path open. A threat-driven host cuts that path while a positive workload test remains healthy.">
<defs><marker id="lh-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker><marker id="lh-c" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow-crit" /></marker></defs>
<text x="0" y="24" class="dg-accent">ONE HOST ROLE · THREE CONTROL STATES</text>
<rect x="0" y="52" width="230" height="176" rx="10" class="dg-box-crit"/><text x="16" y="80" class="dg-crit">A · BASELINE</text><text x="16" y="108" class="dg-label">reachable service</text><text x="16" y="132" class="dg-label">broad authority</text><line x1="22" y1="166" x2="186" y2="166" class="dg-line-crit" marker-end="url(#lh-c)"/><text x="22" y="194" class="dg-muted">entry → privileged effect</text>
<rect x="274" y="52" width="230" height="176" rx="10" class="dg-box"/><text x="290" y="80" class="dg-muted">B · SCORE-DRIVEN</text><text x="290" y="108" class="dg-accent">several checks pass</text><text x="290" y="132" class="dg-label">same authority survives</text><line x1="296" y1="166" x2="460" y2="166" class="dg-line-crit" marker-end="url(#lh-c)"/><text x="296" y="194" class="dg-crit">path still open</text>
<rect x="548" y="52" width="232" height="176" rx="10" class="dg-box-accent"/><text x="564" y="80" class="dg-accent">C · THREAT-DRIVEN</text><text x="564" y="108" class="dg-label">durable edge removed</text><text x="564" y="132" class="dg-label">workload remains healthy</text><line x1="570" y1="166" x2="638" y2="166" class="dg-line"/><rect x="646" y="148" width="34" height="36" rx="6" class="dg-box-crit"/><text x="653" y="172" class="dg-crit">CUT</text><line x1="688" y1="166" x2="750" y2="166" class="dg-line dg-dash"/><text x="564" y="208" class="dg-accent">path denied · service passes</text>
<line x1="230" y1="140" x2="270" y2="140" class="dg-line" marker-end="url(#lh-a)"/><line x1="504" y1="140" x2="544" y2="140" class="dg-line" marker-end="url(#lh-a)"/>
<text x="0" y="270" class="dg-muted">MEASURE</text><text x="94" y="270" class="dg-mono">findings · index · reachable surface · effective privilege · workload health · rollback</text>
<text x="0" y="298" class="dg-accent">PASS</text><text x="94" y="298" class="dg-mono">approved function succeeds AND the original unauthorized behavior is denied</text>
</svg>
<figcaption>The useful comparison keeps the workload and threat model constant. State B asks whether a better score can coexist with the same path; State C requires both security and functional evidence.</figcaption>
</figure>

## Start with the server role, not the scanner

Before the first scan, write a one-page host contract. If this cannot be completed, the hardening
review does not yet know what it is protecting.

| Question | Evidence to record |
| --- | --- |
| What does the host do? | Service owner, workload, data class, availability objective |
| Who can reach it? | Expected source networks, public exposure, management route |
| Which identities does it trust? | Human administrators, service users, workload identities, SSH principals |
| What can the workload control? | Files, sockets, databases, cloud APIs, orchestration or backup systems |
| How is it rebuilt? | Image or configuration source, package provenance, restore dependencies |
| What must never happen? | Named unauthorized action, credential access, privilege transition, or data route |

This contract turns a generic Lynis suggestion into a decision. A compiler on a disposable build
worker has a different meaning from the same compiler on an immutable production appliance. A
second name server may be essential for a server and irrelevant to an isolated single-purpose lab
host. A service legitimately requiring network and device access should not be forced into a
generic sandbox profile only to improve an exposure number.

The same rule applies to external baselines. CIS Benchmarks are consensus configuration
recommendations for specific products and versions. OpenSCAP evaluates explicit SCAP content and
profiles. The trimstray guide provides useful explanations and operational cautions, but its own
README says that it is a checklist rather than an exhaustive standard and that its examples were
built around RHEL 7 and CentOS 7.

Use the reasoning. Select current content for the exact platform. Never copy an old control into a
modern host because its name still sounds secure.

## Preserve a reproducible baseline

The first run should be boring, read-only, and attributable. Use a disposable lab clone or an
approved test host before changing production. Record the exact distribution, kernel, Lynis
version, active profile, virtualization context, running services, listening sockets, mounts, and
package source.

The following commands collect a local baseline without changing configuration:

```bash
uname -a
cat /etc/os-release
systemd-detect-virt
lynis show version
lynis show profiles
lynis show settings
ss -lntup
systemctl --type=service --state=running --no-pager
findmnt -rn -o TARGET,SOURCE,FSTYPE,OPTIONS
```

Run the audit from a trusted package or a reviewed checkout. The common system audit is:

```bash
sudo lynis audit system --quick --nocolors --auditor "controlled-hardening-baseline"
```

Lynis purges its detailed log on the next scan. Preserve the evidence immediately, on the audited
host, in a restricted case directory:

```bash
case_dir="/var/tmp/linux-hardening-case-001/baseline"
sudo install -d -m 0700 "$case_dir"
sudo cp --preserve=timestamps /var/log/lynis.log "$case_dir/"
sudo cp --preserve=timestamps /var/log/lynis-report.dat "$case_dir/"
sudo sha256sum "$case_dir/lynis.log" "$case_dir/lynis-report.dat" |
  sudo tee "$case_dir/SHA256SUMS" >/dev/null
```

These files can expose hostnames, package inventory, services, filesystem paths, and policy state.
Keep the originals inside the engagement evidence boundary. Publish only a redacted derivative,
never the raw report from a customer or production host.

## Turn every suggestion into a hypothesis

Do not sort the backlog only by warning versus suggestion or by the points available to the
hardening index. Add the missing operational fields:

1. **Observation:** what did the test actually see?
2. **Applicability:** does the expected state apply to this platform and role?
3. **Authority:** what additional action becomes possible because of the state?
4. **Reachability:** from which realistic source can that action be attempted?
5. **Composition:** which credential, service, mount, socket, or management plane completes the path?
6. **Change risk:** what legitimate function can the remediation break?
7. **Security test:** what exact action should fail afterward?
8. **Positive control:** what exact workload action must continue to succeed?
9. **Rollback:** how will the team recover if the positive control fails?

This produces four queues instead of one long list:

- **path-breaking controls** remove reachable privilege, exposed authority, or credential access;
- **resilience controls** improve detection, integrity, recovery, and safe operation;
- **baseline obligations** are required by policy even when direct attack-path impact is limited;
- **accepted exceptions** have a named reason, owner, compensating control, and expiry date.

An exception is not a silent skip. It is part of the evidence.

<figure class="diagram">
<svg viewBox="0 0 780 316" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Linux hardening evidence lifecycle. A Lynis control becomes a candidate, is checked for platform applicability, connected to reachable authority, changed with a rollback plan, then tested with a positive workload control and a negative security control. Failed checks return to triage.">
<defs><marker id="lh-e" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker></defs>
<text x="0" y="24" class="dg-accent">FROM CONTROL SIGNAL TO VERIFIED STATE</text>
<rect x="0" y="54" width="122" height="70" rx="9" class="dg-box"/><text x="14" y="80" class="dg-label">Candidate</text><text x="14" y="102" class="dg-muted">Lynis ID</text>
<line x1="124" y1="89" x2="150" y2="89" class="dg-line" marker-end="url(#lh-e)"/>
<rect x="154" y="54" width="122" height="70" rx="9" class="dg-box"/><text x="168" y="80" class="dg-label">Applicable</text><text x="168" y="102" class="dg-muted">role + version</text>
<line x1="278" y1="89" x2="304" y2="89" class="dg-line" marker-end="url(#lh-e)"/>
<rect x="308" y="54" width="122" height="70" rx="9" class="dg-box-crit"/><text x="322" y="80" class="dg-label">Path</text><text x="322" y="102" class="dg-crit">reach + authority</text>
<line x1="432" y1="89" x2="458" y2="89" class="dg-line" marker-end="url(#lh-e)"/>
<rect x="462" y="54" width="122" height="70" rx="9" class="dg-box-accent"/><text x="476" y="80" class="dg-label">Change</text><text x="476" y="102" class="dg-muted">rollback ready</text>
<line x1="586" y1="89" x2="612" y2="89" class="dg-line" marker-end="url(#lh-e)"/>
<rect x="616" y="54" width="164" height="70" rx="9" class="dg-box-accent"/><text x="630" y="80" class="dg-label">Retest</text><text x="630" y="102" class="dg-accent">allow + deny</text>
<rect x="154" y="174" width="220" height="76" rx="9" class="dg-box-accent"/><text x="170" y="202" class="dg-accent">POSITIVE CONTROL</text><text x="170" y="226" class="dg-label">expected workload succeeds</text>
<rect x="406" y="174" width="220" height="76" rx="9" class="dg-box-crit"/><text x="422" y="202" class="dg-crit">NEGATIVE CONTROL</text><text x="422" y="226" class="dg-label">dangerous behavior fails</text>
<line x1="698" y1="128" x2="698" y2="212" class="dg-line"/><line x1="698" y1="212" x2="630" y2="212" class="dg-line" marker-end="url(#lh-e)"/>
<path d="M406 250 C406 286, 214 286, 214 254" class="dg-line-crit" marker-end="url(#lh-e)"/><text x="270" y="298" class="dg-crit">FAIL → REOPEN TRIAGE</text>
</svg>
<figcaption>A scanner result earns priority only after applicability, reachability, and authority are connected. Closure requires the expected function to pass and the original dangerous behavior to fail.</figcaption>
</figure>

## Prioritize the controls that change authority

The exact order depends on the host contract, but the following sequence usually creates better
decisions than starting with cosmetic findings.

### 1. Reduce reachable surface

Inventory listening sockets and the process, namespace, interface, and firewall path behind each
one. Remove or bind unnecessary services. Confirm the change from an approved external source as
well as from the local socket table. A closed local process and a filtered network path are
different controls; record which one changed.

### 2. Narrow administrative identity

List who can authenticate, who can use sudo, which commands are permitted, where SSH keys are
authorized, and whether automation shares the same identity as humans. Prefer named principals,
narrow command authorization, short-lived credentials, and an independently tested emergency
route. Do not print private keys, password hashes, or secret values into the evidence bundle.

### 3. Constrain the service, not only the user account

A non-root Unix user is a useful boundary, but the service may still inherit broad filesystem,
device, network, capability, and IPC access. For systemd services, `systemd-analyze security` can
identify missing sandboxing directives and produce an exposure score. Its manual is explicit that
the analysis is incomplete when the service can ask other processes to act through IPC. Treat the
result as another candidate, not a proof of containment.

Start with the service's real needs: writable paths, address families, devices, capabilities,
system calls, temporary storage, home directories, and kernel interfaces. Add one restriction at a
time in a lab or canary, restart the service, exercise the positive control, and inspect the logs.

### 4. Protect credentials and process boundaries

Ask which files, environment variables, agents, sockets, core dumps, and same-user processes can
expose credentials. The Linux Yama LSM documentation explains why unrestricted same-UID `ptrace`
can let one compromised application inspect another process and expand into SSH sessions or
credential agents. Whether a stricter `ptrace_scope` is appropriate depends on debugging,
observability, crash handling, and the platform's existing policy.

This is why a sysctl should never be copied from a list without a positive control. A change that
blocks the incident responder, profiler, or crash collector may be secure in one role and
operationally destructive in another.

### 5. Make detection and recovery part of the hardening state

Package provenance, time synchronization, audit coverage, log forwarding, file integrity,
configuration history, and tested rebuild instructions decide whether a future compromise can be
bounded. A system with no unnecessary services but no trusted rebuild source is not finished.

NIST's server security guidance frames hardening as part of a lifecycle: plan, configure, maintain,
monitor, and preserve the ability to respond. The practical unit is therefore not one host at one
moment. It is the host, its management plane, its evidence, and its recovery path.

## Run the three-state experiment

Use a disposable clone of a supported Linux release and give it one realistic workload—for
example, OpenSSH plus a small systemd-managed HTTP service. Snapshot it before changes. Keep the
network isolated to the approved lab and use reserved identities and data.

### State A — baseline

Record the host contract, local inventory, Lynis output, service health, reachable sockets, current
administrative path, and rebuild time. Define one bounded security claim, such as:

```text
CLAIM            The web service cannot write outside its approved state directory
SOURCE           a controlled request through the approved lab interface
PRINCIPAL        the service account created for the lab
PROTECTED ASSET  a root-owned canary file outside the writable path
POSITIVE CONTROL the service writes its expected application state
NEGATIVE CONTROL the service cannot modify the protected canary
```

Do not exploit a vulnerability or target a real service. The negative control should be a harmless
permission or reachability test against a canary created for this experiment.

### State B — score-driven

Choose several applicable, reversible findings that can improve the index but are not expected to
change the named path. Apply them through the platform's supported configuration mechanism. Reboot
when required, repeat the workload test, re-run Lynis, and repeat the negative control.

The purpose is not to mock the controls. They may be valuable baseline or resilience work. The
question is narrower: did the score change while the named path stayed the same? Record the answer,
including a failed hypothesis.

### State C — threat-driven

Change the earliest durable edge in the named path: service identity, writable path, capability,
socket reachability, sudo rule, credential reader, or management-plane permission. Repeat the same
positive and negative controls from the same source context. Re-run Lynis, but do not require the
index delta to be large. The security result is the denied path plus the healthy workload.

The comparison record should contain observations, not adjectives:

| Measurement | State A | State B | State C |
| --- | --- | --- | --- |
| Lynis version and profile | Record | Same | Same |
| Hardening index | Measure | Measure | Measure |
| Warnings and suggestions | Preserve | Diff | Diff |
| Listening surface | Measure | Recheck | Recheck |
| Named authority path | Test | Test | Test |
| Positive workload control | Pass/fail | Pass/fail | Pass/fail |
| Negative security control | Pass/fail | Pass/fail | Pass/fail |
| Reboot and recovery | Time | Time | Time |

Until those cells contain captured evidence, the article should not publish a numeric conclusion.

## What can break

Hardening is production change, and production change can remove availability as effectively as an
attacker. Put dependency discovery and rollback beside every recommendation.

| Change family | Likely dependency | Positive control | Negative control |
| --- | --- | --- | --- |
| SSH authentication restriction | automation, break-glass access, bastions | approved administrator can connect through the intended route | removed identity or route is rejected |
| systemd filesystem isolation | upload paths, caches, certificates, Unix sockets | normal request can read and write only required state | protected canary remains unchanged |
| Capability reduction | low ports, time, network administration, tracing | service starts and completes its health check | removed privileged operation fails |
| Firewall or bind change | monitoring, load balancers, cluster peers | expected source reaches the service | unapproved source cannot reach it |
| Kernel or ptrace policy | debuggers, profilers, crash collection, EDR | approved diagnostic workflow succeeds | unrelated same-user process cannot inspect the canary process |
| Mount hardening | package scripts, temporary execution, containers | approved update and workload flow succeed | disallowed execution or write path fails |
| Audit and log changes | disk capacity, privacy, SIEM parsing | expected event arrives with correct time and identity | a controlled policy violation is not silent |

Apply changes in a canary, keep console or recovery access outside the path being modified, preserve
the previous configuration, and define the rollback trigger before the window. A setting that is
silently reverted after an outage is not a hardening success.

## Evidence matrix

| Hardening claim | Evidence required | Negative control | What is not enough |
| --- | --- | --- | --- |
| Unnecessary network exposure is removed | process, socket, interface, firewall, and approved external reachability before and after | connect from the previously reachable unapproved source and require failure | `ss` output alone |
| Administrative access is narrow | named principals, SSH policy, sudo scope, successful approved login, and denied removed route | use the removed identity or source route and require rejection | a changed configuration file |
| The service is contained | runtime identity, effective capabilities, writable paths, devices, address families, IPC dependencies, and workload health | attempt the bounded canary action outside the declared authority | a good `systemd-analyze security` score |
| Kernel controls match the role | running value, persistent source, supported-platform documentation, dependency test, and reboot verification | repeat the harmless action the setting is meant to restrict | copying a sysctl list from another distribution |
| Lynis findings are remediated | preserved baseline report, applicable control ID, root-cause change, post-scan diff, owner, and retest | repeat the original security test from the original source | fewer warnings or a higher index |
| Logging is useful | controlled event, trusted timestamp, source identity, transport, storage, alert or query result, and retention owner | generate the canary event and require it to appear end to end | `auditd` installed or a SIEM agent running |
| Recovery is viable | trusted source, configuration history, isolated credentials, timed rebuild, restored workload, and integrity validation | rebuild without depending on the host or identity plane being recovered | a VM snapshot or successful backup job |

## What the final record should contain

Do not hand the owner a terminal screenshot and a percentage. For each accepted control, record:

- host role, owner, environment, distribution, kernel, and workload version;
- Lynis version, active profile, test identifier, original observation, and evidence hashes;
- platform-specific baseline and the exact version used;
- applicability decision and any accepted exception;
- reachable source, effective principal, protected asset, and authority created by the state;
- configuration change, deployment mechanism, change owner, and rollback trigger;
- positive workload control and negative security control;
- reboot, failover, monitoring, and recovery results;
- remaining exposure, compensating controls, retest date, and evidence location.

This structure prevents two common errors. First, a scanner suggestion is not promoted directly to
a finding. Second, a changed setting is not promoted directly to a closed finding.

## The order I would use

If the review has one week, use this sequence:

1. Define the host role, protected authority, and recovery boundary.
2. Preserve a trusted local baseline and Lynis evidence.
3. Validate reachable services and administrative identity paths.
4. Select the few controls that remove the earliest durable edges.
5. Apply one change family at a time with a rollback path.
6. Reboot when required and test the intended workload.
7. Repeat the exact negative security control.
8. Re-run Lynis and record the diff without treating the score as the conclusion.
9. Test log visibility and rebuild or restore the disposable host.
10. Convert the remaining suggestions into owned work, policy obligations, or expiring exceptions.

The result is smaller than a universal checklist and stronger than one. It connects each control to
a system role, a reachable action, a protected asset, an operational dependency, and a result that
another engineer can repeat.

## My conclusion

The Practical Linux Hardening Guide is valuable because it explains why controls exist and connects
them to established policy families. Lynis is valuable because it quickly turns a live Unix-like
system into a broad, attributable set of observations. Their best use is together—but not as an
automatic sequence of scan, copy, paste, and celebrate.

Start with what the host is trusted to do. Use current platform content. Treat every tool result as
a candidate. Remove the earliest reachable authority edge. Keep the service alive. Then repeat the
original action and preserve the denial.

A higher score may accompany that result. The evidence is what makes it hardening.

## MITRE ATT&CK mapping

| Tactic | Technique ID | Technique name | Defender verification signal |
| --- | --- | --- | --- |
| Execution | `T1059.004` | Command and Scripting Interpreter: Unix Shell | Audit command execution in interactive sessions via `auditd` |
| Privilege Escalation | `T1548.001` | Abuse Elevation Control: Setuid and Setgid | `find / -perm -4000 -type f` audits against baseline inventory |
| Persistence | `T1543.002` | Create or Modify System Process: systemd Service | `systemd-analyze security <unit>` sandboxing posture audit |
| Credential Access | `T1003.008` | OS Credential Dumping: /etc/passwd and /etc/shadow | Strict file mode `000` / `640` on `/etc/shadow` verified via Lynis |
| Defense Evasion | `T1070.002` | Indicator Removal on Host: Clear Linux Logs | Remote syslog forwarding + immutable audit log directory |

## Defender action checklist

Use this actionable checklist during post-assessment remediation:

1. **Enforce Yama ptrace restrictions:** Write `kernel.yama.ptrace_scope = 2` to `/etc/sysctl.d/99-security.conf` and apply with `sysctl -p`.
2. **Harden exposed systemd units:** Add `ProtectSystem=strict`, `ProtectHome=yes`, and `NoNewPrivileges=yes` to service drop-ins.
3. **Audit privileged transitions:** Add `-w /etc/sudoers -p wa -k identity_changes` to `/etc/audit/rules.d/audit.rules`.
4. **Verify workload survivability:** Run test suite against target service before signing off on hardening baseline.

## Sources reviewed

- [trimstray: The Practical Linux Hardening Guide](https://github.com/trimstray/the-practical-linux-hardening-guide)
- [CISOfy: Lynis source repository](https://github.com/CISOfy/lynis)
- [CISOfy: Lynis installation and usage](https://cisofy.com/documentation/lynis/)
- [CISOfy: Lynis report and logging guide](https://cisofy.com/documentation/lynis/get-started/)
- [Center for Internet Security: CIS Benchmarks](https://www.cisecurity.org/cis-benchmarks)
- [OpenSCAP 1.4.1 user manual](https://static.open-scap.org/openscap-1.4.1/oscap_user_manual.html)
- [NIST SP 800-123: Guide to General Server Security](https://www.nist.gov/publications/guide-general-server-security)
- [systemd-analyze manual](https://www.man7.org/linux/man-pages/man1/systemd-analyze.1.html)
- [Linux kernel documentation: Yama](https://docs.kernel.org/admin-guide/LSM/Yama.html)