# Setup recipe: a secure baseline for a new server

> A ready-made first-boot script that adds a sudo user, turns off root and password logins over SSH, and sets up a firewall, fail2ban and security updates.

Source: https://vpsnine.com/help/secure-server-setup-script · Updated: 2026-10-21

This is a [setup script](https://vpsnine.com/help/setup-scripts) that does the first-hour security chores on a new server, so it comes up that way. It runs once, as root, on the first boot. It works on Ubuntu, Debian, AlmaLinux, Rocky Linux and Fedora, and stops with a clear message on anything else.

It does five things:

- Creates a user called `deploy` with `sudo`, and copies root's SSH keys to it.
- Changes SSH to keys only, with no root login, in its own file, `/etc/ssh/sshd_config.d/01-vps9-hardening.conf`. It checks the result with `sshd -t` and takes the file out again if `sshd` refuses it.
- Turns on the host firewall with only SSH (port 22) allowed in: `ufw` on Ubuntu and Debian, `firewalld` on AlmaLinux, Rocky Linux and Fedora.
- Installs [fail2ban](https://vpsnine.com/help/fail2ban), which bans an address for an hour after five failed SSH logins in ten minutes.
- Turns on [automatic security updates](https://vpsnine.com/help/automatic-security-updates): security fixes only, with no automatic reboot.

It needs an SSH key on the server. If root has no key, it stops before changing anything: with password logins off and no key, you'd be locked out of SSH (the [serial console](https://vpsnine.com/help/serial-console) would still let you in).

## Deploy it

[Deploy with VPSNine](https://my.vpsnine.com/servers/new?recipe=secure-baseline): this opens the order form with the script already in the setup script field (you log in first). Change `USER_NAME` at the top if you want a different name, pick at least one SSH key, and order.

With the [API](https://vpsnine.com/help/api-quickstart) or the [CLI](https://vpsnine.com/help/install-the-vps9-cli), save the script below as `secure-baseline.sh` and send it as the setup script:

```bash
vps9 servers create --plan budget --image debian-13 --hostname web-1 --key key_… --setup-script ./secure-baseline.sh
```

You can also run it by hand on a server you already have: copy it over and run `sudo bash secure-baseline.sh`.

## The script

```bash
#!/bin/bash
# VPSNine setup recipe: secure baseline
# https://vpsnine.com/help/secure-server-setup-script
# A sudo user with root's SSH keys, SSH with keys only and no root login, a host firewall that
# allows SSH, fail2ban for SSH, and automatic security updates.
set -euo pipefail
umask 022

# ---- settings: change these before you run it ----
USER_NAME=deploy   # the sudo user to create; it gets root's SSH keys

# Which family of distributions this is: Debian and Ubuntu, or AlmaLinux, Rocky Linux and Fedora.
. /etc/os-release
case " ${ID:-} ${ID_LIKE:-} " in
  *" debian "*|*" ubuntu "*) family=debian ;;
  *" rhel "*|*" fedora "*|*" centos "*) family=rhel ;;
  *) echo "This recipe supports Ubuntu, Debian, AlmaLinux, Rocky Linux and Fedora, not ${PRETTY_NAME:-this system}" >&2; exit 1 ;;
esac

case "$USER_NAME" in
  ""|*[!a-z0-9_-]*) echo "USER_NAME must be lower-case letters, digits, _ and -" >&2; exit 1 ;;
esac
# Turning off password logins with no key in place would lock you out of SSH: stop before changing anything.
if [ ! -s /root/.ssh/authorized_keys ]; then
  echo "root has no SSH keys (/root/.ssh/authorized_keys): order the server with a key, or add one, then run this again" >&2
  exit 1
fi
if ! grep -qsE '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/' /etc/ssh/sshd_config; then
  echo "/etc/ssh/sshd_config doesn't read /etc/ssh/sshd_config.d/: this system is too old for this recipe" >&2
  exit 1
fi

echo "== Installing the packages"
if [ "$family" = debian ]; then
export DEBIAN_FRONTEND=noninteractive
apt-get -o DPkg::Lock::Timeout=300 update
apt-get -o DPkg::Lock::Timeout=300 install -y sudo ufw fail2ban python3-systemd unattended-upgrades
else
# fail2ban comes from EPEL on AlmaLinux and Rocky Linux; Fedora has it in its own repositories.
if [ "$ID" != fedora ]; then dnf -y install epel-release; fi
dnf -y install sudo firewalld fail2ban python3-systemd
dnf -y install dnf-automatic || dnf -y install dnf5-plugin-automatic
fi

echo "== The sudo user $USER_NAME"
id "$USER_NAME" >/dev/null 2>&1 || useradd -m -s /bin/bash "$USER_NAME"
# sudo without a password, since the account has none (set one with passwd and drop NOPASSWD: if you prefer)
echo "$USER_NAME ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/90-$USER_NAME"
chmod 440 "/etc/sudoers.d/90-$USER_NAME"
install -d -m 700 -o "$USER_NAME" -g "$USER_NAME" "/home/$USER_NAME/.ssh"
install -m 600 -o "$USER_NAME" -g "$USER_NAME" /root/.ssh/authorized_keys "/home/$USER_NAME/.ssh/authorized_keys"
if command -v restorecon >/dev/null 2>&1; then restorecon -R "/home/$USER_NAME/.ssh"; fi

echo "== SSH: keys only, no root login"
# sshd keeps the first value it reads for each setting, and reads sshd_config.d in name order: 01- comes first.
cat > /etc/ssh/sshd_config.d/01-vps9-hardening.conf <<'EOF'
# VPSNine setup recipe: secure baseline
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 4
EOF
if ! sshd -t; then
  rm -f /etc/ssh/sshd_config.d/01-vps9-hardening.conf
  echo "sshd refused the new settings, so they were removed again" >&2
  exit 1
fi
for unit in ssh sshd; do
  if systemctl is-active --quiet "$unit"; then systemctl reload "$unit"; break; fi
done

echo "== Firewall: SSH in, everything else in refused"
if [ "$family" = debian ]; then
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw --force enable
else
systemctl enable --now firewalld
firewall-cmd --permanent --add-service=ssh
firewall-cmd --reload
fi

echo "== fail2ban for SSH"
install -d -m 0755 /etc/fail2ban/jail.d
cat > /etc/fail2ban/jail.d/vps9-sshd.local <<'EOF'
# VPSNine setup recipe: secure baseline. Five failed logins in 10 minutes: banned for an hour.
[sshd]
enabled = true
backend = systemd
maxretry = 5
findtime = 10m
bantime = 1h
EOF
if [ "$family" = debian ]; then printf 'banaction = ufw\n' >> /etc/fail2ban/jail.d/vps9-sshd.local; fi
systemctl enable fail2ban
systemctl restart fail2ban

echo "== Automatic security updates (no automatic reboot)"
if [ "$family" = debian ]; then
cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
EOF
systemctl enable apt-daily.timer apt-daily-upgrade.timer
else
install -d -m 0755 /etc/dnf
cat > /etc/dnf/automatic.conf <<'EOF'
# VPSNine setup recipe: install security updates every day, and never reboot on our own.
[commands]
upgrade_type = security
download_updates = yes
apply_updates = yes
reboot = never

[emitters]
emit_via = stdio
EOF
if systemctl cat dnf5-automatic.timer >/dev/null 2>&1; then t=dnf5-automatic; else t=dnf-automatic; fi
systemctl enable --now "${t}.timer"
fi

echo "== Done. Log in as: ssh $USER_NAME@<your server's address>"
```

## Check the result

Log in as the new user, not root:

```bash
ssh deploy@203.0.113.42
sudo tail -n 20 /var/log/vps9-setup.log
cat /var/lib/vps9-setup.exit
```

`0` in the exit file means every step worked. Then check each part:

```bash
sudo sshd -T | grep -E 'permitrootlogin|passwordauthentication'
sudo ufw status                # Ubuntu and Debian
sudo firewall-cmd --list-all   # AlmaLinux, Rocky Linux and Fedora
sudo fail2ban-client status sshd
```

## Before you open more ports

The host firewall now refuses everything but SSH. When you add a web server or anything else that listens, allow its port too: `sudo ufw allow 443/tcp`, or `sudo firewall-cmd --permanent --add-service=https` then `sudo firewall-cmd --reload`. The [server firewall in the panel](https://vpsnine.com/help/server-firewall) is separate: it filters before traffic reaches the server, and this script doesn't change it.

Docker publishes container ports by writing its own firewall rules, which go around `ufw`. If you add the [Docker host recipe](https://vpsnine.com/help/docker-host-setup-script), publish ports on `127.0.0.1` and put a reverse proxy in front, or use the panel firewall.

The [SSH hardening checklist](https://vpsnine.com/help/ssh-hardening-checklist) explains each SSH setting, and the [first-hour checklist](https://vpsnine.com/help/first-hour-checklist) covers the rest of a new server.
