I Built a Script That Hardens a Linux Server Automatically — Here’s How
When you spin up a fresh Ubuntu server, it comes insecure by default. Root login is enabled. SSH accepts passwords. There is no firewall…
I Built a Script That Hardens a Linux Server Automatically — Here’s How
When you spin up a fresh Ubuntu server, it comes insecure by default. Root login is enabled. SSH accepts passwords. There is no firewall. Nothing is watching for brute force attacks. Most people don’t think about this until something goes wrong. I decided to do something about it before that happens — so I built a bash script that automates the entire hardening process in one command.
Here’s how I built it, phase by phase.
The Problem With Default Servers
A default Ubuntu install is built for accessibility, not security. The goal is to get you up and running quickly. But that means several dangerous settings are left open out of the box.
If you put that server on the internet without hardening it, you’re exposed. Bots are constantly scanning for open SSH ports and trying common passwords. It’s not a matter of if — it’s when.
I wanted a script that takes a fresh server from zero to hardened automatically, the same way a sysadmin would configure it manually before pushing anything to production. And I wanted it to log every single action it took, with timestamps.
How I Structured It
I built the script in six phases so each concern is isolated and easy to follow:
- Phase 1 — Foundation: root check, logging, color-coded output
- Phase 2 — System update and package installation
- Phase 3 — SSH hardening
- Phase 4 — Firewall configuration with UFW
- Phase 5 — Fail2Ban and automatic security updates
- Phase 6 — Final summary report
Every phase announces what it’s doing, confirms when it’s done, and writes a timestamped entry to a log file at /var/log/hardening_report.log. If anything fails, the script exits immediately rather than continuing halfway through.
Phase 1 — Foundation
Before touching anything on the server, the script needs to know it has the right to be running. The very first thing it does is check if it’s being run as root:
if [[ $EUID -ne 0 ]]; then
echo "[ERROR] This script must be run as root."
exit 1
fi
$EUID is the effective user ID. If it's not 0 — which is root — the script exits immediately. No partial changes, no half-configured server.
I also set up a log() function early so every action prints to the terminal and writes to the log file at the same time:
log() {
local MESSAGE="$1"
local TIMESTAMP
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo -e "$MESSAGE"
echo "[$TIMESTAMP] $MESSAGE" >> "$LOG_FILE"
}
This means by the time the script finishes, there’s a full audit trail of everything that happened.
Phase 2 — System Update
There’s no point hardening a server running outdated packages. Vulnerabilities in old software bypass everything else you configure. So before anything else, the script updates the system and installs the three tools it needs for later phases — ufw, fail2ban, and unattended-upgrades.
I used a loop with a check so it skips packages that are already installed instead of trying to reinstall them:
for PACKAGE in "${PACKAGES[@]}"; do
if dpkg -l | grep -q "^ii $PACKAGE"; then
log "[SKIP] $PACKAGE is already installed."
else
apt install -y "$PACKAGE"
fi
done
This makes the script idempotent — you can run it multiple times on the same server and it won’t break anything.
Phase 3 — SSH Hardening
This is the most critical phase. SSH is the main entry point into any Linux server, and by default it’s too permissive.
The first thing I do is back up the original config before touching it:
cp "$SSHD_CONFIG" "$SSHD_CONFIG.bak.$(date +%F)"
Always back up before editing system config files. If something breaks, one command restores it.
Then I use sed to apply four changes to /etc/ssh/sshd_config:
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' "$SSHD_CONFIG"
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' "$SSHD_CONFIG"
sed -i 's/^#*PermitEmptyPasswords.*/PermitEmptyPasswords no/' "$SSHD_CONFIG"
The ^#* in each pattern matches the line whether it's commented out or not — so it works regardless of the state of the config file.
For MaxAuthTries, initially, I ran into an error, and the fix was simpler than expected. The line existed in the config but I had accidentally written MAxAuthTRIES in my sed command, which was the wrong casing. Since SSH config values are case sensitive, MaxAuthTRIES 3 meant nothing to the service. A quick sed to correct the casing fixed it:
sudo sed -i 's/MaxAuthTRIES/MaxAuthTries/' /etc/ssh/sshd_config
A good reminder that sed won’t warn you when it writes the wrong value — it just writes it. Always verify with grep after.
Before restarting SSH I validate the config first:
sshd -t
If that fails, the script restores the backup automatically and exits. This prevents the worst case scenario — a broken SSH config that locks you out of the server.
Phase 4 — Firewall With UFW
UFW makes firewall configuration straightforward. The logic here is simple: deny everything incoming by default, then only open what you actually need.
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
The order matters. I allow SSH before enabling the firewall. If you enable UFW first and forget to allow SSH, you lock yourself out immediately. The --force flag skips the interactive prompt so the script doesn't hang waiting for input.
UFW automatically applies rules to both IPv4 and IPv6, so every port I opened is covered on both.
Phase 5 — Fail2Ban and Auto Updates
Even with SSH hardened, an attacker can still sit there and try passwords repeatedly. Fail2Ban watches the auth logs and bans any IP that fails to authenticate too many times.
I configured it by writing directly to /etc/fail2ban/jail.local — never jail.conf, which gets overwritten on updates:
cat > /etc/fail2ban/jail.local << EOF
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s
EOF
This bans any IP for one hour after three failed SSH attempts within ten minutes.
For automatic security updates, I configured unattended-upgrades to apply security patches daily without manual intervention:
cat > /etc/apt/apt.conf.d/20auto-upgrades << EOF
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
EOF
Phase 6 — Final Report
When everything is done, the script prints a clean summary of every change it made and the current status of each service:
============================================
HARDENING SUMMARY REPORT
============================================
Hostname : prod-server-01
Date : Wed May 20 14:28:54 UTC 2026
SSH Configuration:
Root login : disabled
Password auth : disabled
Empty passwords : disabled
Max auth tries : 3
Firewall (UFW):
Status : active
Allowed ports : 22 (SSH), 80 (HTTP), 443 (HTTPS)
Services:
SSH : active
Fail2Ban : active
Auto updates : active
The full log at /var/log/hardening_report.log contains a timestamped record of every action taken from start to finish.
What I Learned Building This
A few things stood out to me during this build.
sed is silent when it finds nothing to replace. It won't throw an error — it just moves on. That means you can run a command, think it worked, and have nothing actually change. Always verify config changes landed with grep after running sed.
Backup before you edit. Every time. One cp command before touching a system config file is the difference between a five-second restore and a broken server.
Order matters with firewalls. Allowing SSH after enabling UFW is too late. The sequence has to be: set rules first, enable second. Getting that backwards on a real server means losing access completely.
And sshd -t is your safety net. Always validate SSH config before restarting the service. The script does this automatically, but it's a habit worth building manually too.
The Full Script
The complete script is available on my GitHub: github.com/cjemmyyy/linux-server-hardening
It’s built for Ubuntu 20.04, 22.04, and 24.04. Clone it, read through it, and run it on a VM before using it anywhere real like for your AWS instance. And make sure your SSH key is set up before you do — because after Phase 3, passwords won’t work anymore.
메타데이터
- post_id
- 2b50a1d0e65a
- slug
- i-built-a-script-that-hardens-a-linux-server-automatically-heres-how-2b50a1d0e65a
- url
- https://medium.com/@cjemmyyy_33657/i-built-a-script-that-hardens-a-linux-server-automatically-heres-how-2b50a1d0e65a
- canonical_url
- https://medium.com/@cjemmyyy_33657/i-built-a-script-that-hardens-a-linux-server-automatically-heres-how-2b50a1d0e65a
- author_url
- https://medium.com/@cjemmyyy_33657
- status
- ok
- fetched_at
- 2026-06-09 15:37:30