Server Initialization Script
A fresh server is not ready to use out of the box. By default root can log in with a password, the firewall is off, and system packages are frozen at the image's factory version — any of these is an open door for brute force. Turning every initialization step into a script isn't just about saving time: you can run the same configuration on new servers and environments repeatedly, with predictable, auditable results and no skipped steps. The script below targets Ubuntu 22.04 / 24.04 and covers security hardening, user creation, the firewall, and essential software.
Reference: Ubuntu security hardening docs https://ubuntu.com/server/docs/security-hardening
One-Command Init Script
#!/bin/bash
# server-init.sh — Fresh server initialization
# Ubuntu 22.04 / 24.04
set -euo pipefail
# === Configuration variables ===
NEW_USER="deploy"
SSH_PORT="22" # Change to a non-standard port for security
TIMEZONE="UTC"
# === 1. System update ===
echo ">>> Updating system packages..."
apt update && apt upgrade -y
apt autoremove -y
# === 2. Create deploy user ===
echo ">>> Creating deploy user..."
if ! id "$NEW_USER" &>/dev/null; then
useradd -m -s /bin/bash "$NEW_USER"
usermod -aG sudo "$NEW_USER"
fi
# === 3. Configure SSH key login ===
echo ">>> Configuring SSH keys..."
mkdir -p /home/$NEW_USER/.ssh
cp ~/.ssh/authorized_keys /home/$NEW_USER/.ssh/ 2>/dev/null || true
chown -R $NEW_USER:$NEW_USER /home/$NEW_USER/.ssh
chmod 700 /home/$NEW_USER/.ssh
chmod 600 /home/$NEW_USER/.ssh/authorized_keys
# === 4. Harden SSH ===
echo ">>> Hardening SSH configuration..."
sed -i 's/#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/#Port 22/Port '"$SSH_PORT"'/' /etc/ssh/sshd_config
systemctl restart sshd
# === 5. Configure firewall ===
echo ">>> Configuring firewall..."
ufw allow $SSH_PORT/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
# === 6. Configure timezone and NTP ===
echo ">>> Configuring timezone..."
timedatectl set-timezone $TIMEZONE
apt install -y chrony
systemctl enable --now chrony
# === 7. Install essentials ===
echo ">>> Installing essential packages..."
apt install -y curl wget git ufw fail2ban htop nginx docker.io docker-compose-plugin certbot python3-certbot-nginx
# === 8. Configure fail2ban ===
echo ">>> Configuring fail2ban..."
cat > /etc/fail2ban/jail.local << 'EOF'
[sshd]
enabled = true
port = $SSH_PORT
maxretry = 3
bantime = 3600
EOF
systemctl enable --now fail2ban
# === 9. Configure swap ===
echo ">>> Configuring swap..."
if [ ! -f /swapfile ]; then
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
fi
# === 10. Done ===
echo ""
echo "=== Server initialization complete! ==="
echo "Created user: $NEW_USER"
echo "SSH port: $SSH_PORT"
echo "Please logout and test SSH with new user before closing this session."
set -euo pipefail at the top stops the script the moment any step fails, so it never keeps running with a half-configured system. The variables at the top (user, SSH port, timezone) mean that moving to a new machine only requires editing a few lines.
What Each Step Does
| Step | Purpose | Details |
|---|---|---|
| System update | apt update & upgrade | Latest system packages |
| User creation | Create deploy user | No direct root login |
| SSH keys | Copy + set permissions | Key-only auth |
| SSH hardening | No root, no passwords | Key authentication only |
| Firewall | UFW | Only SSH/HTTP/HTTPS |
| Timezone & NTP | chrony sync | Consistent logs, valid certs |
| Fail2ban | Brute force protection | 3 failures = 1-hour ban |
| Swap | Memory swap | 2GB swap prevents OOM |
A few details are easy to miss: without chmod 700 and chmod 600, OpenSSH silently refuses to use the key file; mismatched timezones across servers make log correlation painful; and on machines with under 2GB of RAM, swap is almost a necessity — otherwise the OOM killer may take down your database process.
How to Use
scp server-init.sh root@your-server:~/
ssh root@your-server
chmod +x server-init.sh && sudo ./server-init.sh
After the script finishes, open a brand-new SSH session and log in as deploy first to confirm everything works before closing the root session — this is the last line of defense against locking yourself out. If key login fails, roll back while the root session is still open.
A quick self-check after the run: ss -tlnp | grep -E ':(22|80|443)' shows listening ports, ufw status confirms firewall rules, and fail2ban-client status sshd verifies brute-force protection is active.
A Real-World Scenario
When setting up a WordPress site cluster for a client, three 2C4G cloud VPS instances were initialized with the same script, and all were hardened in under fifteen minutes from purchase. All subsequent operations used the deploy user with keys, and the public SSH port was moved to 22022. Three months later, the fail2ban logs showed 40+ blocked brute-force attempts against SSH — any one of those succeeding would have meant a fully compromised box.
FAQ
- Does this run on Debian too? Most commands carry over, but package names can differ — run the whole thing on a test box before production.
- What if SSH login fails afterward? Keep the current root session open, inspect
sshd_configfor the port and password settings, and retry after fixing. - Is
set -euo pipefailtoo aggressive? During initialization it's better to fail than to keep running half-configured; if a command must be allowed to fail, append|| trueexplicitly. - What if the firewall cuts me off? UFW denies unlisted ports by default, so allow your SSH port before enabling it — otherwise you may lock yourself out.
Security Tips
- Change SSH to a non-standard port (e.g., 2222); combined with fail2ban it filters out most automated scans
- Use your own SSH keys, not server-generated ones
- Enable unattended-upgrades for automatic security patches
- Monitor logs regularly with
journalctl -xeandtail -f /var/log/auth.log - Be careful granting passwordless sudo in
visudo— scope it narrowly rather than allowing every command - Back up key configs (
/etc/ssh/sshd_config) after init so you can diff or roll back later
16IDC Takeaway
Security hardening has no "good enough." A compromised server can become a springboard for attacking downstream users. An init script turns the security baseline into a repeatable default action — run it on every new server, and you stop reinventing the same settings by hand. That's configuration as code: a script validated once can be rolled out to every similar machine, keeping the security baseline consistent across the fleet. A clean, properly hardened server is the foundation every downstream service relies on.