← Back to list

How I Built a Linux Security Auditing Tool in Python: Auto-Hardener

Imagine you’re a system administrator and you log in one morning to find a user account called “attacker” or a random user account you…

jAy-JaY · 2026-06-10 06:33 · 0 claps · 5.5 min read
#cybersecurity #python #linux #automation #learning
Open on Medium ↗
Wiki topics: EDU · Education & Learning 🔒 · Cybersecurity 🔓 · Open Source

How I Built a Linux Security Auditing Tool in Python: Auto-Hardener

Imagine you’re a system administrator and you log in one morning to find a user account called “attacker” or a random user account you didn’t create with full root privileges on your server. The compromise happened simply because someone created an account with UID 0, the user ID Linux strictly reserves for root, and it went unnoticed. That scenario is exactly what one of the checks in my tool, Auto-Hardener, is designed to catch. And building it taught me far more about Linux security and python than I expected.

What Is Auto-Hardener?

Auto-Hardener is a Python-based security auditing and hardening tool for Linux systems. It scans for common security misconfigurations, scores the system’s security posture from 0 to 100, applies fixes automatically where needed, and logs every action for review.

I am building it as a learning project. I had just started learning Python and wanted to build something real, something that solves an actual problem. A security tool felt like the right domain for me as I am a cybersecurity beginner.

The tool checks four things:

  • Whether the UFW firewall is active
  • Whether SSH root login is disabled
  • Whether /etc/shadow; the file that stores password hashes has safe permissions
  • Whether any rogue accounts with root-level access exist on the system

Each finding carries a weight that shows how severe or critical it is. A disabled firewall deducts 20 points. An enabled SSH root login deducts 30. A world-writable shadow file deducts 40. A rogue root account deducts 50 and regardless of the overall score, a rogue root account always forces the risk level to HIGH. Because an unexpected UID 0 account may indicate unauthorized privilege escalation or persistence.

The Architecture: Four Files, One Responsibility Each

One of the most important decisions I made was splitting the tool into four separate files instead of writing everything in one script. Each file has a single responsibility, a principle in software development called separation of concerns.

Here’s how it breaks down:

  • checks.py — scans the system for misconfigurations and returns findings
  • hardener.py — applies fixes for detected vulnerabilities
  • main.py — orchestrates everything: runs the scan, evaluates results, presents them to the user, and calls the hardener where needed
  • logger.py — records every audit action to a persistent log file

The practical benefit of this structure became clear when I was implementing logging. I had to decide whether main.py handles all logging, or should hardener.py handle it too? My conclusion was both but each file should only log what it directly knows about. main.py logs scan findings and risk levels, hardener.py logs fix results because that’s where the commands actually run and outcomes are determined.

If everything had been in one file, making that distinction would have been rather difficult.

The Bug That Taught Me the Most

I think every project has a moment where something breaks in a way you didn’t expect. Mine was the scoring system.

I had assigned deduction weights to each finding: 20, 30, 40, and 50 points respectively. And it seemed straight forward enough. But when I ran the tool with all four misconfigurations present simultaneously, the score displayed as -40/100.

Negative security scores don’t make sense. A system can’t be less than zero percent secure.

My first fix was adding a score floor:

score = max(0, score)

This prevented negative numbers; max(0, score) returns whichever value is larger, so the score can never drop below zero. But that only solved the display problem. It didn’t really solve the deeper issue: the risk level logic was still broken.

With all four findings, the score floors at 0 and should clearly be HIGH risk. But what about a rogue root account detected alone? That deducts 50 points, leaving a score of 50, which my original logic classified as MEDIUM. A confirmed backdoor account being labelled MEDIUM risk is wrong.

The real fix was making the risk level consider what was found and not just the number:

if score == 100:
print("Risk Level: LOW")
sys.exit(0)
elif not is_root_safe:
print("Risk Level: HIGH")
elif score >= 60:
print("Risk Level: MEDIUM")
else:
print("Risk Level: HIGH")

Now a rogue root account always forces HIGH regardless of everything else. The score reflects the cumulative severity of findings. The risk level reflects the nature of what was found. Both tell an accurate story.

Execution flow of the script detecting local vulnerabilities, deleting a rogue account, and automatically applying remediations.

Execution flow of the script detecting local vulnerabilities, deleting a rogue account, and automatically applying remediations.

The Security Decisions Behind the Code

Building a security tool forces you to think about security at every step including the security of the tool itself.

Running as root. The tool requires root privileges to scan and modify system files. That’s a significant level of trust. To make sure it’s intentional and not accidental, the very first thing the tool does is verify it’s being run as root:

if os.geteuid() != 0:
print("[-] This script must be run as root.")
sys.exit(1)

os.geteuid() returns the effective user ID of the process. If it’s not 0 which is the root’s ID the tool exits immediately with a clear message.

Human-in-the-loop for destructive actions. Deleting a user account is irreversible. The tool can detect a rogue root account and recommend deletion, but it will never delete a user without explicit human confirmation:

print(f"[!] WARNING: You are about to DELETE user '{rogue_user}'.")
print("This cannot be undone.")
confirm = input("Are you sure you want to proceed? (y/n): ")

Automation is powerful and unchecked or unverified automation is dangerous.

Avoiding shell injection. An earlier version of the tool used shell=True in subprocess calls a pattern that passes commands directly to the system shell as a string. This creates shell injection vulnerabilities where malicious input could execute arbitrary commands. The fix was switching to list-based subprocess calls where each argument is passed separately, and Python never interprets input as executable code.

Logging: The Feature I Think I Should Have Built First

I added logging last. If I started over, I would build it first.

In security, an audit trail shouldn’t be optional because it’s the evidence. Every scan finding, every fix applied, every action the tool takes is now written to a file named autohardener.log with a timestamp and severity level:

2026–05–13 10:31:56 - INFO - Firewall: ACTIVE
2026–05–13 10:31:56 - WARNING - SSH Root Login: ENABLED
2026–05–13 10:31:56 - CRITICAL - Rogue Root Account Detected: "attacker"
2026–05–13 10:31:56 - INFO - FIX APPLIED: Enable Firewall (UFW)

The log file rotates automatically at 1MB, keeping three backups. This means the tool never fills a disk and always maintains a history.

The severity levels; INFO, WARNING, ERROR, CRITICAL aren’t random. They map directly to the CIA triad thinking behind each check. A world-writable shadow file is CRITICAL because it violates both confidentiality (password hashes exposed) and integrity (hashes can be modified in this context). A disabled firewall is WARNING because it’s a misconfiguration that increases exposure without confirming compromise.

What I Would Do Differently

Three things:

Implement logging from day one. Retrofitting logging into existing code is harder than building with it from the start.

Write cleaner error handling earlier. My first version had functions that silently returned “safe” when they encountered errors, which means the tool would give a false all-clear if something went wrong. That’s the worst possible behavior for a security tool. I think for my tool error visibility should be prioritized over containment.

Test every combination of findings. I caught the negative score bug only because I manually ran the tool with all four misconfigurations present. A more systematic approach to testing would have caught it earlier.

What’s Next

Auto-Hardener currently audits four controls. CIS benchmarks for Linux cover over 200. The roadmap includes password policy enforcement, checking for unauthorized open ports, sudo configuration auditing, and more.

The tool is open source and available on GitHub. If you’re learning Python and want to build something real, security tooling is one of the most rewarding domains to work in because you learn a lot while at it. Every line of code you write has a direct connection to protecting real systems.

Built by jAy-JaY — currently learning, always building. GitHub: Auto-Hardener


메타데이터
post_id
6be3c836774a
slug
how-i-built-a-linux-security-auditing-tool-in-python-auto-hardener-6be3c836774a
url
https://medium.com/@jay-jay/how-i-built-a-linux-security-auditing-tool-in-python-auto-hardener-6be3c836774a
canonical_url
https://medium.com/@jay-jay/how-i-built-a-linux-security-auditing-tool-in-python-auto-hardener-6be3c836774a
author_url
https://medium.com/@jay-jay
status
ok
fetched_at
2026-06-10 18:44:10