Shore Up
A freshly built cabin having a deadbolt fitted to its door, bars set into its windows, and a gate installed at the fence, while a watchman at the gate checks a visitor's ID card.
LinuxSecurity

Harden a Fresh Ubuntu Server with a First-Boot Bash Script

Ketan Aagja10 min read
No ratings yet

Before you run this

This script applies a standard first-boot baseline to a fresh Ubuntu server: it creates a non-root sudo user with your SSH key, turns on the UFW firewall (allowing only SSH), disables direct root login and SSH password authentication, enables automatic security updates, and installs fail2ban to throttle SSH brute-forcing. Its purpose is to take a default cloud or VM image from "wide open with a root password" to a sane, keys-only baseline in one pass.

It needs root. Run it as root (that's the norm during cloud-init / first login) or with sudo. It uses apt, writes to /etc/ssh/sshd_config.d/, /etc/apt/apt.conf.d/, and /etc/fail2ban/, and restarts services — all of which require elevation.

Read it first, and test it on a throwaway VM before you point it at anything you care about. The most important reason: this script disables SSH password login and root login. If your public key is wrong, mis-pasted, or missing, and you have no console access, you will lock yourself out — that part is effectively irreversible without out-of-band console/rescue access. Before you reboot or close your first session, always open a second SSH session as the new user and confirm it works. Keep your provider's web console or a rescue shell within reach.

What it changes that you should be conscious of: it creates a new user account, edits SSH policy (root login and password auth), enables a firewall that will drop everything except SSH, and installs two packages. None of it deletes data, but the firewall and SSH changes will cut off any service or login method you haven't explicitly allowed. Set SSH_PORT and the allowed rules to match your environment before running.

I'm assuming Ubuntu 22.04 or 24.04 LTS, systemd, and a default OpenSSH install where /etc/ssh/sshd_config contains the stock Include /etc/ssh/sshd_config.d/*.conf line (both releases ship this). One caveat for 24.04: SSH is socket-activated, so if you change the port, the port is set by ssh.socket, not sshd_config — I keep the default port 22 below and note this where it matters.

The script

Edit the three variables at the top. ADMIN_PUBKEY must be your public key (the .pub file), pasted in full on one line.

#!/usr/bin/env bash
set -euo pipefail

# ---- edit these three before running --------------------------------
ADMIN_USER="admin"                              # non-root sudo account to create
ADMIN_PUBKEY="ssh-ed25519 AAAAC3Nz... you@host" # paste your PUBLIC key, one line
SSH_PORT="22"                                    # leave 22 unless you know why
# ---------------------------------------------------------------------

# Must be root
if [[ "${EUID}" -ne 0 ]]; then
  echo "Run this as root or with sudo." >&2
  exit 1
fi

# Refuse to run with an empty/placeholder key -- this is the lockout guard
if [[ -z "${ADMIN_PUBKEY}" || "${ADMIN_PUBKEY}" == *"AAAAC3Nz... you@host"* ]]; then
  echo "Set ADMIN_PUBKEY to your real public key first." >&2
  exit 1
fi

export DEBIAN_FRONTEND=noninteractive

echo "==> Updating package lists and installing packages"
apt-get update
apt-get install -y ufw fail2ban unattended-upgrades

echo "==> Creating ${ADMIN_USER} (no password, key-only) and granting sudo"
if ! id "${ADMIN_USER}" >/dev/null 2>&1; then
  adduser --disabled-password --gecos "" "${ADMIN_USER}"
fi
usermod -aG sudo "${ADMIN_USER}"

echo "==> Installing SSH key for ${ADMIN_USER}"
install -d -m 700 -o "${ADMIN_USER}" -g "${ADMIN_USER}" "/home/${ADMIN_USER}/.ssh"
echo "${ADMIN_PUBKEY}" > "/home/${ADMIN_USER}/.ssh/authorized_keys"
chmod 600 "/home/${ADMIN_USER}/.ssh/authorized_keys"
chown "${ADMIN_USER}:${ADMIN_USER}" "/home/${ADMIN_USER}/.ssh/authorized_keys"

echo "==> Writing SSH hardening drop-in"
cat > /etc/ssh/sshd_config.d/99-hardening.conf <<EOF
# Managed by first-boot hardening script
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication no
EOF

echo "==> Validating SSH config before restart"
sshd -t   # aborts here (set -e) if the config is invalid

echo "==> Configuring UFW: deny inbound, allow outbound, allow SSH"
ufw default deny incoming
ufw default allow outgoing
ufw allow "${SSH_PORT}/tcp"   # allow SSH before enabling, or you cut yourself off
ufw --force enable            # --force skips the interactive y/n prompt

echo "==> Enabling automatic security updates"
cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
EOF

echo "==> Enabling fail2ban for SSH"
cat > /etc/fail2ban/jail.local <<'EOF'
[sshd]
enabled = true
maxretry = 5
bantime = 1h
EOF
systemctl enable --now fail2ban
systemctl restart fail2ban

echo "==> Applying SSH changes"
systemctl restart ssh

echo "==> Done. Open a NEW session as ${ADMIN_USER} to confirm before logging out."

A few notes on the non-obvious parts. adduser --disabled-password --gecos "" creates the account with no password set and no interactive prompts, so login is key-only from the start. The SSH policy goes in a drop-in file (/etc/ssh/sshd_config.d/99-hardening.conf) rather than editing the main sshd_config — it's cleaner and trivial to remove. sshd -t validates syntax before the restart, so a typo can't leave you with a dead SSH daemon. And UFW's SSH rule is added before ufw --force enable, which is the whole game: enable the firewall first without that rule and you drop your own connection.

If you prefer, ufw allow OpenSSH uses UFW's named application profile instead of ${SSH_PORT}/tcp; I use the explicit port so the SSH_PORT variable stays honest.

On the SSH port

I keep port 22. Moving SSH to a non-standard port is obscurity, not security, and on Ubuntu 24.04 it takes an extra step: because SSH is socket-activated there, the listening port is defined in ssh.socket, not sshd_config. If you genuinely need a different port on 24.04, change the socket unit — see the OpenSSH server documentation and man systemd.socket for the exact ListenStream syntax rather than trusting this script to do it. On 22.04 you'd add a Port line to the drop-in and the matching ufw allow rule.

Verify it worked

From a new terminal — do not close your current session yet:

ssh admin@your.server.ip        # should log in with your key, no password prompt

Then, on the server, confirm each piece:

# SSH policy actually in effect (reads the live, effective config)
sudo sshd -T | grep -Ei 'permitrootlogin|passwordauthentication|pubkeyauthentication'

# Firewall status and rules
sudo ufw status verbose

# Your user is in the sudo group
id admin

# fail2ban running and watching sshd
systemctl is-active fail2ban
sudo fail2ban-client status sshd

# Auto-updates config present
cat /etc/apt/apt.conf.d/20auto-upgrades

You want permitrootlogin no, passwordauthentication no, pubkeyauthentication yes; UFW reporting Status: active with your SSH rule; fail2ban active with an sshd jail listed.

Undo / roll back

Everything except the account creation is reversible while you still have a working session:

# Re-enable root login / password auth by removing the drop-in, then restart
sudo rm /etc/ssh/sshd_config.d/99-hardening.conf
sudo sshd -t && sudo systemctl restart ssh

# Turn the firewall off
sudo ufw disable

# Stop and disable fail2ban
sudo systemctl disable --now fail2ban

# Turn off unattended upgrades (set both values to 0)
sudo sed -i 's/"1"/"0"/g' /etc/apt/apt.conf.d/20auto-upgrades

The created user and its authorized_keys are the one thing I'd leave in place; remove the account manually with the standard deluser tool only if you're sure nothing depends on it. For the official reference on any directive here, see the OpenSSH sshd_config manual, the Ubuntu Server documentation for UFW and unattended-upgrades, and the fail2ban manual — check them for exact syntax before you adapt this to a non-default setup.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.

Harden a Fresh Ubuntu Server with a First-Boot Bash Script