Skip to content
ernwPublic

About

A cross-platform security auditing and hardening tool. Load a ruleset, run an audit, apply fixes, and roll back if needed, all from a single binary with no external dependencies.

Resources

Security policy

Stars

11 stars

Watchers

0 watching

Forks

Repository files navigation

Hardener

A cross-platform security auditing and hardening tool. Load a ruleset, run an audit, apply fixes, and roll back if needed, all from a single binary with no external dependencies.


Prerequisites

Go 1.24+ is required to build the binary. The golang-go package in most distro repos is too old; install from the official source instead:

GO_VERSION=$(curl -s https://go.dev/VERSION?m=text | head -1)
curl -fsSL "https://go.dev/dl/${GO_VERSION}.linux-amd64.tar.gz" -o /tmp/go.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf /tmp/go.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.profile
source ~/.profile
go version

macOS: use darwin-arm64 or darwin-amd64 instead of linux-amd64 in the URL above, or install via brew install go.


Quick Start

Linux

1. Download the ruleset and the guide

Resource Link
Ruleset (YAML) ruleset.yaml
Hardening Guide (PDF/MD) ERNW_Hardening_Linux.md

2. Build the binary

git clone https://github.com/mev0lent/hardener.git
cd hardener
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o hardener-linux .

3. Audit your system

# Interactive suite picker
./hardener-linux audit --ruleset ruleset.yaml

# Run all suites non-interactively
./hardener-linux audit --ruleset ruleset.yaml --all

# Include high-security checks (default: baseline only)
./hardener-linux audit --ruleset ruleset.yaml --all --security-level high

4. Apply fixes

./hardener-linux fix --ruleset ruleset.yaml --all

5. Roll back

# Undo the last fix run
./hardener-linux rollback --latest

# Undo a specific run
./hardener-linux rollback --timestamp 2026-05-18T14:32:00Z

# Roll back specific files only
./hardener-linux rollback --files /etc/sysctl.conf,/etc/hosts.deny

A fix only counts as fixed when its check passes when re-run straight after the fix. A fix whose command succeeds while the check still fails is reported as not effective, often because its post_action (a reboot or a service restart) is still due.

Before each fix, its affected_file and any live kernel values the check reads (/proc/sys/..., sysctl -n/-w <key>) are written to runs.json. If that backup fails, the fix is skipped. Rollback removes files a fix created, recreates files it deleted, and refuses to overwrite a file that changed after the fix. A link the fix created, such as the /dev/null link of systemctl mask, is removed. Not covered: package installs and removals, directories, files named only in prose or by glob in affected_file, and ~/ paths. The fix still runs for these.

fix --dry-run runs the audit and lists the fix each failing check would get, without applying any.


macOS

1. Download the ruleset and the guide

Resource Link
Ruleset (YAML) ruleset.yaml
Hardening Guide (PDF/MD) Hardening_Guide-macOS_26_Tahoe_1.0.md

2. Build the binary

git clone https://github.com/mev0lent/hardener.git
cd hardener

# Apple Silicon
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o hardener-macos .

# Intel
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -o hardener-macos .

3. Audit your system

# Interactive suite picker
./hardener-macos audit --ruleset ruleset.yaml

# Run all suites non-interactively
./hardener-macos audit --ruleset ruleset.yaml --all

# Include high-security checks (default: baseline only)
./hardener-macos audit --ruleset ruleset.yaml --all --security-level high

4. Apply fixes

./hardener-macos fix --ruleset ruleset.yaml --all

5. Roll back

./hardener-macos rollback --latest

Verifying a Release

#verifying-a-release

Every tagged release publishes a SHA-256 checksum alongside the built binary. Before running Hardener under elevated privileges, verify that your local binary matches the released one:

Linux / macOS:

sha256sum -c hardener.sha256
# expected output: hardener: OK

Windows (PowerShell):

(Get-FileHash .\hardener -Algorithm SHA256).Hash.ToLower() `
  -eq (Get-Content .\hardener.sha256).Split(' ')[0]
# expected output: True

If the check fails, the binary in your working directory does not match the tagged release; do not run it with elevated privileges. Report the discrepancy in an issue.

Release assets (binary and hardener.sha256) are attached to each entry under the Releases page.


Live terminal dashboard

Add --tui to audit or fix for live category scores, progress, and scrolling check results with skip reasons and error details.

./hardener audit --ruleset ruleset.yaml --all --tui
./hardener fix --ruleset ruleset.yaml --all --tui

# Preview using simulated checks
./hardener tui-demo

# Browse saved audit/fix results without executing commands
./hardener view reports/example.json

# Open the report path prompt
./hardener view

Use your binary's filename, such as hardener-linux or hardener-macos.

Scores are red below 35%, orange at 35–64%, and green at 65% or above. Skipped and manual checks are excluded; successful fixes require another audit to count as verified passes.

Press o after a run finishes to open a saved report. Saved reports reconstruct results, not the original timed activity stream.

See TUI.md for keyboard controls, scoring, and terminal requirements.

Command Reference

Command Flag Short Description
audit / fix --ruleset <file> -r Load checks from a ruleset YAML file
audit / fix --path <dir> -p Load checks from a hardening-guide directory (alternative to --ruleset)
audit / fix --all -A Skip interactive suite picker, run everything
audit / fix --security-level <level> -s baseline (default), medium, high, or optional — runs checks at or below this level
audit / fix --profile <name> -P Role profile for distro overrides (e.g. server, client)
audit / fix --label <l1,l2> -l Run only suites whose labels: list contains at least one match
fix --dry-run -d Audit and list the fix each failing check would get; nothing is applied
rollback --latest Roll back to the most recent fix run
rollback --timestamp <ts> -t Roll back to a specific run, by its UTC timestamp in runs.json (e.g. 2026-05-18T14:32:00Z)
rollback --files <f1,f2> Roll back specific files only
audit / fix --tui Show the live terminal dashboard
tui-demo Preview the dashboard with simulated checks
view [report.json] Browse saved results; omit the path to open a prompt

--ruleset and --path are mutually exclusive; exactly one must be provided for audit and fix.


Ruleset Features

Security levels

Every check has a security_level field: baseline, medium, high, or optional, in that order. Only checks at or below the level requested via --security-level are executed; the rest are reported as skipped. Default is baseline.

optional marks controls a hardening guide calls optional: recommended, but they may disable functionality a system needs. They are never included by high; --security-level optional runs every check, optional ones included.

Levels are case-insensitive. An unknown --security-level is rejected, and checks whose security_level is missing or unknown are listed in a warning before the run, since they never run at any level.

Platform scope (os, arch)

A suite's os: and arch: decide whether it runs on the current system; suites for another OS or architecture are skipped when the ruleset loads. A check can narrow this with its own os: and arch: lists, and is then reported as distro-skipped elsewhere. amd64/x86_64 and arm64/aarch64 are treated as the same architecture.

Distro-specific overrides

Checks can carry a distro: map to override command, fix, expected, sudo, or fix_sudo for a specific Linux distribution or distro family. The key is either the lowercase ID= value from /etc/os-release (e.g. ubuntu, rocky, opensuse-leap, arch) or a family name derived from ID_LIKE= (e.g. debian, rhel, suse, arch, fedora):

- id: secure-boot-active
  command: mokutil --sb-state | grep -c enabled
  distro:
    ubuntu:
      command: |
        if ! command -v mokutil >/dev/null 2>&1; then echo 1
        else mokutil --sb-state | grep -c enabled; fi
    debian:   # applies to any debian-family distro (Ubuntu, Kali, Mint, ...) as fallback
      command: |
        command -v mokutil >/dev/null 2>&1 && mokutil --sb-state | grep -c enabled || echo 1

Lookup order (most specific → least):

  1. <distro>-<profile> (e.g. ubuntu-server)
  2. <family>-<profile> for each family in the chain (e.g. debian-server, rhel-server)
  3. <distro> (e.g. ubuntu)
  4. <family> for each family in the chain (e.g. debian, rhel, suse, arch)

Family chains are derived from ID + ID_LIKE in /etc/os-release, with a small hand-maintained fallback for distros that ship without ID_LIKE:

Distro Chain
Ubuntu, Kali, Mint, Pop!_OS <id>, debian
Rocky, AlmaLinux, CentOS, Oracle <id>, rhel, fedora
RHEL rhel, fedora
Fedora fedora, rhel
openSUSE Leap/Tumbleweed, SLES <id>, suse
Manjaro, EndeavourOS, Artix <id>, arch

If a check has a distro: map but no key in the chain matches, the check is skipped with a skipped_distro state — it does not count as a failure.

Profiles (--profile)

For role-based differences (server vs. client), pass a profile at runtime:

./hardener-linux audit --ruleset ruleset.yaml --profile server

The engine tries the combined key distro-profile (e.g. debian-server) first, then falls back to distro alone, then runs the universal check. This lets you express distro+role overrides without duplicating entries:

distro:
  debian-server:
    command: systemctl is-active sshd | grep -c active
  debian:
    command: command -v sshd >/dev/null 2>&1 && echo 0 || echo 0

Labels (--label)

Each suite in a ruleset has a labels: list. Use --label to run only matching suites:

# Run only kernel and network suites
./hardener-linux audit --ruleset ruleset.yaml --label kernel,network

Available labels in the Linux ruleset: auth, network, kernel, filesystem, boot, services, audit, logging, mac.

The bundled Linux ruleset ships 48 suites / ~131 checks covering: administrative accounts and PAM stack, password policy, faillock and account aging, sudo hardening, screen/session lock, service hardening (Avahi, Bluetooth, CUPS, Samba), firewall, SSH, kernel and network sysctls, IPv6, DNS resolver, /etc/hosts sanity, chrony, boot (GRUB/Secure Boot/BIOS), UEFI mode, kernel command line, package integrity (rpm/deb/pacman), single-user mode, USBGuard, PATH and umask, mount options, /proc hardening, SUID/SGID/orphan audits, polyinstantiated tmp, auditd rules, journald hardening, syslog, cron/at whitelist, systemd service sandboxing, and MAC (SELinux + AppArmor) enforcement.

requires_command

A check with requires_command: <binary> is silently skipped with a missing-command state when that binary is not in the system's PATH. This keeps checks that depend on optional tools from showing as errors on systems where the tool isn't installed:

- id: secure-boot-active
  requires_command: mokutil
  command: mokutil --sb-state | grep -c enabled
  expected: 1

Missing-command skips appear in suite summaries and are tracked separately from failures.

requires_file

A check with requires_file: <path> is skipped with a missing-command state when the file does not exist. Useful for checks against distro-specific files that some distros ship without (e.g. /etc/hosts.allow, /etc/login.defs, /etc/pam.d/common-password):

- id: hosts-deny-permissions
  requires_file: /etc/hosts.deny
  command: stat -c '%a' /etc/hosts.deny
  expected: 644

A permission error on stat is treated as "file exists but not readable" and does not skip the check — only a genuine "no such file" does.

PATH handling

The runner prepends /usr/local/sbin, /usr/sbin, and /sbin to PATH before executing checks and fixes. This makes sysctl, firewall-cmd, iptables, auditctl, and similar tools discoverable even on distros (openSUSE, some minimal images) that do not include sbin dirs in a regular user's login PATH.

expected_op

By default the check compares command output against expected as a string. Set expected_op to use a numeric comparison instead:

expected: 1
expected_op: '>='

Supported operators: >=, >, <=, <.

Reading kernel state

Kernel sysctl checks in the bundled ruleset use cat /proc/sys/<key> rather than sysctl -n <key>. This has two upsides: (a) it works without sudo because /proc/sys entries are world-readable, and (b) it sidesteps distros (notably openSUSE Leap) whose sudoers secure_path omits /usr/sbin and would otherwise return exit code 127. If you author a new sysctl-style check, prefer the /proc/sys path for the same reasons.

Ruleset authoring tips

  • Prefer runtime file detection over a distro: map when the only thing that changes across distros is a filepath. Example — instead of duplicating the check three times for /etc/pam.d/common-password (Debian) vs /etc/pam.d/system-auth (RHEL/Arch), write:
    command: |
      f=/etc/pam.d/system-auth; [ -f "$f" ] || f=/etc/pam.d/common-password
      grep -cE '^\s*password\s+required\s+pam_pwquality.so' "$f"
  • Use requires_command for optional binaries (gsettings, auditctl, mokutil) and requires_file for optional files (/etc/hosts.allow, /etc/pam.d/common-auth). Both produce a neutral "missing-command" state, not a failure.
  • For "should not be enabled" service checks, pass unless systemctl is-enabled returns exactly enabled — this treats disabled, masked, static, indirect, and missing units all as compliant.
  • A command that exits non-zero is reported as an error, not a failure. The exception is a counting grep (grep -c, -ic, -Eci, -v -c, --count, also egrep/fgrep) that prints 0: grep exits 1 when nothing matches, and the 0 is compared against expected like any other output. For other tests, end the command with && echo 1 || echo 0 rather than relying on the exit code.

Cross-Distro Test Harness (contributor use)

Automated VM-based testing across Linux distributions. Each distro gets a fresh KVM VM, runs the full audit → fix → rollback cycle, and produces structured results.

Linux bare-metal host required. KVM/libvirt needs hardware virtualisation (VT-x or AMD-V) available directly on the CPU. This test harness does not work on macOS, Windows, or inside a VM (VMware/VirtualBox/WSL2).

Vagrant / KVM

1. Install KVM and libvirt

sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients virt-manager
sudo systemctl enable --now libvirtd
sudo usermod -aG libvirt,kvm $USER
# log out and back in for group membership to take effect

Verify:

virt-host-validate

IOMMU and secure-guest WARN lines are fine. A FAIL on the KVM line means hardware virtualisation is not available; on bare metal, enable VT-x/AMD-V in BIOS/UEFI; inside a VM this setup cannot be used, use Docker instead.

2. Install Vagrant

The key at HashiCorp's /gpg URL does not always match the key currently signing the repo. Fetch it by fingerprint from a keyserver instead, which is always authoritative:

sudo gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys AA16FCBCA621E701
sudo gpg --export AA16FCBCA621E701 \
  | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg > /dev/null

echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
  https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" \
  | sudo tee /etc/apt/sources.list.d/hashicorp.list

sudo apt update && sudo apt install vagrant
vagrant --version   # should print 2.4+

Kali Linux: lsb_release -cs returns kali-rolling which has no entry in the HashiCorp repo. Replace the codename part of the echo line with a hardcoded bookworm (Kali's Debian base):

echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
  https://apt.releases.hashicorp.com bookworm main" \
  | sudo tee /etc/apt/sources.list.d/hashicorp.list

3. Install the vagrant-libvirt plugin

sudo apt install libvirt-dev ruby-dev build-essential
vagrant plugin install vagrant-libvirt
vagrant plugin list   # should list vagrant-libvirt

4. Run the tests

Build the binary from the repo root (Go 1.24+ required, see Prerequisites above):

GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o hardener-linux .

Then from the testing/ directory:

cd testing
chmod +x run_tests.sh

With a markdown guide directory:

./run_tests.sh \
  --guide   path/to/linux-guide/sections \
  --binary  ../hardener-linux \
  --distros "ubuntu debian rocky opensuse archlinux"

With a standalone ruleset YAML (--binary is required in this mode):

./run_tests.sh \
  --ruleset ../ruleset.yaml \
  --binary  ../hardener-linux \
  --distros "ubuntu"
Flag Default Description
--guide Path to a hardening-guide directory of .md files
--ruleset Path to a standalone ruleset.yaml (alternative to --guide)
--binary (required with --ruleset) Path to the pre-built hardener binary
--level baseline Security level: baseline, medium, high, or optional
--distros all Space-separated list of distros to run
--profile Passed as --profile to hardener (e.g. server, client)
--label Passed as --label to hardener (e.g. kernel,network)

--guide and --ruleset are mutually exclusive; one must be provided.

What the runner does

For each distro:

  1. Boots a fresh KVM VM via Vagrant
  2. Installs distro-specific prerequisites inside the VM
  3. Rsyncs the guide and binary into the VM at /opt/hardener/
  4. Runs the 5-step test cycle: initial audit → fix → post-fix audit → rollback → post-rollback audit
  5. Pulls structured results out of the VM over SSH
  6. Destroys the VM

Artefacts

Every run creates a timestamped directory under testing/results/:

results/
└── <TIMESTAMP>/
    ├── summary.txt          # cross-distro table
    ├── ubuntu.json          # per-suite results for all 5 steps
    ├── ubuntu.log           # verbose hardener output
    ├── ubuntu-vagrant.log   # raw vagrant up / provisioning output
    └── ...

Summary table columns:

Column Meaning
STATE OK = results extracted · SKIP = VM failed or no results
A1_P / A1_F / A1_E Initial audit: passed / failed / errors
A1_S Skipped: check above the requested security level (neutral)
A1_DS Distro-skipped: check has a distro: map but current distro not listed (neutral)
A1_MC Missing-command: requires_command binary not found on this distro (neutral)
A1_T Total checks in suite
FX_OK / FX_F Fixes verified by re-check / still failing after the fix run, including fixes that ran but were not effective
A3_* Post-rollback audit: same columns as A1

A healthy run has low A1_F and A1_E. A1_S, A1_DS, and A1_MC are all neutral — they are expected to vary by distro and security level and do not indicate broken checks.

After the table, the runner prints a missing commands per distro section listing the exact binary names that triggered A1_MC on each distro. Use this to decide whether to add a package to the Vagrantfile's pkg_cmd or to add a requires_command: field to the relevant checks.

Supported distros

Distro Vagrant box Firewall
ubuntu bento/ubuntu-24.04 ufw
debian generic/debian12 ufw
rocky bento/rockylinux-9 firewalld
opensuse opensuse/Leap-15.6.x86_64 firewalld
archlinux generic/arch ufw
fedora bento/fedora-latest firewalld
rhel generic/rhel9 (requires subscription) firewalld

Troubleshooting

Distro shows SKIP: open <distro>-vagrant.log. Common causes: box has no libvirt provider variant; package install failed during provisioning.

All results are zero: the binary failed system validation. Open <distro>.log and look for [> ERROR] lines. Usually a required tool from preconditions is missing on that distro.

High A1_MC: many checks are skipped due to missing binaries. Check the "Missing commands per distro" section of summary.txt for the exact binary names. Either add the package to the Vagrantfile pkg_cmd for that distro, or add requires_command: to the affected checks so the skip is intentional and documented.

vagrant plugin install fails: install dev libraries first:

sudo apt install libvirt-dev ruby-dev build-essential

Adding a distro

  1. Add an entry to the DISTROS hash in testing/Vagrantfile with a libvirt-compatible box and its prereq install command
  2. Run: ./run_tests.sh --guide path/to/sections --binary ../hardener-linux --distros "newdistro"
  3. Check the vagrant log if it shows SKIP, the hardener log if results are all zero

About

A cross-platform security auditing and hardening tool. Load a ruleset, run an audit, apply fixes, and roll back if needed, all from a single binary with no external dependencies.

Resources

Security policy

Stars

11 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages